Skip to content

Commit e94734e

Browse files
authored
handle pnpm@11 (#1074)
1 parent 65d8f01 commit e94734e

15 files changed

Lines changed: 228 additions & 29 deletions

File tree

.changeset/pnpm-11-allow-builds.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/sv-utils': patch
3+
---
4+
5+
handle `pnpm@11`: add `pnpm.allowBuilds` helper that auto-detects the installed pnpm version and writes to `allowBuilds` (pnpm 11+) or the legacy `onlyBuiltDependencies` list (pnpm 10). Deprecate `pnpm.onlyBuiltDependencies`

documentation/docs/50-api/20-sv-utils.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,15 +232,20 @@ Namespaced helpers for AST manipulation:
232232
233233
## Package manager helpers
234234
235-
### `pnpm.onlyBuiltDependencies`
235+
### `pnpm.allowBuilds`
236236
237-
Returns a transform for `pnpm-workspace.yaml` that adds packages to the `onlyBuiltDependencies` list. Use with `sv.file` when the project uses pnpm.
237+
Returns a transform for `pnpm-workspace.yaml` that adds packages to the pnpm "allow builds" config. Use with `sv.file` when the project uses pnpm.
238+
239+
The helper detects the installed pnpm version via `pnpm --version`:
240+
241+
- pnpm `>= 11`: writes to the unified `allowBuilds` map (`{ pkg: true }`), migrating any legacy `onlyBuiltDependencies` list into the map.
242+
- pnpm `< 11`: writes to the legacy `onlyBuiltDependencies` list.
238243
239244
```js
240245
// @noErrors
241246
import { pnpm } from '@sveltejs/sv-utils';
242247

243248
if (packageManager === 'pnpm') {
244-
sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.onlyBuiltDependencies('my-native-dep'));
249+
sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.allowBuilds('my-native-dep'));
245250
}
246251
```

packages/sv-utils/api-surface.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,9 +717,13 @@ declare const transforms: {
717717
text(cb: (file: { content: string; text: typeof text_d_exports }) => string | false): TransformFn;
718718
};
719719
declare namespace pnpm_d_exports {
720-
export { onlyBuiltDependencies };
720+
export { allowBuilds, onlyBuiltDependencies };
721721
}
722722

723+
declare function allowBuilds(...packages: string[]): TransformFn;
724+
/**
725+
* @deprecated Use {@link allowBuilds} instead.
726+
*/
723727
declare function onlyBuiltDependencies(...packages: string[]): TransformFn;
724728
type Version = {
725729
major?: number;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { execSync } from 'node:child_process';
2+
import { coerceVersion } from './semver.ts';
3+
4+
export function detectPnpmMajor(): number | undefined {
5+
try {
6+
const out = execSync('pnpm --version', {
7+
encoding: 'utf-8',
8+
stdio: ['ignore', 'pipe', 'ignore']
9+
});
10+
return coerceVersion(out.trim()).major;
11+
} catch {
12+
return undefined;
13+
}
14+
}

packages/sv-utils/src/pnpm.ts

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,72 @@
1+
import { detectPnpmMajor } from './pnpm-internals.ts';
12
import { transforms, type TransformFn } from './tooling/transforms.ts';
23

4+
type YamlMap = {
5+
get(key: string): unknown;
6+
set(key: string, value: unknown): void;
7+
has(key: string): boolean;
8+
};
9+
10+
type YamlSeq = { items?: Array<{ value: string } | string> };
11+
12+
type YamlDoc = {
13+
get(key: string): unknown;
14+
set(key: string, value: unknown): void;
15+
has(key: string): boolean;
16+
delete(key: string): boolean;
17+
createNode(value: unknown, options?: { flow?: boolean }): unknown;
18+
};
19+
320
/**
4-
* Returns a TransformFn for `pnpm-workspace.yaml` that adds packages to `onlyBuiltDependencies`.
21+
* Returns a TransformFn for `pnpm-workspace.yaml` that adds packages to the
22+
* pnpm "allow builds" config.
23+
*
24+
* The helper detects the installed pnpm version (via `pnpm --version`) and:
25+
* - on pnpm `>= 11` writes to the unified `allowBuilds` map (`{ pkg: true }`),
26+
* migrating any legacy `onlyBuiltDependencies` list into the map;
27+
* - on pnpm `< 11` writes to the legacy `onlyBuiltDependencies` list.
528
*
6-
* Use with `sv.file`:
729
* ```ts
830
* if (packageManager === 'pnpm') {
9-
* sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.onlyBuiltDependencies('my-native-dep'));
31+
* sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.allowBuilds('my-native-dep'));
1032
* }
1133
* ```
1234
*/
13-
export function onlyBuiltDependencies(...packages: string[]): TransformFn {
35+
export function allowBuilds(...packages: string[]): TransformFn {
36+
const major = detectPnpmMajor();
37+
if (major !== undefined && major < 11) return writeLegacy(packages);
38+
return writeAllowBuilds(packages);
39+
}
40+
41+
function writeAllowBuilds(packages: string[]): TransformFn {
1442
return transforms.yaml(({ data }) => {
15-
const existing = data.get('onlyBuiltDependencies') as
16-
| { items?: Array<{ value: string } | string> }
17-
| undefined;
43+
const doc = data as unknown as YamlDoc;
44+
45+
const toMigrate: string[] = [];
46+
const legacy = doc.get('onlyBuiltDependencies') as YamlSeq | undefined;
47+
if (legacy?.items) {
48+
for (const item of legacy.items) {
49+
toMigrate.push(typeof item === 'object' ? item.value : item);
50+
}
51+
}
52+
53+
let map = doc.get('allowBuilds') as YamlMap | undefined;
54+
if (!map || typeof map.set !== 'function') {
55+
map = doc.createNode({}, { flow: false }) as YamlMap;
56+
doc.set('allowBuilds', map);
57+
}
58+
59+
for (const pkg of [...toMigrate, ...packages]) {
60+
if (!map.has(pkg)) map.set(pkg, true);
61+
}
62+
63+
if (legacy) doc.delete('onlyBuiltDependencies');
64+
});
65+
}
66+
67+
function writeLegacy(packages: string[]): TransformFn {
68+
return transforms.yaml(({ data }) => {
69+
const existing = data.get('onlyBuiltDependencies') as YamlSeq | undefined;
1870
const items: Array<{ value: string } | string> = existing?.items ?? [];
1971
for (const pkg of packages) {
2072
if (items.includes(pkg)) continue;
@@ -24,3 +76,10 @@ export function onlyBuiltDependencies(...packages: string[]): TransformFn {
2476
data.set('onlyBuiltDependencies', items);
2577
});
2678
}
79+
80+
/**
81+
* @deprecated Use {@link allowBuilds} instead.
82+
*/
83+
export function onlyBuiltDependencies(...packages: string[]): TransformFn {
84+
return allowBuilds(...packages);
85+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { detectPnpmMajor } from '../pnpm-internals.ts';
3+
import { allowBuilds, onlyBuiltDependencies } from '../pnpm.ts';
4+
5+
const major = detectPnpmMajor();
6+
const isPnpm11 = major === undefined || major >= 11;
7+
8+
describe.runIf(isPnpm11)('allowBuilds (pnpm >= 11: writes allowBuilds map)', () => {
9+
it('creates allowBuilds map in empty file', () => {
10+
expect(allowBuilds('esbuild')('')).toBe('allowBuilds:\n esbuild: true\n');
11+
});
12+
13+
it('appends to existing allowBuilds map', () => {
14+
const input = `packages:
15+
- 'packages/*'
16+
allowBuilds:
17+
bar: true
18+
`;
19+
expect(allowBuilds('esbuild')(input)).toBe(`packages:
20+
- 'packages/*'
21+
allowBuilds:
22+
bar: true
23+
esbuild: true
24+
`);
25+
});
26+
27+
it('preserves false entries when adding new packages', () => {
28+
const input = `allowBuilds:
29+
core-js: false
30+
`;
31+
expect(allowBuilds('esbuild')(input)).toBe(`allowBuilds:
32+
core-js: false
33+
esbuild: true
34+
`);
35+
});
36+
37+
it('migrates legacy onlyBuiltDependencies to allowBuilds', () => {
38+
const input = `packages:
39+
- 'packages/*'
40+
onlyBuiltDependencies:
41+
- foo
42+
- bar
43+
`;
44+
expect(allowBuilds('esbuild')(input)).toBe(`packages:
45+
- 'packages/*'
46+
allowBuilds:
47+
foo: true
48+
bar: true
49+
esbuild: true
50+
`);
51+
});
52+
53+
it('merges legacy and existing allowBuilds without duplicating', () => {
54+
const input = `onlyBuiltDependencies:
55+
- shared
56+
allowBuilds:
57+
shared: false
58+
`;
59+
expect(allowBuilds('newone')(input)).toBe(`allowBuilds:
60+
shared: false
61+
newone: true
62+
`);
63+
});
64+
65+
it('is idempotent when package already present', () => {
66+
const input = `allowBuilds:
67+
esbuild: true
68+
`;
69+
expect(allowBuilds('esbuild')(input)).toBe(input);
70+
});
71+
72+
it('deprecated onlyBuiltDependencies delegates to allowBuilds', () => {
73+
expect(onlyBuiltDependencies('esbuild')('')).toBe('allowBuilds:\n esbuild: true\n');
74+
});
75+
});
76+
77+
describe.runIf(!isPnpm11)('allowBuilds (pnpm < 11: writes onlyBuiltDependencies list)', () => {
78+
it('creates onlyBuiltDependencies list in empty file', () => {
79+
expect(allowBuilds('esbuild')('')).toBe('onlyBuiltDependencies:\n - esbuild\n');
80+
});
81+
82+
it('appends to existing onlyBuiltDependencies list', () => {
83+
const input = `onlyBuiltDependencies:
84+
- foo
85+
`;
86+
expect(allowBuilds('esbuild')(input)).toBe(`onlyBuiltDependencies:
87+
- foo
88+
- esbuild
89+
`);
90+
});
91+
92+
it('is idempotent on legacy list', () => {
93+
const input = `onlyBuiltDependencies:
94+
- esbuild
95+
`;
96+
expect(allowBuilds('esbuild')(input)).toBe(input);
97+
});
98+
99+
it('deprecated onlyBuiltDependencies delegates to allowBuilds', () => {
100+
expect(onlyBuiltDependencies('esbuild')('')).toBe('onlyBuiltDependencies:\n - esbuild\n');
101+
});
102+
});

packages/sv/src/addons/drizzle.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export default defineAddon({
136136
sv.dependency('better-sqlite3', '^12.8.0');
137137
sv.devDependency('@types/better-sqlite3', '^7.6.13');
138138
if (packageManager === 'pnpm') {
139-
sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.onlyBuiltDependencies('better-sqlite3'));
139+
sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.allowBuilds('better-sqlite3'));
140140
}
141141
}
142142

packages/sv/src/addons/tailwindcss.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export default defineAddon({
3636
sv.devDependency('tailwindcss', '^4.2.2');
3737
sv.devDependency('@tailwindcss/vite', '^4.2.2');
3838
if (packageManager === 'pnpm') {
39-
sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.onlyBuiltDependencies('@tailwindcss/oxide'));
39+
sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.allowBuilds('@tailwindcss/oxide'));
4040
}
4141

4242
if (prettierInstalled) sv.devDependency('prettier-plugin-tailwindcss', '^0.7.2');

packages/sv/src/cli/add.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { downloadPackage, getPackageJSON } from '../core/fetch-packages.ts';
2323
import { formatFiles } from '../core/formatFiles.ts';
2424
import {
2525
AGENT_NAMES,
26-
addPnpmOnlyBuiltDependencies,
26+
addPnpmAllowBuilds,
2727
installDependencies,
2828
installOption,
2929
packageManagerPrompt
@@ -712,7 +712,7 @@ export async function runAddonsApply({
712712
? await packageManagerPrompt(options.cwd)
713713
: options.install;
714714

715-
addPnpmOnlyBuiltDependencies(workspace.cwd, packageManager, 'esbuild');
715+
addPnpmAllowBuilds(workspace.cwd, packageManager, 'esbuild');
716716

717717
const argsFormattedAddons: string[] = [];
718718
for (const loaded of successfulAddons) {

packages/sv/src/cli/create.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { LoadedAddon, OptionValues, SetupResult } from '../core/config.ts';
1010
import { formatFiles } from '../core/formatFiles.ts';
1111
import {
1212
AGENT_NAMES,
13-
addPnpmOnlyBuiltDependencies,
13+
addPnpmAllowBuilds,
1414
detectPackageManager,
1515
installDependencies,
1616
installOption,
@@ -396,7 +396,7 @@ async function createProject(cwd: ProjectPath, options: Options) {
396396
}
397397
const addOnNextSteps = getNextSteps(addOnSuccessfulAddons, workspace, answers, addonSetupResults);
398398

399-
addPnpmOnlyBuiltDependencies(projectPath, packageManager, 'esbuild');
399+
addPnpmAllowBuilds(projectPath, packageManager, 'esbuild');
400400
if (packageManager) {
401401
await installDependencies(packageManager, projectPath);
402402
await formatFiles({ packageManager, cwd: projectPath, filesToFormat: addOnFilesToFormat });

0 commit comments

Comments
 (0)