diff --git a/packages/vscode/e2e/fixtures/fmt-missing-config-dependency/pnpm-workspace.yaml b/packages/vscode/e2e/fixtures/fmt-missing-config-dependency/pnpm-workspace.yaml new file mode 100644 index 0000000..b15641f --- /dev/null +++ b/packages/vscode/e2e/fixtures/fmt-missing-config-dependency/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +# E2E fixture install config; rationale in packages/vscode/e2e/setupFixtures.mjs. +minimumReleaseAgeExclude: + - rstack + - '@rslint/core' + - '@rstest/core' + - '@rsbuild/core' + - '@rslib/core' + - '@rstackjs/*' + - rsbuild-plugin-dts + - '@rspack/*' diff --git a/packages/vscode/e2e/fixtures/rslint/pnpm-workspace.yaml b/packages/vscode/e2e/fixtures/rslint/pnpm-workspace.yaml new file mode 100644 index 0000000..b15641f --- /dev/null +++ b/packages/vscode/e2e/fixtures/rslint/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +# E2E fixture install config; rationale in packages/vscode/e2e/setupFixtures.mjs. +minimumReleaseAgeExclude: + - rstack + - '@rslint/core' + - '@rstest/core' + - '@rsbuild/core' + - '@rslib/core' + - '@rstackjs/*' + - rsbuild-plugin-dts + - '@rspack/*' diff --git a/packages/vscode/e2e/fixtures/rstack/pnpm-workspace.yaml b/packages/vscode/e2e/fixtures/rstack/pnpm-workspace.yaml new file mode 100644 index 0000000..b15641f --- /dev/null +++ b/packages/vscode/e2e/fixtures/rstack/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +# E2E fixture install config; rationale in packages/vscode/e2e/setupFixtures.mjs. +minimumReleaseAgeExclude: + - rstack + - '@rslint/core' + - '@rstest/core' + - '@rsbuild/core' + - '@rslib/core' + - '@rstackjs/*' + - rsbuild-plugin-dts + - '@rspack/*' diff --git a/packages/vscode/e2e/fixtures/rstest/pnpm-workspace.yaml b/packages/vscode/e2e/fixtures/rstest/pnpm-workspace.yaml new file mode 100644 index 0000000..b15641f --- /dev/null +++ b/packages/vscode/e2e/fixtures/rstest/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +# E2E fixture install config; rationale in packages/vscode/e2e/setupFixtures.mjs. +minimumReleaseAgeExclude: + - rstack + - '@rslint/core' + - '@rstest/core' + - '@rsbuild/core' + - '@rslib/core' + - '@rstackjs/*' + - rsbuild-plugin-dts + - '@rspack/*' diff --git a/packages/vscode/e2e/lint/fixtures/dependency-recovery/pnpm-workspace.yaml b/packages/vscode/e2e/lint/fixtures/dependency-recovery/pnpm-workspace.yaml new file mode 100644 index 0000000..b15641f --- /dev/null +++ b/packages/vscode/e2e/lint/fixtures/dependency-recovery/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +# E2E fixture install config; rationale in packages/vscode/e2e/setupFixtures.mjs. +minimumReleaseAgeExclude: + - rstack + - '@rslint/core' + - '@rstest/core' + - '@rsbuild/core' + - '@rslib/core' + - '@rstackjs/*' + - rsbuild-plugin-dts + - '@rspack/*' diff --git a/packages/vscode/e2e/lint/fixtures/pnpm-workspace.yaml b/packages/vscode/e2e/lint/fixtures/pnpm-workspace.yaml new file mode 100644 index 0000000..b15641f --- /dev/null +++ b/packages/vscode/e2e/lint/fixtures/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +# E2E fixture install config; rationale in packages/vscode/e2e/setupFixtures.mjs. +minimumReleaseAgeExclude: + - rstack + - '@rslint/core' + - '@rstest/core' + - '@rsbuild/core' + - '@rslib/core' + - '@rstackjs/*' + - rsbuild-plugin-dts + - '@rspack/*' diff --git a/packages/vscode/e2e/lint/suite-dependency-recovery/dependency-recovery.test.ts b/packages/vscode/e2e/lint/suite-dependency-recovery/dependency-recovery.test.ts index d410c0b..a1467c5 100644 --- a/packages/vscode/e2e/lint/suite-dependency-recovery/dependency-recovery.test.ts +++ b/packages/vscode/e2e/lint/suite-dependency-recovery/dependency-recovery.test.ts @@ -1,15 +1,12 @@ import * as assert from 'node:assert'; -import { execFile as execFileCallback } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; -import { promisify } from 'node:util'; import * as vscode from 'vscode'; +import { pnpmInstallFrozen } from '../../pnpmInstall'; import type { StackState } from '../../../src/types'; import { waitForRslintDiagnostics } from '../utils/diagnostics'; import { extensionExports } from '../utils/extension'; -const execFile = promisify(execFileCallback); - function lintExports(): { getFolderStates(): ReadonlyMap; } { @@ -61,17 +58,7 @@ suite('Rslint dependency polling recovery', function () { const beforeContents = fs.readFileSync(lockfile); const beforeMtime = fs.statSync(lockfile).mtimeMs; - await execFile( - 'pnpm', - ['install', '--frozen-lockfile', '--ignore-scripts'], - { - cwd: root, - timeout: 90_000, - // Match setupFixtures.mjs/run.mjs: Windows needs a shell for pnpm's - // .cmd shim. All arguments are fixed safe tokens; cwd is not interpolated. - shell: process.platform === 'win32', - }, - ); + await pnpmInstallFrozen(root); assert.deepStrictEqual( fs.readFileSync(lockfile), diff --git a/packages/vscode/e2e/pnpmInstall.ts b/packages/vscode/e2e/pnpmInstall.ts new file mode 100644 index 0000000..23757a6 --- /dev/null +++ b/packages/vscode/e2e/pnpmInstall.ts @@ -0,0 +1,34 @@ +import { execFile as execFileCallback } from 'node:child_process'; +import { promisify } from 'node:util'; + +const execFile = promisify(execFileCallback); + +/** + * Runs `pnpm install --frozen-lockfile --ignore-scripts` in `cwd`. pnpm 11 + * prints install errors (e.g. ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION) to + * stdout, while execFile's error message carries only stderr, so a failure + * rethrows with both streams appended. + */ +export async function pnpmInstallFrozen(cwd: string): Promise { + try { + await execFile( + 'pnpm', + ['install', '--frozen-lockfile', '--ignore-scripts'], + { + cwd, + timeout: 90_000, + // Windows needs a shell for pnpm's .cmd shim; the arguments are fixed + // safe tokens and cwd is not interpolated. + shell: process.platform === 'win32', + }, + ); + } catch (error) { + const { stdout = '', stderr = '' } = error as { + stdout?: string; + stderr?: string; + }; + throw new Error(`${(error as Error).message}\n${stdout}${stderr}`, { + cause: error, + }); + } +} diff --git a/packages/vscode/e2e/rstest/fixtures/workspace-1/pnpm-workspace.yaml b/packages/vscode/e2e/rstest/fixtures/workspace-1/pnpm-workspace.yaml new file mode 100644 index 0000000..b15641f --- /dev/null +++ b/packages/vscode/e2e/rstest/fixtures/workspace-1/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +# E2E fixture install config; rationale in packages/vscode/e2e/setupFixtures.mjs. +minimumReleaseAgeExclude: + - rstack + - '@rslint/core' + - '@rstest/core' + - '@rsbuild/core' + - '@rslib/core' + - '@rstackjs/*' + - rsbuild-plugin-dts + - '@rspack/*' diff --git a/packages/vscode/e2e/rstest/fixtures/workspace-2/pnpm-workspace.yaml b/packages/vscode/e2e/rstest/fixtures/workspace-2/pnpm-workspace.yaml new file mode 100644 index 0000000..b15641f --- /dev/null +++ b/packages/vscode/e2e/rstest/fixtures/workspace-2/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +# E2E fixture install config; rationale in packages/vscode/e2e/setupFixtures.mjs. +minimumReleaseAgeExclude: + - rstack + - '@rslint/core' + - '@rstest/core' + - '@rsbuild/core' + - '@rslib/core' + - '@rstackjs/*' + - rsbuild-plugin-dts + - '@rspack/*' diff --git a/packages/vscode/e2e/rstest/suite-dependency-recovery/dependency-recovery.test.ts b/packages/vscode/e2e/rstest/suite-dependency-recovery/dependency-recovery.test.ts index d3e5704..74e80d9 100644 --- a/packages/vscode/e2e/rstest/suite-dependency-recovery/dependency-recovery.test.ts +++ b/packages/vscode/e2e/rstest/suite-dependency-recovery/dependency-recovery.test.ts @@ -1,14 +1,11 @@ import assert from 'node:assert/strict'; -import { execFile as execFileCallback } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; -import { promisify } from 'node:util'; import vscode from 'vscode'; +import { pnpmInstallFrozen } from '../../pnpmInstall'; import type { RstackExtensionExports } from '../../../src/types'; import { getProjectItems, getRstestExports, waitFor } from '../suite/helpers'; -const execFile = promisify(execFileCallback); - suite('Rstest dependency polling recovery', function () { this.timeout(180_000); @@ -30,16 +27,7 @@ suite('Rstest dependency polling recovery', function () { const lockfile = path.join(root, 'pnpm-lock.yaml'); const contents = fs.readFileSync(lockfile); const mtime = fs.statSync(lockfile).mtimeMs; - await execFile( - 'pnpm', - ['install', '--frozen-lockfile', '--ignore-scripts'], - { - cwd: root, - timeout: 90_000, - // Windows needs a shell for pnpm.cmd; arguments are fixed safe tokens. - shell: process.platform === 'win32', - }, - ); + await pnpmInstallFrozen(root); assert.deepEqual( fs.readFileSync(lockfile), contents, diff --git a/packages/vscode/e2e/setupFixtures.mjs b/packages/vscode/e2e/setupFixtures.mjs index fb4cd6d..5bca7dd 100644 --- a/packages/vscode/e2e/setupFixtures.mjs +++ b/packages/vscode/e2e/setupFixtures.mjs @@ -5,9 +5,20 @@ // from the project, so a fixture that linked this repo's own node_modules would // test nothing. `rstack@0.7.2` itself pins `@rslint/core@0.9.0` exactly, so the // Rstack fixture pins its lint core transitively. Each fixture is its own -// independent install; `--ignore-workspace` keeps test setup out of the parent -// workspace. Exact toolchain pins make installs reproducible without committed -// lockfiles, and Renovate bumps those pins. +// independent install. Exact toolchain pins make installs reproducible +// without committed lockfiles, and Renovate bumps those pins. +// +// Each fixture carries a committed, settings-only `pnpm-workspace.yaml`. It +// stops pnpm from walking up into this repo's workspace, and it exempts the +// Rstack family from pnpm's `minimumReleaseAge` gate (fixtures pin releases +// that are often hours old; third-party packages stay gated). The exemption +// has to live in a file rather than `--config.minimumReleaseAge=0` because the +// dependency-recovery tests copy a fixture to the OS tmpdir and re-install +// with `--frozen-lockfile`, and pnpm 11 re-checks release age even when +// frozen, so the policy must travel with the copy. The same file rules out +// `--ignore-workspace`, under which pnpm ignores its settings. Setup-only +// options stay flags below. Keep the list in step with the root +// `pnpm-workspace.yaml`. // // Idempotent: pnpm is a no-op when the fixture is already up to date, so // `test:e2e` can always run it. @@ -65,9 +76,6 @@ const install = (name) => { pnpmCommand, [ 'install', - // A fixture is a standalone project, never a workspace member of this - // repo: the whole point is a plain, published-versions install. - '--ignore-workspace', // Fixtures pin exact toolchain versions rather than committing lockfiles; // Renovate updates the pins. '--no-frozen-lockfile', @@ -76,10 +84,6 @@ const install = (name) => { // `node_modules`, which it refuses to do without a TTY. The directory is // disposable. '--config.confirmModulesPurge=false', - // Fixtures deliberately install pinned published versions of the Rstack - // toolchain, which are often hours old — disable pnpm's - // minimum-release-age supply-chain gate for these sandboxes. - '--config.minimumReleaseAge=0', // pnpm's build-script gate exits non-zero on unapproved postinstalls // (e.g. core-js in the rstest fixture). These sandboxes install real // published packages exactly like a user project would, so run their diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 34cde38..085be5a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -11,6 +11,7 @@ allowBuilds: # The whole point of this repo is tracking the freshest Rstack toolchain # releases, so exempt them from the minimum-release-age supply-chain policy. +# Each E2E fixture's pnpm-workspace.yaml repeats this list; keep them in step. minimumReleaseAgeExclude: - rstack - '@rslint/core' @@ -19,3 +20,4 @@ minimumReleaseAgeExclude: - '@rslib/core' - '@rstackjs/*' - rsbuild-plugin-dts + - '@rspack/*'