Skip to content

Commit aba44a0

Browse files
authored
chore: Upgrade to pnpm 11. (#196)
1 parent eb38a7b commit aba44a0

7 files changed

Lines changed: 136 additions & 65 deletions

File tree

‎CLAUDE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ See [`packages/solid/README.md`](./packages/solid/README.md) for details.
133133
| -------- | --------- | ---------------------------------------------------------------------------------------------- |
134134
| Node.js | 24.13.1 | Runtime (TS type stripping enabled natively — no `tsx` or `--experimental-strip-types` needed) |
135135
| Vite+ | 0.1.x | Unified toolchain (`vp`, Vite, Vitest, Oxlint, Oxfmt, tsdown) |
136-
| pnpm | 10.x | Package manager used by Vite+ for this workspace |
136+
| pnpm | 11.x | Package manager used by Vite+ for this workspace |
137137
| tsdown | via Vite+ | Library packaging through `vp pack` |
138138
| wrangler | 4.x | CF Worker dev/deploy |
139139
| vitest | via Vite+ | Unit + integration tests via `vp test` |

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,5 @@
2828
"vite-plus": "catalog:",
2929
"vitest": "catalog:"
3030
},
31-
"packageManager": "pnpm@10.33.2"
31+
"packageManager": "pnpm@11.0.8"
3232
}

‎packages/void/src/cli/init.ts‎

Lines changed: 98 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ const EMPTY_PROJECT_ALLOWED_ENTRIES = new Set([
115115
'.zencoder',
116116
]);
117117

118-
const PNPM_ONLY_BUILT_DEPENDENCIES = ['better-sqlite3', 'esbuild', 'workerd'] as const;
118+
const PNPM_ALLOWED_BUILDS = ['better-sqlite3', 'esbuild', 'workerd'] as const;
119119
const drizzleKitBin = join(fileURLToPath(import.meta.resolve('drizzle-kit')), '..', 'bin.cjs');
120120

121121
const EMPTY_PROJECT_ALLOWED_PACKAGES = new Set([
@@ -1801,23 +1801,97 @@ function ensureGitignore(root: string): void {
18011801
log.success('Updated .gitignore with Void defaults');
18021802
}
18031803

1804-
function ensurePnpmBuiltDependencies(pkg: JsonObject): void {
1805-
const pnpm = asRecord(pkg.pnpm) ?? {};
1806-
pkg.pnpm = pnpm;
1807-
1808-
const onlyBuiltDependencies = Array.isArray(pnpm.onlyBuiltDependencies)
1809-
? pnpm.onlyBuiltDependencies.filter(
1810-
(value: unknown): value is string => typeof value === 'string',
1811-
)
1812-
: [];
1804+
function removeTopLevelYamlBlock(yaml: string, key: string): string {
1805+
const lines = yaml.split(/\r?\n/);
1806+
const next: Array<string> = [];
1807+
for (let i = 0; i < lines.length; i += 1) {
1808+
const line = lines[i];
1809+
if (!new RegExp(`^${key}:\\s*(?:#.*)?$`).test(line)) {
1810+
next.push(line);
1811+
continue;
1812+
}
18131813

1814-
for (const dependency of PNPM_ONLY_BUILT_DEPENDENCIES) {
1815-
if (!onlyBuiltDependencies.includes(dependency)) {
1816-
onlyBuiltDependencies.push(dependency);
1814+
i += 1;
1815+
while (i < lines.length && (lines[i].trim() === '' || /^\s/.test(lines[i]))) {
1816+
i += 1;
18171817
}
1818+
i -= 1;
1819+
}
1820+
return next.join('\n');
1821+
}
1822+
1823+
function pnpmAllowBuildsYaml(): string {
1824+
const lines = ['allowBuilds:'];
1825+
for (const dependency of PNPM_ALLOWED_BUILDS) {
1826+
lines.push(` ${dependency}: true`);
1827+
}
1828+
return lines.join('\n');
1829+
}
1830+
1831+
function insertPnpmAllowBuilds(yaml: string): string {
1832+
const trimmed = yaml.trim();
1833+
const allowBuilds = pnpmAllowBuildsYaml();
1834+
if (trimmed === '') {
1835+
return `${allowBuilds}\n`;
1836+
}
1837+
1838+
const lines = trimmed.split(/\r?\n/);
1839+
const packagesIndex = lines.findIndex((line) => /^packages:\s*(?:#.*)?$/.test(line));
1840+
if (packagesIndex === -1) {
1841+
return `${trimmed}\n\n${allowBuilds}\n`;
1842+
}
1843+
1844+
let insertIndex = packagesIndex + 1;
1845+
while (
1846+
insertIndex < lines.length &&
1847+
(lines[insertIndex].trim() === '' || /^\s/.test(lines[insertIndex]))
1848+
) {
1849+
insertIndex += 1;
1850+
}
1851+
lines.splice(insertIndex, 0, '', ...allowBuilds.split('\n'), '');
1852+
return `${lines.join('\n').replace(/\n{3,}/g, '\n\n')}\n`;
1853+
}
1854+
1855+
function resolvePnpmConfigRoot(root: string, pkg: JsonObject): string {
1856+
if (
1857+
existsSync(join(root, 'pnpm-workspace.yaml')) ||
1858+
existsSync(join(root, 'pnpm-lock.yaml')) ||
1859+
parsePackageManagerField(pkg) === 'pnpm'
1860+
) {
1861+
return root;
18181862
}
18191863

1820-
pnpm.onlyBuiltDependencies = onlyBuiltDependencies;
1864+
const workspaceRoot = findWorkspaceRoot(root);
1865+
if (
1866+
workspaceRoot !== undefined &&
1867+
isWorkspaceSubpackage(workspaceRoot, root) &&
1868+
hasAncestorPnpmMarkers(root)
1869+
) {
1870+
return workspaceRoot;
1871+
}
1872+
1873+
return root;
1874+
}
1875+
1876+
function ensurePnpmAllowBuilds(root: string, pkg: JsonObject): boolean {
1877+
const configRoot = resolvePnpmConfigRoot(root, pkg);
1878+
const workspacePath = join(configRoot, 'pnpm-workspace.yaml');
1879+
const before = existsSync(workspacePath) ? readFileSync(workspacePath, 'utf-8') : '';
1880+
const withoutLegacy = removeTopLevelYamlBlock(
1881+
removeTopLevelYamlBlock(before, 'onlyBuiltDependencies'),
1882+
'allowBuilds',
1883+
);
1884+
const after = insertPnpmAllowBuilds(withoutLegacy);
1885+
if (after !== before) {
1886+
writeFileSync(workspacePath, after);
1887+
log.success(
1888+
configRoot === root
1889+
? 'Updated pnpm-workspace.yaml with approved builds'
1890+
: `Updated ${relative(root, workspacePath)} with approved builds`,
1891+
);
1892+
return true;
1893+
}
1894+
return false;
18211895
}
18221896

18231897
function installProjectDependencies(
@@ -2065,16 +2139,16 @@ function scaffoldPagesApp(
20652139
ensureDependency('devDependencies', name, version);
20662140
}
20672141

2068-
// Round 44: `ensurePnpmBuiltDependencies` must run for pnpm workspace
2142+
// Round 44: the pnpm build approval setup must run for workspace
20692143
// subpackages too. `detectProjectState` only checks LOCAL markers and
20702144
// returns `'npm'` for a subpackage without its own lockfile / pin,
20712145
// but `isPnpmProject(root, pkg)` walks ancestors and (after round 43)
20722146
// verifies membership. Gate the allowlist on the ancestor-aware check
2073-
// so the subpackage gets `pnpm.onlyBuiltDependencies` and its
2147+
// so the workspace gets `allowBuilds` and its
20742148
// better-sqlite3 / esbuild / workerd postinstalls run on the next
20752149
// workspace-root `pnpm install`.
20762150
if (isPnpmProject(root, pkg)) {
2077-
ensurePnpmBuiltDependencies(pkg);
2151+
ensurePnpmAllowBuilds(root, pkg);
20782152
}
20792153
// The Corepack pin is ONLY for the standalone pnpm case. In a
20802154
// workspace subpackage the pin lives on the workspace root's
@@ -2084,7 +2158,7 @@ function scaffoldPagesApp(
20842158
packageManager === 'pnpm' &&
20852159
(!pkg.packageManager || typeof pkg.packageManager !== 'string')
20862160
) {
2087-
pkg.packageManager = 'pnpm@10.33.0';
2161+
pkg.packageManager = 'pnpm@11.0.8';
20882162
}
20892163

20902164
writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n');
@@ -2188,14 +2262,7 @@ function ensureExistingProjectPackage(
21882262
dependencyAdded = true;
21892263
}
21902264

2191-
if (isPnpmProject(root, pkg)) {
2192-
const before = JSON.stringify(asRecord(pkg.pnpm)?.onlyBuiltDependencies ?? null);
2193-
ensurePnpmBuiltDependencies(pkg);
2194-
const after = JSON.stringify(asRecord(pkg.pnpm)?.onlyBuiltDependencies ?? null);
2195-
if (before !== after) {
2196-
changed = true;
2197-
}
2198-
}
2265+
const pnpmConfigChanged = isPnpmProject(root, pkg) ? ensurePnpmAllowBuilds(root, pkg) : false;
21992266

22002267
if (changed) {
22012268
writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n');
@@ -2210,7 +2277,7 @@ function ensureExistingProjectPackage(
22102277
);
22112278
}
22122279

2213-
return { dependencyAdded, packageJsonMutated: changed };
2280+
return { dependencyAdded, packageJsonMutated: changed || pnpmConfigChanged };
22142281
}
22152282

22162283
function insertVoidImport(code: string): string {
@@ -2855,9 +2922,8 @@ export function ensureVoidDependency(root: string): boolean {
28552922
changed = true;
28562923
}
28572924

2858-
// Mirror `scaffoldPagesApp`'s pnpm built-dep allowlist. pnpm 10 ignores
2859-
// build scripts unless the package is listed in
2860-
// `pnpm.onlyBuiltDependencies`; without this, the freshly scaffolded
2925+
// Mirror `scaffoldPagesApp`'s pnpm build approval config. pnpm requires
2926+
// build scripts to be approved through `allowBuilds`; without this, the freshly scaffolded
28612927
// demo breaks on first local DB / dev commands because
28622928
// `better-sqlite3`/`esbuild`/`workerd` never run their postinstall
28632929
// build steps. Honor both pnpm signals via `isPnpmProject` — a committed
@@ -2866,14 +2932,7 @@ export function ensureVoidDependency(root: string): boolean {
28662932
// `detectProjectState` and `runInitCommand` already treat as pnpm; the
28672933
// demo would then run `pnpm install` against the project but skip the
28682934
// allowlist, leaving the local DB / dev setup broken on first run.
2869-
if (isPnpmProject(root, pkg)) {
2870-
const before = JSON.stringify(asRecord(pkg.pnpm)?.onlyBuiltDependencies ?? null);
2871-
ensurePnpmBuiltDependencies(pkg);
2872-
const after = JSON.stringify(asRecord(pkg.pnpm)?.onlyBuiltDependencies ?? null);
2873-
if (before !== after) {
2874-
changed = true;
2875-
}
2876-
}
2935+
const pnpmConfigChanged = isPnpmProject(root, pkg) ? ensurePnpmAllowBuilds(root, pkg) : false;
28772936

28782937
if (changed) {
28792938
writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n');
@@ -2884,7 +2943,7 @@ export function ensureVoidDependency(root: string): boolean {
28842943
);
28852944
}
28862945

2887-
return changed;
2946+
return changed || pnpmConfigChanged;
28882947
}
28892948

28902949
interface AgentSetupSelection {

‎packages/void/src/migrations/runner.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function toBetterSqlite3Error(error: unknown): Error {
3434

3535
return new Error(
3636
'db: better-sqlite3 is installed but its native binding was not built. ' +
37-
"Add it to your 'onlyBuiltDependencies' and run `pnpm rebuild better-sqlite3`. See https://pnpm.io/settings#onlybuiltdependencies",
37+
'Set `allowBuilds.better-sqlite3` to `true` in pnpm-workspace.yaml and run `pnpm rebuild better-sqlite3`. See https://pnpm.io/settings#allowbuilds',
3838
);
3939
}
4040

‎packages/void/test/unit/init.test.ts‎

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ const mockIsStagingMode = vi.fn();
3535
const mockListProjects = vi.fn();
3636
const mockCreateProject = vi.fn();
3737

38+
function expectPnpmAllowBuilds(root: string): void {
39+
const yaml = readFileSync(join(root, 'pnpm-workspace.yaml'), 'utf-8');
40+
expect(yaml).toContain('allowBuilds:\n');
41+
expect(yaml).toContain(' better-sqlite3: true');
42+
expect(yaml).toContain(' esbuild: true');
43+
expect(yaml).toContain(' workerd: true');
44+
expect(yaml).not.toContain('onlyBuiltDependencies:');
45+
}
46+
3847
vi.mock('@clack/prompts', () => ({
3948
intro: vi.fn(),
4049
outro: (...args: Array<unknown>) => mockOutro(...args),
@@ -1815,9 +1824,7 @@ export default config;
18151824
expect(pkg.devDependencies['@rolldown/plugin-babel']).toBe('^0.2.3');
18161825
expect(pkg.devDependencies['@vitejs/plugin-react']).toBe('^6.0.1');
18171826
expect(pkg.devDependencies['babel-plugin-react-compiler']).toBe('^1.0.0');
1818-
expect(pkg.pnpm.onlyBuiltDependencies).toEqual(
1819-
expect.arrayContaining(['better-sqlite3', 'esbuild', 'workerd']),
1820-
);
1827+
expectPnpmAllowBuilds(root);
18211828

18221829
const reactViteConfig = readFileSync(join(root, 'vite.config.ts'), 'utf-8');
18231830
expect(reactViteConfig).toContain('from "@rolldown/plugin-babel"');
@@ -3858,14 +3865,14 @@ export default config;
38583865
});
38593866

38603867
// Round 44 regression: `scaffoldPagesApp` previously only wrote
3861-
// `pnpm.onlyBuiltDependencies` when `packageManager === 'pnpm'`. But
3868+
// pnpm build approvals when `packageManager === 'pnpm'`. But
38623869
// for a workspace subpackage with no local lockfile / Corepack pin,
38633870
// `detectProjectState` returns `'npm'` — the pnpm branch never fires
38643871
// and the subpackage's built-deps allowlist is silently dropped,
38653872
// which suppresses better-sqlite3 / esbuild / workerd postinstalls on
38663873
// the next workspace-root `pnpm install`. Gate the allowlist on the
38673874
// ancestor-aware `isPnpmProject` check instead.
3868-
it('writes pnpm.onlyBuiltDependencies for a pnpm workspace subpackage (Round 44)', async () => {
3875+
it('writes pnpm allowBuilds for a pnpm workspace subpackage (Round 44)', async () => {
38693876
const wsroot = mkdtempSync(join(tmpdir(), 'void-init-'));
38703877
try {
38713878
// Workspace root: pnpm-workspace.yaml with `packages/*`. The root
@@ -3874,7 +3881,7 @@ export default config;
38743881
writeFileSync(
38753882
join(wsroot, 'package.json'),
38763883
JSON.stringify(
3877-
{ name: 'workspace-root', private: true, packageManager: 'pnpm@10.33.0' },
3884+
{ name: 'workspace-root', private: true, packageManager: 'pnpm@11.0.8' },
38783885
null,
38793886
2,
38803887
) + '\n',
@@ -3895,11 +3902,11 @@ export default config;
38953902

38963903
const pkg = JSON.parse(readFileSync(join(subpkgRoot, 'package.json'), 'utf-8'));
38973904

3898-
// The built-deps allowlist MUST be present — this is the
3899-
// regression target. Mirrors the standalone-pnpm assertion above.
3900-
expect(pkg.pnpm?.onlyBuiltDependencies).toEqual(
3901-
expect.arrayContaining(['better-sqlite3', 'esbuild', 'workerd']),
3902-
);
3905+
// The built-deps allowlist MUST be present in the workspace
3906+
// config — this is the regression target. Mirrors the
3907+
// standalone-pnpm assertion above.
3908+
expectPnpmAllowBuilds(wsroot);
3909+
expect(pkg.pnpm).toBeUndefined();
39033910
} finally {
39043911
rmSync(wsroot, { recursive: true, force: true });
39053912
}
@@ -3916,7 +3923,7 @@ export default config;
39163923
writeFileSync(
39173924
join(wsroot, 'package.json'),
39183925
JSON.stringify(
3919-
{ name: 'workspace-root', private: true, packageManager: 'pnpm@10.33.0' },
3926+
{ name: 'workspace-root', private: true, packageManager: 'pnpm@11.0.8' },
39203927
null,
39213928
2,
39223929
) + '\n',
@@ -3940,7 +3947,7 @@ export default config;
39403947
}
39413948
});
39423949

3943-
it('writes BOTH pnpm.onlyBuiltDependencies AND packageManager pin in a standalone pnpm project (Round 44)', async () => {
3950+
it('writes BOTH pnpm allowBuilds AND packageManager pin in a standalone pnpm project (Round 44)', async () => {
39443951
// Symmetric proof that the standalone-pnpm path still writes both
39453952
// the allowlist AND the Corepack pin, after the Round 44 split.
39463953
const root = mkdtempSync(join(tmpdir(), 'void-init-'));
@@ -3957,10 +3964,8 @@ export default config;
39573964
await runInitCommand(root);
39583965

39593966
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8'));
3960-
expect(pkg.pnpm?.onlyBuiltDependencies).toEqual(
3961-
expect.arrayContaining(['better-sqlite3', 'esbuild', 'workerd']),
3962-
);
3963-
expect(pkg.packageManager).toBe('pnpm@10.33.0');
3967+
expectPnpmAllowBuilds(root);
3968+
expect(pkg.packageManager).toBe('pnpm@11.0.8');
39643969
} finally {
39653970
rmSync(root, { recursive: true, force: true });
39663971
}

‎playground/ssr/blog/getting-started.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ This guide walks you through setting up a Void project with SSR and blog support
55
## Prerequisites
66

77
- Node.js 22+
8-
- pnpm 10+
8+
- pnpm 11+
99

1010
## Steps
1111

‎pnpm-workspace.yaml‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ packages:
44
- playground/*
55
- playground-local/*
66

7+
allowBuilds:
8+
'@parcel/watcher': false
9+
'@prisma/engines': false
10+
better-sqlite3: true
11+
esbuild: true
12+
lmdb: false
13+
msgpackr-extract: false
14+
prisma: false
15+
sharp: false
16+
vue-demi: false
17+
workerd: true
18+
719
catalog:
820
'@clack/prompts': ^1.3.0
921
'@cloudflare/workers-types': ^4.20260508.1
@@ -15,11 +27,6 @@ catalog:
1527
vitest: npm:@voidzero-dev/vite-plus-test@0.1.20
1628
wrangler: ^4.90.0
1729

18-
onlyBuiltDependencies:
19-
- better-sqlite3
20-
- esbuild
21-
- workerd
22-
2330
overrides:
2431
esbuild: ^0.27.3
2532
vite: 'catalog:'

0 commit comments

Comments
 (0)