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
5 changes: 5 additions & 0 deletions .changeset/pnpm-11-allow-builds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/sv-utils': patch
---

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`
11 changes: 8 additions & 3 deletions documentation/docs/50-api/20-sv-utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,15 +232,20 @@ Namespaced helpers for AST manipulation:
## Package manager helpers
### `pnpm.onlyBuiltDependencies`
### `pnpm.allowBuilds`
Returns a transform for `pnpm-workspace.yaml` that adds packages to the `onlyBuiltDependencies` list. Use with `sv.file` when the project uses pnpm.
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.
The helper detects the installed pnpm version via `pnpm --version`:
- pnpm `>= 11`: writes to the unified `allowBuilds` map (`{ pkg: true }`), migrating any legacy `onlyBuiltDependencies` list into the map.
- pnpm `< 11`: writes to the legacy `onlyBuiltDependencies` list.
```js
// @noErrors
import { pnpm } from '@sveltejs/sv-utils';

if (packageManager === 'pnpm') {
sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.onlyBuiltDependencies('my-native-dep'));
sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.allowBuilds('my-native-dep'));
}
```
6 changes: 5 additions & 1 deletion packages/sv-utils/api-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -717,9 +717,13 @@ declare const transforms: {
text(cb: (file: { content: string; text: typeof text_d_exports }) => string | false): TransformFn;
};
declare namespace pnpm_d_exports {
export { onlyBuiltDependencies };
export { allowBuilds, onlyBuiltDependencies };
}

declare function allowBuilds(...packages: string[]): TransformFn;
/**
* @deprecated Use {@link allowBuilds} instead.
*/
declare function onlyBuiltDependencies(...packages: string[]): TransformFn;
type Version = {
major?: number;
Expand Down
14 changes: 14 additions & 0 deletions packages/sv-utils/src/pnpm-internals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { execSync } from 'node:child_process';
import { coerceVersion } from './semver.ts';

export function detectPnpmMajor(): number | undefined {
try {
const out = execSync('pnpm --version', {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore']
});
return coerceVersion(out.trim()).major;
} catch {
return undefined;
}
}
73 changes: 66 additions & 7 deletions packages/sv-utils/src/pnpm.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,72 @@
import { detectPnpmMajor } from './pnpm-internals.ts';
import { transforms, type TransformFn } from './tooling/transforms.ts';

type YamlMap = {
get(key: string): unknown;
set(key: string, value: unknown): void;
has(key: string): boolean;
};

type YamlSeq = { items?: Array<{ value: string } | string> };

type YamlDoc = {
get(key: string): unknown;
set(key: string, value: unknown): void;
has(key: string): boolean;
delete(key: string): boolean;
createNode(value: unknown, options?: { flow?: boolean }): unknown;
};

/**
* Returns a TransformFn for `pnpm-workspace.yaml` that adds packages to `onlyBuiltDependencies`.
* Returns a TransformFn for `pnpm-workspace.yaml` that adds packages to the
* pnpm "allow builds" config.
*
* The helper detects the installed pnpm version (via `pnpm --version`) and:
* - on pnpm `>= 11` writes to the unified `allowBuilds` map (`{ pkg: true }`),
* migrating any legacy `onlyBuiltDependencies` list into the map;
* - on pnpm `< 11` writes to the legacy `onlyBuiltDependencies` list.
*
* Use with `sv.file`:
* ```ts
* if (packageManager === 'pnpm') {
* sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.onlyBuiltDependencies('my-native-dep'));
* sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.allowBuilds('my-native-dep'));
* }
* ```
*/
export function onlyBuiltDependencies(...packages: string[]): TransformFn {
export function allowBuilds(...packages: string[]): TransformFn {
const major = detectPnpmMajor();
if (major !== undefined && major < 11) return writeLegacy(packages);
return writeAllowBuilds(packages);
}

function writeAllowBuilds(packages: string[]): TransformFn {
return transforms.yaml(({ data }) => {
const existing = data.get('onlyBuiltDependencies') as
| { items?: Array<{ value: string } | string> }
| undefined;
const doc = data as unknown as YamlDoc;

const toMigrate: string[] = [];
const legacy = doc.get('onlyBuiltDependencies') as YamlSeq | undefined;
if (legacy?.items) {
for (const item of legacy.items) {
toMigrate.push(typeof item === 'object' ? item.value : item);
}
}

let map = doc.get('allowBuilds') as YamlMap | undefined;
if (!map || typeof map.set !== 'function') {
map = doc.createNode({}, { flow: false }) as YamlMap;
doc.set('allowBuilds', map);
}

for (const pkg of [...toMigrate, ...packages]) {
if (!map.has(pkg)) map.set(pkg, true);
}

if (legacy) doc.delete('onlyBuiltDependencies');
});
}

function writeLegacy(packages: string[]): TransformFn {
return transforms.yaml(({ data }) => {
const existing = data.get('onlyBuiltDependencies') as YamlSeq | undefined;
const items: Array<{ value: string } | string> = existing?.items ?? [];
for (const pkg of packages) {
if (items.includes(pkg)) continue;
Expand All @@ -24,3 +76,10 @@ export function onlyBuiltDependencies(...packages: string[]): TransformFn {
data.set('onlyBuiltDependencies', items);
});
}

/**
* @deprecated Use {@link allowBuilds} instead.
*/
export function onlyBuiltDependencies(...packages: string[]): TransformFn {
return allowBuilds(...packages);
}
102 changes: 102 additions & 0 deletions packages/sv-utils/src/tests/pnpm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { describe, expect, it } from 'vitest';
import { detectPnpmMajor } from '../pnpm-internals.ts';
import { allowBuilds, onlyBuiltDependencies } from '../pnpm.ts';

const major = detectPnpmMajor();
const isPnpm11 = major === undefined || major >= 11;

describe.runIf(isPnpm11)('allowBuilds (pnpm >= 11: writes allowBuilds map)', () => {
it('creates allowBuilds map in empty file', () => {
expect(allowBuilds('esbuild')('')).toBe('allowBuilds:\n esbuild: true\n');
});

it('appends to existing allowBuilds map', () => {
const input = `packages:
- 'packages/*'
allowBuilds:
bar: true
`;
expect(allowBuilds('esbuild')(input)).toBe(`packages:
- 'packages/*'
allowBuilds:
bar: true
esbuild: true
`);
});

it('preserves false entries when adding new packages', () => {
const input = `allowBuilds:
core-js: false
`;
expect(allowBuilds('esbuild')(input)).toBe(`allowBuilds:
core-js: false
esbuild: true
`);
});

it('migrates legacy onlyBuiltDependencies to allowBuilds', () => {
const input = `packages:
- 'packages/*'
onlyBuiltDependencies:
- foo
- bar
`;
expect(allowBuilds('esbuild')(input)).toBe(`packages:
- 'packages/*'
allowBuilds:
foo: true
bar: true
esbuild: true
`);
});

it('merges legacy and existing allowBuilds without duplicating', () => {
const input = `onlyBuiltDependencies:
- shared
allowBuilds:
shared: false
`;
expect(allowBuilds('newone')(input)).toBe(`allowBuilds:
shared: false
newone: true
`);
});

it('is idempotent when package already present', () => {
const input = `allowBuilds:
esbuild: true
`;
expect(allowBuilds('esbuild')(input)).toBe(input);
});

it('deprecated onlyBuiltDependencies delegates to allowBuilds', () => {
expect(onlyBuiltDependencies('esbuild')('')).toBe('allowBuilds:\n esbuild: true\n');
});
});

describe.runIf(!isPnpm11)('allowBuilds (pnpm < 11: writes onlyBuiltDependencies list)', () => {
it('creates onlyBuiltDependencies list in empty file', () => {
expect(allowBuilds('esbuild')('')).toBe('onlyBuiltDependencies:\n - esbuild\n');
});

it('appends to existing onlyBuiltDependencies list', () => {
const input = `onlyBuiltDependencies:
- foo
`;
expect(allowBuilds('esbuild')(input)).toBe(`onlyBuiltDependencies:
- foo
- esbuild
`);
});

it('is idempotent on legacy list', () => {
const input = `onlyBuiltDependencies:
- esbuild
`;
expect(allowBuilds('esbuild')(input)).toBe(input);
});

it('deprecated onlyBuiltDependencies delegates to allowBuilds', () => {
expect(onlyBuiltDependencies('esbuild')('')).toBe('onlyBuiltDependencies:\n - esbuild\n');
});
});
2 changes: 1 addition & 1 deletion packages/sv/src/addons/drizzle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export default defineAddon({
sv.dependency('better-sqlite3', '^12.8.0');
sv.devDependency('@types/better-sqlite3', '^7.6.13');
if (packageManager === 'pnpm') {
sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.onlyBuiltDependencies('better-sqlite3'));
sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.allowBuilds('better-sqlite3'));
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/sv/src/addons/tailwindcss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default defineAddon({
sv.devDependency('tailwindcss', '^4.2.2');
sv.devDependency('@tailwindcss/vite', '^4.2.2');
if (packageManager === 'pnpm') {
sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.onlyBuiltDependencies('@tailwindcss/oxide'));
sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.allowBuilds('@tailwindcss/oxide'));
}

if (prettierInstalled) sv.devDependency('prettier-plugin-tailwindcss', '^0.7.2');
Expand Down
4 changes: 2 additions & 2 deletions packages/sv/src/cli/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { downloadPackage, getPackageJSON } from '../core/fetch-packages.ts';
import { formatFiles } from '../core/formatFiles.ts';
import {
AGENT_NAMES,
addPnpmOnlyBuiltDependencies,
addPnpmAllowBuilds,
installDependencies,
installOption,
packageManagerPrompt
Expand Down Expand Up @@ -712,7 +712,7 @@ export async function runAddonsApply({
? await packageManagerPrompt(options.cwd)
: options.install;

addPnpmOnlyBuiltDependencies(workspace.cwd, packageManager, 'esbuild');
addPnpmAllowBuilds(workspace.cwd, packageManager, 'esbuild');

const argsFormattedAddons: string[] = [];
for (const loaded of successfulAddons) {
Expand Down
4 changes: 2 additions & 2 deletions packages/sv/src/cli/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { LoadedAddon, OptionValues, SetupResult } from '../core/config.ts';
import { formatFiles } from '../core/formatFiles.ts';
import {
AGENT_NAMES,
addPnpmOnlyBuiltDependencies,
addPnpmAllowBuilds,
detectPackageManager,
installDependencies,
installOption,
Expand Down Expand Up @@ -396,7 +396,7 @@ async function createProject(cwd: ProjectPath, options: Options) {
}
const addOnNextSteps = getNextSteps(addOnSuccessfulAddons, workspace, answers, addonSetupResults);

addPnpmOnlyBuiltDependencies(projectPath, packageManager, 'esbuild');
addPnpmAllowBuilds(projectPath, packageManager, 'esbuild');
if (packageManager) {
await installDependencies(packageManager, projectPath);
await formatFiles({ packageManager, cwd: projectPath, filesToFormat: addOnFilesToFormat });
Expand Down
16 changes: 13 additions & 3 deletions packages/sv/src/cli/tests/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ describe('cli', () => {
const { data: packageJson } = parse.json(fs.readFileSync(packageJsonPath, 'utf-8'));
packageJson.peerDependencies['sv'] = 'file:../../../..';
packageJson.devDependencies['sv'] = 'file:../../../..';
packageJson.devDependencies['@sveltejs/sv-utils'] = 'file:../../../../sv-utils';
packageJson.devDependencies['@sveltejs/sv-utils'] = 'file:../../../../../sv-utils';
fs.writeFileSync(
packageJsonPath,
JSON.stringify(packageJson, null, 3).replaceAll(' ', '\t')
Expand All @@ -158,8 +158,18 @@ describe('cli', () => {
['run', 'test']
];
for (const cmd of cmds) {
const res = await exec('pnpm', cmd, {
nodeOptions: { stdio: 'pipe', cwd: testOutputPath }
// use npm here so the install doesn't walk up into the monorepo's
// pnpm workspace and try to resolve packages from there
const res = await exec('npm', cmd, {
nodeOptions: {
stdio: 'pipe',
cwd: testOutputPath,
env: {
...process.env,
// allow npm under a repo whose packageManager is pnpm
COREPACK_ENABLE_STRICT: '0'
}
}
});
expect(
res.exitCode,
Expand Down
2 changes: 1 addition & 1 deletion packages/sv/src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type { OptionValues } from './options.ts';
export type ConditionDefinition = (Workspace: Workspace) => boolean;

export type SvApi = {
/** @deprecated use `pnpm.onlyBuiltDependencies` from `@sveltejs/sv-utils` instead */
/** @deprecated use `pnpm.allowBuilds` from `@sveltejs/sv-utils` instead */
pnpmBuildDependency: (pkg: string) => void;
/** Add a package to the dependencies. */
dependency: (pkg: string, version: string) => void;
Expand Down
8 changes: 4 additions & 4 deletions packages/sv/src/core/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
} from './config.ts';
import { svDeprecated } from './deprecated.ts';
import { TESTING } from './env.ts';
import { addPnpmOnlyBuiltDependencies } from './package-manager.ts';
import { addPnpmAllowBuilds } from './package-manager.ts';
import { createWorkspace, type Workspace } from './workspace.ts';

function alphabetizeRecord(obj: Record<string, string>) {
Expand Down Expand Up @@ -257,12 +257,12 @@ async function runAddon({ addon, loaded, multiple, workspace, workspaceOptions }
devDependency: (pkg, version) => {
dependencies.push({ pkg, version, dev: true });
},
/** @deprecated use `pnpm.onlyBuiltDependencies` from `@sveltejs/sv-utils` instead */
/** @deprecated use `pnpm.allowBuilds` from `@sveltejs/sv-utils` instead */
pnpmBuildDependency: (pkg) => {
svDeprecated(
'use `pnpm.onlyBuiltDependencies` from `@sveltejs/sv-utils` instead of `sv.pnpmBuildDependency`'
'use `pnpm.allowBuilds` from `@sveltejs/sv-utils` instead of `sv.pnpmBuildDependency`'
);
addPnpmOnlyBuiltDependencies(workspace.cwd, workspace.packageManager, pkg);
addPnpmAllowBuilds(workspace.cwd, workspace.packageManager, pkg);
}
};

Expand Down
4 changes: 2 additions & 2 deletions packages/sv/src/core/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export function getUserAgent(): AgentName | undefined {
return AGENTS.includes(name) ? name : undefined;
}

export function addPnpmOnlyBuiltDependencies(
export function addPnpmAllowBuilds(
cwd: string,
packageManager: AgentName | null | undefined,
...packages: string[]
Expand All @@ -102,6 +102,6 @@ export function addPnpmOnlyBuiltDependencies(
const found = find.up('pnpm-workspace.yaml', { cwd });
const filePath = found ?? path.join(cwd, 'pnpm-workspace.yaml');
const content = found ? fs.readFileSync(found, 'utf-8') : '';
const newContent = pnpm.onlyBuiltDependencies(...packages)(content);
const newContent = pnpm.allowBuilds(...packages)(content);
if (newContent !== content) fs.writeFileSync(filePath, newContent, 'utf-8');
}
Loading
Loading