Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/plain-donuts-smile.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 7 additions & 6 deletions packages/create-reactive/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
82 changes: 82 additions & 0 deletions packages/create-reactive/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>
}

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()
Expand Down
6 changes: 5 additions & 1 deletion packages/create-reactive/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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<string, string> | undefined
const requiredNpmRange = engines?.npm
Expand Down
27 changes: 27 additions & 0 deletions packages/create-reactive/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,33 @@ export function emptyDir(dir: string) {
}
}

export type PkgManager = 'bun' | 'npm' | 'pnpm' | 'yarn'

// 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'
const YARN_UUID_RESOLUTION_KEY = '**/@metamask/utils/uuid'

// 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<string, unknown>,
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]!
Expand Down
8 changes: 0 additions & 8 deletions packages/create-reactive/templates/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
8 changes: 0 additions & 8 deletions packages/create-reactive/templates/nuxt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,5 @@
},
"engines": {
"npm": ">=11"
},
"overrides": {
"uuid@<11.1.1": "^11.1.1"
},
"pnpm": {
"overrides": {
"uuid@<11.1.1": "^11.1.1"
}
}
}
8 changes: 0 additions & 8 deletions packages/create-reactive/templates/vite-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
8 changes: 0 additions & 8 deletions packages/create-reactive/templates/vite-solid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
8 changes: 0 additions & 8 deletions packages/create-reactive/templates/vite-vanilla/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
8 changes: 0 additions & 8 deletions packages/create-reactive/templates/vite-vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}