diff --git a/package.json b/package.json index 774e27eb..646eba93 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "prettier:check": "prettier --experimental-cli --check .", "prettier:write": "prettier --experimental-cli --write .", "test": "npm test --workspace react-native-node-api --workspace cmake-rn --workspace gyp-to-cmake --workspace node-addon-examples", - "bootstrap": "node --run build && npm run bootstrap --workspaces --if-present", + "bootstrap": "node --run build && node scripts/bootstrap.ts", "changeset": "changeset", "release": "changeset publish", "prerelease": "node --run build && npm run prerelease --workspaces --if-present && node --run publint", diff --git a/scripts/bootstrap.ts b/scripts/bootstrap.ts new file mode 100644 index 00000000..1e993061 --- /dev/null +++ b/scripts/bootstrap.ts @@ -0,0 +1,68 @@ +import path from "node:path"; +import assert from "node:assert/strict"; +import cp from "node:child_process"; +import fs from "node:fs"; + +// Bootstrap every workspace sequentially, failing fast on the first error. +// +// We deliberately don't use "npm run bootstrap --workspaces --if-present" here: +// npm keeps iterating the remaining workspaces even after one of them fails and +// only surfaces a non-zero exit code once they've all run. Because later +// workspaces depend on artifacts produced by earlier ones (e.g. the +// weak-node-api xcframework consumed by node-addon-examples, node-tests and +// ferric-example), a failure in an early workspace produces a cascade of +// confusing secondary errors, hiding the actual root cause. Iterating ourselves +// lets us stop at the first failing workspace. + +const rootDir = path.resolve(import.meta.dirname, ".."); + +function readPackageJson(packageDir: string): Record { + const contents = fs.readFileSync( + path.join(packageDir, "package.json"), + "utf8", + ); + const parsed = JSON.parse(contents) as unknown; + assert( + typeof parsed === "object" && parsed !== null, + `Expected an object in ${packageDir}/package.json`, + ); + return parsed as Record; +} + +const rootPackage = readPackageJson(rootDir); +assert( + Array.isArray(rootPackage.workspaces), + "Expected a 'workspaces' array in the root package.json", +); + +for (const workspace of rootPackage.workspaces) { + assert(typeof workspace === "string"); + // The workspaces are declared as concrete directories – reject globs rather + // than silently skipping workspaces they were meant to expand to. + assert( + !workspace.includes("*"), + `Glob workspace patterns are not supported ('${workspace}')`, + ); + + const workspaceDir = path.resolve(rootDir, workspace); + const { name, scripts } = readPackageJson(workspaceDir); + assert(typeof name === "string"); + + const hasBootstrap = + typeof scripts === "object" && scripts !== null && "bootstrap" in scripts; + if (!hasBootstrap) { + continue; + } + + console.log(`Bootstrapping '${name}'`); + const { status } = cp.spawnSync( + "npm", + ["run", "bootstrap", "--workspace", name], + { stdio: "inherit" }, + ); + assert.equal( + status, + 0, + `Bootstrapping '${name}' failed (status = ${status})`, + ); +}