From 92d61189991e937e696f081bc45101099911b165 Mon Sep 17 00:00:00 2001 From: Badi Ifaoui Date: Wed, 2 Sep 2026 21:30:21 +0000 Subject: [PATCH 1/2] fix(create-reactive): scaffold the uuid override per chosen package manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The templates carried one static overrides/pnpm.overrides pair that only ever protected npm and pnpm 10 — pnpm 11 stopped reading the pnpm field, and yarn's resolutions key was already dropped because pnpm reads it too and the two selector grammars collide. create-reactive already knows which package manager the user picked at scaffold time, so it now generates the matching mechanism instead of shipping a static key: package.json overrides for npm/bun, a pnpm-workspace.yaml override for pnpm 10 and 11 (which both honour it outside an actual multi-package workspace), and a scoped resolutions path selector for yarn. No generated project ever carries two engines' keys at once, which removes the grammar collision entirely. --- packages/create-reactive/src/index.test.ts | 82 +++++++++++++++++++ packages/create-reactive/src/index.ts | 6 +- packages/create-reactive/src/utils.ts | 40 +++++++++ .../templates/next/package.json | 8 -- .../templates/nuxt/package.json | 8 -- .../templates/vite-react/package.json | 8 -- .../templates/vite-solid/package.json | 8 -- .../templates/vite-vanilla/package.json | 8 -- .../templates/vite-vue/package.json | 8 -- 9 files changed, 127 insertions(+), 49 deletions(-) diff --git a/packages/create-reactive/src/index.test.ts b/packages/create-reactive/src/index.test.ts index b02df71..32585bd 100644 --- a/packages/create-reactive/src/index.test.ts +++ b/packages/create-reactive/src/index.test.ts @@ -90,6 +90,88 @@ describe('template copying', () => { }) }) +describe('manager-specific uuid override generation', () => { + let tempDir: string + const originalCwd = process.cwd() + const originalUserAgent = process.env.npm_config_user_agent + + const allVariants = frameworks.flatMap((f) => f.variants.map((v) => v.name)) + + beforeEach(async () => { + tempDir = join( + tmpdir(), + `create-reactive-override-test-${Date.now()}-${Math.random().toString(36).slice(2)}`, + ) + await mkdir(tempDir, { recursive: true }) + process.chdir(tempDir) + }) + + afterEach(async () => { + process.chdir(originalCwd) + if (originalUserAgent === undefined) { + delete process.env.npm_config_user_agent + } else { + process.env.npm_config_user_agent = originalUserAgent + } + await rm(tempDir, { recursive: true, force: true }) + }) + + async function scaffoldAs(template: string, userAgent: string) { + process.env.npm_config_user_agent = userAgent + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + await createReactive({ targetDir: 'app', template }) + logSpy.mockRestore() + return JSON.parse( + readFileSync(join(tempDir, 'app', 'package.json'), 'utf-8'), + ) as Record + } + + for (const template of allVariants) { + it(`emits npm overrides only, for ${template}`, async () => { + const pkg = await scaffoldAs( + template, + 'npm/10.9.8 node/v20.11.0 linux x64', + ) + expect(pkg.overrides).toEqual({ 'uuid@<11.1.1': '^11.1.1' }) + expect(pkg.resolutions).toBeUndefined() + expect(pkg.pnpm).toBeUndefined() + expect(existsSync(join(tempDir, 'app', 'pnpm-workspace.yaml'))).toBe( + false, + ) + }) + + it(`emits a pnpm-workspace.yaml override and no package.json key, for ${template}`, async () => { + const pkg = await scaffoldAs( + template, + 'pnpm/10.33.0 node/v20.11.0 linux x64', + ) + expect(pkg.overrides).toBeUndefined() + expect(pkg.resolutions).toBeUndefined() + expect(pkg.pnpm).toBeUndefined() + const workspaceYaml = readFileSync( + join(tempDir, 'app', 'pnpm-workspace.yaml'), + 'utf-8', + ) + expect(workspaceYaml).toContain("'uuid@<11.1.1': '^11.1.1'") + }) + + it(`emits yarn resolutions with a scoped path selector, for ${template}`, async () => { + const pkg = await scaffoldAs( + template, + 'yarn/1.22.22 node/v20.11.0 linux x64', + ) + expect(pkg.resolutions).toEqual({ + '**/@metamask/utils/uuid': '^11.1.1', + }) + expect(pkg.overrides).toBeUndefined() + expect(pkg.pnpm).toBeUndefined() + expect(existsSync(join(tempDir, 'app', 'pnpm-workspace.yaml'))).toBe( + false, + ) + }) + } +}) + describe('scaffold-time npm engine warning', () => { let scaffoldTempDir: string const originalCwd = process.cwd() diff --git a/packages/create-reactive/src/index.ts b/packages/create-reactive/src/index.ts index 39a62aa..84f6866 100644 --- a/packages/create-reactive/src/index.ts +++ b/packages/create-reactive/src/index.ts @@ -6,11 +6,13 @@ import prompts from 'prompts' import { type Framework, frameworks } from './frameworks' import { + applyUuidOverride, copy, emptyDir, formatTargetDir, isEmpty, isValidPackageName, + type PkgManager, pkgFromUserAgent, satisfiesEngineRange, toValidPackageName, @@ -154,7 +156,6 @@ export async function createReactive( const template: string = variant || framework?.name || argTemplate const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent) - type PkgManager = 'bun' | 'npm' | 'pnpm' | 'yarn' let pkgManager: PkgManager if (options.bun) pkgManager = 'bun' else if (options.npm) pkgManager = 'npm' @@ -187,7 +188,10 @@ export async function createReactive( pkg.name = packageName || getProjectName() + const pnpmWorkspaceYaml = applyUuidOverride(pkg, pkgManager) + write('package.json', `${JSON.stringify(pkg, null, 2)}\n`) + if (pnpmWorkspaceYaml) write('pnpm-workspace.yaml', pnpmWorkspaceYaml) const engines = pkg.engines as Record | undefined const requiredNpmRange = engines?.npm diff --git a/packages/create-reactive/src/utils.ts b/packages/create-reactive/src/utils.ts index 110b40c..63ca069 100644 --- a/packages/create-reactive/src/utils.ts +++ b/packages/create-reactive/src/utils.ts @@ -48,6 +48,46 @@ export function emptyDir(dir: string) { } } +export type PkgManager = 'bun' | 'npm' | 'pnpm' | 'yarn' + +// uuid <11.1.1, pulled in transitively via `@metamask/utils` (a dependency +// of `@growae/reactive-connectors`), carries a known advisory. Each package +// manager reads a different override mechanism, and pnpm and yarn's +// grammars are mutually exclusive within a single field, so the generated +// project must carry only the one key its own package manager reads. +const UUID_OVERRIDE_SELECTOR = 'uuid@<11.1.1' +const UUID_OVERRIDE_RANGE = '^11.1.1' +// pnpm's own path-selector grammar (`**/@metamask/utils/uuid`) is rejected +// with ERR_PNPM_INVALID_SELECTOR under `resolutions`, and yarn's path +// selector without `**/` only partially resolves — this is the one key +// verified to fully protect yarn 1.x without depending on pnpm ever reading +// the same field. +const YARN_UUID_RESOLUTION_KEY = '**/@metamask/utils/uuid' + +/** + * Mutates `pkg` in place with the uuid override npm, bun and yarn read + * directly from `package.json`. Returns the `pnpm-workspace.yaml` contents + * to write alongside it for pnpm, since pnpm 11 dropped the `pnpm.overrides` + * package.json field pnpm 10 used to read and moved it to a workspace file + * that both majors honour outside an actual multi-package workspace. + */ +export function applyUuidOverride( + pkg: Record, + pkgManager: PkgManager, +): string | undefined { + switch (pkgManager) { + case 'npm': + case 'bun': + pkg.overrides = { [UUID_OVERRIDE_SELECTOR]: UUID_OVERRIDE_RANGE } + return undefined + case 'yarn': + pkg.resolutions = { [YARN_UUID_RESOLUTION_KEY]: UUID_OVERRIDE_RANGE } + return undefined + case 'pnpm': + return `overrides:\n '${UUID_OVERRIDE_SELECTOR}': '${UUID_OVERRIDE_RANGE}'\n` + } +} + export function pkgFromUserAgent(userAgent: string | undefined) { if (!userAgent) return undefined const pkgSpec = userAgent.split(' ')[0]! diff --git a/packages/create-reactive/templates/next/package.json b/packages/create-reactive/templates/next/package.json index a743ff6..79495ea 100644 --- a/packages/create-reactive/templates/next/package.json +++ b/packages/create-reactive/templates/next/package.json @@ -18,13 +18,5 @@ "@types/react": "^19.2.18", "@types/react-dom": "^19.2.5", "typescript": "^5.7.0" - }, - "overrides": { - "uuid@<11.1.1": "^11.1.1" - }, - "pnpm": { - "overrides": { - "uuid@<11.1.1": "^11.1.1" - } } } diff --git a/packages/create-reactive/templates/nuxt/package.json b/packages/create-reactive/templates/nuxt/package.json index fba8482..886b221 100644 --- a/packages/create-reactive/templates/nuxt/package.json +++ b/packages/create-reactive/templates/nuxt/package.json @@ -21,13 +21,5 @@ }, "engines": { "npm": ">=11" - }, - "overrides": { - "uuid@<11.1.1": "^11.1.1" - }, - "pnpm": { - "overrides": { - "uuid@<11.1.1": "^11.1.1" - } } } diff --git a/packages/create-reactive/templates/vite-react/package.json b/packages/create-reactive/templates/vite-react/package.json index 35e2373..da9e830 100644 --- a/packages/create-reactive/templates/vite-react/package.json +++ b/packages/create-reactive/templates/vite-react/package.json @@ -19,13 +19,5 @@ "@vitejs/plugin-react": "^6.1.0", "typescript": "^5.7.0", "vite": "^8.2.2" - }, - "overrides": { - "uuid@<11.1.1": "^11.1.1" - }, - "pnpm": { - "overrides": { - "uuid@<11.1.1": "^11.1.1" - } } } diff --git a/packages/create-reactive/templates/vite-solid/package.json b/packages/create-reactive/templates/vite-solid/package.json index f6ccb75..b1d3954 100644 --- a/packages/create-reactive/templates/vite-solid/package.json +++ b/packages/create-reactive/templates/vite-solid/package.json @@ -16,13 +16,5 @@ "typescript": "^7.0.2", "vite": "^8.2.2", "vite-plugin-solid": "^2.11.14" - }, - "overrides": { - "uuid@<11.1.1": "^11.1.1" - }, - "pnpm": { - "overrides": { - "uuid@<11.1.1": "^11.1.1" - } } } diff --git a/packages/create-reactive/templates/vite-vanilla/package.json b/packages/create-reactive/templates/vite-vanilla/package.json index 240f2da..45fe816 100644 --- a/packages/create-reactive/templates/vite-vanilla/package.json +++ b/packages/create-reactive/templates/vite-vanilla/package.json @@ -13,13 +13,5 @@ "devDependencies": { "typescript": "^7.0.2", "vite": "^8.2.2" - }, - "overrides": { - "uuid@<11.1.1": "^11.1.1" - }, - "pnpm": { - "overrides": { - "uuid@<11.1.1": "^11.1.1" - } } } diff --git a/packages/create-reactive/templates/vite-vue/package.json b/packages/create-reactive/templates/vite-vue/package.json index 64d09eb..6053673 100644 --- a/packages/create-reactive/templates/vite-vue/package.json +++ b/packages/create-reactive/templates/vite-vue/package.json @@ -17,13 +17,5 @@ "typescript": "^5.7.0", "vite": "^8.2.2", "vue-tsc": "^3.3.11" - }, - "overrides": { - "uuid@<11.1.1": "^11.1.1" - }, - "pnpm": { - "overrides": { - "uuid@<11.1.1": "^11.1.1" - } } } From 1b8d3b86a4be3e5831df76a7f6d48f112b5ea31d Mon Sep 17 00:00:00 2001 From: Badi Ifaoui Date: Wed, 2 Sep 2026 21:43:18 +0000 Subject: [PATCH 2/2] fix(create-reactive): add changeset, correct changelog, trim comments Three follow-ups from review on the manager-specific uuid override: add the missing patch changeset for @growae/create-reactive, correct the unreleased 0.0.6 changelog entry that still claimed yarn lost the override and that overrides/pnpm.overrides alone cover npm and pnpm (pnpm 11 never reads pnpm.overrides), and trim the rejected-alternative rationale out of utils.ts down to what the code itself cannot say. --- .changeset/plain-donuts-smile.md | 7 +++++++ packages/create-reactive/CHANGELOG.md | 13 +++++++------ packages/create-reactive/src/utils.ts | 21 ++++----------------- 3 files changed, 18 insertions(+), 23 deletions(-) create mode 100644 .changeset/plain-donuts-smile.md diff --git a/.changeset/plain-donuts-smile.md b/.changeset/plain-donuts-smile.md new file mode 100644 index 0000000..9af8eec --- /dev/null +++ b/.changeset/plain-donuts-smile.md @@ -0,0 +1,7 @@ +--- +'@growae/create-reactive': patch +--- + +Scaffold the `uuid` advisory override per chosen package manager instead of shipping one static key. + +Each generated project now gets only the mechanism its own package manager reads: `overrides` in `package.json` for npm and bun, a `pnpm-workspace.yaml` override for pnpm (both 10 and 11 — pnpm 11 dropped the `pnpm.overrides` package.json field pnpm 10 used to read), and a `resolutions` path selector for yarn. Previously only npm and pnpm 10 were protected; pnpm 11 silently stopped reading the old key, and yarn had no working key at all because pnpm and yarn's selector grammars collide within a single field. Yarn users now get the override back. diff --git a/packages/create-reactive/CHANGELOG.md b/packages/create-reactive/CHANGELOG.md index b4ca6e9..b512985 100644 --- a/packages/create-reactive/CHANGELOG.md +++ b/packages/create-reactive/CHANGELOG.md @@ -49,12 +49,13 @@ (`uuid@<11.1.1`) instead of being unbounded, so it no longer force-upgrades `uuid` for every dependency your generated app adds later. - The `resolutions` key is gone. pnpm and yarn read that field with mutually - exclusive selector grammars — a yarn-shaped key hard-fails `pnpm install` and - a pnpm-shaped key hard-fails `yarn install`, and no key satisfies both. - `overrides` and `pnpm.overrides` cover npm and pnpm. **Yarn users:** you lose - this override and `yarn audit` will surface one moderate `uuid` finding via - `@metamask/utils`; install and resolution are otherwise identical. + The mechanism is now generated per package manager instead of shipped as one + static key: `overrides` in `package.json` for npm and bun, a + `pnpm-workspace.yaml` override for pnpm (both 10 and 11 — pnpm 11 dropped the + `pnpm.overrides` package.json field pnpm 10 used to read), and a `resolutions` + path selector for yarn. Every generated project carries only its own + manager's key, so the mutually exclusive pnpm/yarn selector grammars never + collide, and yarn users keep the override. - Template tooling floors moved again before this candidate: `next` `^16.3.2`, `vite` `^8.2.2`, `@vitejs/plugin-react` `^6.1.0` and `vue-tsc` `^3.3.11`. All diff --git a/packages/create-reactive/src/utils.ts b/packages/create-reactive/src/utils.ts index 63ca069..b728b28 100644 --- a/packages/create-reactive/src/utils.ts +++ b/packages/create-reactive/src/utils.ts @@ -50,27 +50,14 @@ export function emptyDir(dir: string) { export type PkgManager = 'bun' | 'npm' | 'pnpm' | 'yarn' -// uuid <11.1.1, pulled in transitively via `@metamask/utils` (a dependency -// of `@growae/reactive-connectors`), carries a known advisory. Each package -// manager reads a different override mechanism, and pnpm and yarn's -// grammars are mutually exclusive within a single field, so the generated -// project must carry only the one key its own package manager reads. +// Each package manager reads a different override mechanism, and pnpm 11 +// dropped the package.json field pnpm 10 used to read. const UUID_OVERRIDE_SELECTOR = 'uuid@<11.1.1' const UUID_OVERRIDE_RANGE = '^11.1.1' -// pnpm's own path-selector grammar (`**/@metamask/utils/uuid`) is rejected -// with ERR_PNPM_INVALID_SELECTOR under `resolutions`, and yarn's path -// selector without `**/` only partially resolves — this is the one key -// verified to fully protect yarn 1.x without depending on pnpm ever reading -// the same field. const YARN_UUID_RESOLUTION_KEY = '**/@metamask/utils/uuid' -/** - * Mutates `pkg` in place with the uuid override npm, bun and yarn read - * directly from `package.json`. Returns the `pnpm-workspace.yaml` contents - * to write alongside it for pnpm, since pnpm 11 dropped the `pnpm.overrides` - * package.json field pnpm 10 used to read and moved it to a workspace file - * that both majors honour outside an actual multi-package workspace. - */ +// Mutates `pkg` with the override npm, bun and yarn read from package.json; +// returns pnpm-workspace.yaml contents to write alongside it for pnpm. export function applyUuidOverride( pkg: Record, pkgManager: PkgManager,