diff --git a/packages/test/test-version-utils/.npmignore b/packages/test/test-version-utils/.npmignore index 83fce4bddd3c..1e67af1c625a 100644 --- a/packages/test/test-version-utils/.npmignore +++ b/packages/test/test-version-utils/.npmignore @@ -8,3 +8,5 @@ lib/test # Default .npmignore rules only include the root node_modules. # Adding an explicit rule here ensures we don't publish the compat_workspace's node_modules. **/node_modules +# Preserve the .npmrc file in compat-workspaces/full so that its settings apply to the postinstall script in test-version-utils +!compat-workspaces/full/.npmrc diff --git a/packages/test/test-version-utils/compat-workspaces/full/.npmrc b/packages/test/test-version-utils/compat-workspaces/full/.npmrc index 1aa498c4e2ee..33b51528c47f 100644 --- a/packages/test/test-version-utils/compat-workspaces/full/.npmrc +++ b/packages/test/test-version-utils/compat-workspaces/full/.npmrc @@ -1 +1,4 @@ node-linker=isolated +frozen-lockfile=true +engine-strict=true +strict-peer-dependencies=true diff --git a/packages/test/test-version-utils/compat-workspaces/full/package.json b/packages/test/test-version-utils/compat-workspaces/full/package.json index 7c8dd93cfba6..6714b0df44ee 100644 --- a/packages/test/test-version-utils/compat-workspaces/full/package.json +++ b/packages/test/test-version-utils/compat-workspaces/full/package.json @@ -11,6 +11,6 @@ "license": "MIT", "author": "Microsoft and contributors", "scripts": { - "preinstall": "node ../scripts/only-pnpm.cjs" + "preinstall": "node scripts/only-pnpm.cjs" } } diff --git a/packages/test/test-version-utils/compat-workspaces/scripts/only-pnpm.cjs b/packages/test/test-version-utils/compat-workspaces/full/scripts/only-pnpm.cjs similarity index 100% rename from packages/test/test-version-utils/compat-workspaces/scripts/only-pnpm.cjs rename to packages/test/test-version-utils/compat-workspaces/full/scripts/only-pnpm.cjs diff --git a/packages/test/test-version-utils/package.json b/packages/test/test-version-utils/package.json index 0f4b8e15972a..9ba06548f76e 100644 --- a/packages/test/test-version-utils/package.json +++ b/packages/test/test-version-utils/package.json @@ -43,7 +43,7 @@ "eslint:fix": "eslint --quiet --format stylish src --fix --fix-type problem,suggestion,layout", "format": "npm run format:biome", "format:biome": "biome check . --write", - "postinstall": "pnpm --dir compat-workspaces/full install --frozen-lockfile", + "postinstall": "pnpm --dir compat-workspaces/full install", "lint": "fluid-build . --task lint", "lint:fix": "fluid-build . --task eslint:fix --task format", "test": "npm run test:mocha", diff --git a/packages/test/test-version-utils/scripts/updateCompatVersions.ts b/packages/test/test-version-utils/scripts/updateCompatVersions.ts index 810e07c2a79f..4daad19d7251 100644 --- a/packages/test/test-version-utils/scripts/updateCompatVersions.ts +++ b/packages/test/test-version-utils/scripts/updateCompatVersions.ts @@ -16,17 +16,22 @@ * 3. Writes `compat-workspaces/generated-versions.cjs` with the resolved exact versions. * 4. Creates or updates per-version `package.json` files in `compat-workspaces/full/`. * 5. Removes version directories that are no longer needed. - * 6. Runs `pnpm install --no-frozen-lockfile` in the workspace to regenerate the lockfile. + * 6. Runs `pnpm install --no-frozen-lockfile [extra args]` in the workspace to regenerate the lockfile. * * Commit all files produced by this script, including the updated `pnpm-lock.yaml`. * + * Any extra arguments passed to this script are forwarded verbatim to `pnpm install`. For example: + * + * pnpm run update-compat-versions -- --lockfile-only + * pnpm run update-compat-versions -- --store-dir /custom/store + * * MACHINE-MAINTAINED (do not edit by hand — regenerated by this script): * compat-workspaces/generated-versions.cjs * compat-workspaces/full//package.json (one per version) * compat-workspaces/full/pnpm-lock.yaml */ -import { execSync } from "node:child_process"; +import { execFileSync, execSync } from "node:child_process"; import { existsSync, mkdirSync, @@ -204,12 +209,20 @@ function removeStaleVersionDirs(workspaceDir: string, keepVersions: Set) return removed; } -function pnpmInstallWorkspace(workspaceDir: string): void { - console.log(`\nRunning pnpm install in ${path.relative(pkgRoot, workspaceDir)} ...`); - execSync(`pnpm install --no-frozen-lockfile`, { +function pnpmInstallWorkspace(workspaceDir: string, extraPnpmArgs: string[] = []): void { + const args = ["install", "--no-frozen-lockfile", ...extraPnpmArgs]; + console.log( + `\nRunning pnpm ${args.join(" ")} in ${path.relative(pkgRoot, workspaceDir)} ...`, + ); + // On Windows, pnpm is a .cmd shim, which Node refuses to spawn without shell: true + // (CVE-2024-27980). With shell: true on Windows, Node escapes array args for cmd.exe, + // so this still passes arguments verbatim — no shell parsing of caller-provided values. + const isWindows = process.platform === "win32"; + execFileSync(isWindows ? "pnpm.cmd" : "pnpm", args, { cwd: workspaceDir, env: { ...process.env, NODE_OPTIONS: "" }, stdio: "inherit", + shell: isWindows, }); } @@ -218,6 +231,9 @@ function pnpmInstallWorkspace(workspaceDir: string): void { // --------------------------------------------------------------------------- async function main(): Promise { + // Any arguments after the script name are forwarded to pnpm install. + const extraPnpmArgs = process.argv.slice(2); + // Read current package version const pkgJsonPath = path.join(pkgRoot, "package.json"); const { version: pkgVer } = JSON.parse(readFileSync(pkgJsonPath, "utf8")) as { @@ -331,7 +347,7 @@ async function main(): Promise { if (anyChanged) { // Regenerate lockfile - pnpmInstallWorkspace(fullDir); + pnpmInstallWorkspace(fullDir, extraPnpmArgs); console.log("\nDone. Commit all changes in compat-workspaces/ to the repository."); } else {