From b651d10ced51fc873162c28eb571350669c446c7 Mon Sep 17 00:00:00 2001 From: Radoslaw Nowacki Date: Tue, 15 Sep 2026 12:28:36 +0200 Subject: [PATCH 1/3] fix(scripts): escape brace in u-flag regex in check-expo-preview An unescaped `}` in a `u`-flag regex is a malformed quantifier, not a literal. The pattern `/\n}\s*$/u` raised `SyntaxError: Lone quantifier brackets` at parse time, so check-expo-preview.ts never loaded and the Expo preview scheduled workflow failed at its first step on every run since 1e08ea5 (2026-07-14). The existing suite in scripts/__tests__ already covered this and now passes (9/9); it was simply never wired into CI. That gap is addressed separately. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/check-expo-preview.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check-expo-preview.ts b/scripts/check-expo-preview.ts index 49fe4b1a..752655d6 100644 --- a/scripts/check-expo-preview.ts +++ b/scripts/check-expo-preview.ts @@ -198,7 +198,7 @@ export function ensureConsumerNavigationSpec(contents: string): string { } const updated = contents.replace( - /\n}\s*$/u, + /\n\}\s*$/u, `${CONSUMER_ROAD_TEST_NAVIGATION_METHODS}\n}` ); From 3d6520f9a06db59f644a1a33c2bf0fb89ec27a7b Mon Sep 17 00:00:00 2001 From: Radoslaw Nowacki Date: Tue, 15 Sep 2026 12:32:51 +0200 Subject: [PATCH 2/3] fix(scripts): do not run the check-expo-preview CLI on import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main() was invoked unconditionally at module scope and fetches registry.npmjs.org, so merely importing the module ran the CLI and hit the network. The returned promise had no .catch(), so a registry outage became an unhandled rejection that exits the process non-zero — which would have failed the whole test suite for reasons unrelated to the code under test. Guard the invocation on direct execution and handle the rejection. The workflow invokes the script by path at all three call sites, so its behaviour is unchanged; verified exit 0 on success and exit 1 with a readable error on failure. Prerequisite for running this suite in CI. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/__tests__/check-expo-preview.test.ts | 28 ++++++++++++++++++++ scripts/check-expo-preview.ts | 16 +++++++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/scripts/__tests__/check-expo-preview.test.ts b/scripts/__tests__/check-expo-preview.test.ts index b45e78f0..0911c7e7 100644 --- a/scripts/__tests__/check-expo-preview.test.ts +++ b/scripts/__tests__/check-expo-preview.test.ts @@ -1,5 +1,8 @@ import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import path from 'node:path'; import test from 'node:test'; +import { fileURLToPath } from 'node:url'; import { ensureConsumerNavigationSpec, @@ -136,3 +139,28 @@ test('fails loudly when expo dependency is missing', () => { /Could not locate dependencies\.expo in ExpoAppPreview\/package\.json/, }); }); + +const repoRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../..' +); + +test('importing the module does not run the CLI or reach the network', () => { + // main() used to be invoked unconditionally at module scope, so merely + // importing this file fetched registry.npmjs.org — and an unhandled + // rejection from that fetch failed the whole suite. Importing must be + // a pure, offline operation. + const result = spawnSync( + process.execPath, + [ + '--experimental-strip-types', + '--no-warnings', + '--eval', + "import('./scripts/check-expo-preview.ts').then(() => console.log('IMPORT_ONLY'));", + ], + { cwd: repoRoot, encoding: 'utf8' } + ); + + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stdout.trim(), 'IMPORT_ONLY'); +}); diff --git a/scripts/check-expo-preview.ts b/scripts/check-expo-preview.ts index 752655d6..9b88b877 100644 --- a/scripts/check-expo-preview.ts +++ b/scripts/check-expo-preview.ts @@ -8,7 +8,7 @@ import { readFileSync, writeFileSync, } from 'node:fs'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; interface CliOptions { expoVersion?: string; @@ -395,4 +395,16 @@ async function main(): Promise { console.log(`Applied: ${applied}`); } -main(); +const isDirectInvocation = + process.argv[1] !== undefined && + import.meta.url === pathToFileURL(process.argv[1]).href; + +// Importing this module (e.g. from the test suite) must not run the CLI — +// main() fetches the npm registry, and an unhandled rejection from that +// fetch would fail the importer for reasons unrelated to it. +if (isDirectInvocation) { + main().catch((error: unknown) => { + console.error(error); + process.exitCode = 1; + }); +} From 78e21b35e8dd4cae6c8fd59d4f27f4194f05da06 Mon Sep 17 00:00:00 2001 From: Radoslaw Nowacki Date: Tue, 15 Sep 2026 12:33:32 +0200 Subject: [PATCH 3/3] ci: run scripts/__tests__ suite in build-lint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/__tests__ holds 18 node:test cases across three files, covering check-expo-preview, the Gradle plugin release notes generator and the plugin version sync. None had ever executed anywhere: scripts/ is not a yarn workspace, the root package has no test runner, and CI only ran turbo test filtered to ./packages/* and ./apps/*. That gap is why the u-flag regex SyntaxError reached main and broke every scheduled Expo preview run since 2026-07-14. Add a root test:scripts script and call it from build-lint. Gate the job on a new `scripts` paths-filter output rather than folding scripts/** into `ci` — `ci` also gates the Android and iOS E2E jobs, and a script edit should not trigger ~30 minutes of emulator runs. Verified by reintroducing the regex defect: yarn test:scripts fails with the original SyntaxError. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 11 ++++++++++- package.json | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 229ba5dc..a7473815 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,7 @@ jobs: androidapp: ${{ steps.filter.outputs.androidapp }} appleapp: ${{ steps.filter.outputs.appleapp }} gradle-plugins: ${{ steps.filter.outputs.gradle-plugins }} + scripts: ${{ steps.filter.outputs.scripts }} ci: ${{ steps.filter.outputs.ci }} steps: - name: Checkout @@ -56,6 +57,8 @@ jobs: - 'apps/brownfield-example-shared-tests/**' gradle-plugins: - 'gradle-plugins/**' + scripts: + - 'scripts/**' ci: - '.github/**' @@ -81,7 +84,10 @@ jobs: name: Build, lint, typecheck & Jest runs-on: ubuntu-latest needs: filter - if: needs.filter.outputs.packages == 'true' || needs.filter.outputs.ci == 'true' + if: | + needs.filter.outputs.packages == 'true' || + needs.filter.outputs.scripts == 'true' || + needs.filter.outputs.ci == 'true' steps: - name: Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 @@ -104,6 +110,9 @@ jobs: - name: Run packages' tests (Jest & vitest) run: yarn test:packages + - name: Run repo script tests (node:test) + run: yarn test:scripts + - name: Test Brownfield CLI (version) run: | yarn workspace @callstack/react-native-brownfield brownfield --version diff --git a/package.json b/package.json index 4dc635da..a88afff0 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "build": "turbo run build", "test:packages": "turbo run test --filter='./packages/*'", "test:apps": "turbo run test --filter='./apps/*'", + "test:scripts": "node --experimental-strip-types --no-warnings --test 'scripts/__tests__/**/*.test.ts'", "dev": "yarn workspaces foreach -Api run dev", "ci:version": "changeset version && yarn install --no-immutable && node --experimental-strip-types --no-warnings ./scripts/consolidate-changelog.ts", "ci:publish": "changeset publish",