Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/sharp-pianos-detect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@sveltejs/sv-utils': patch
'sv': patch
---

fix(sv-utils): detect pnpm version from the target project, not the invoker cwd
6 changes: 4 additions & 2 deletions documentation/docs/50-api/20-sv-utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,16 +275,18 @@ Lower-level building blocks, both reading candidate files through an injected `r

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`:
The helper detects the pnpm version via `pnpm --version` in the given `cwd` (or `os.tmpdir()` when omitted, so the invoker's `packageManager` pin is not used):

- 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.

Pass `{ cwd }` so detection matches the target project. Pass `{ pnpmVersion }` to skip detection.

```js
// @noErrors
import { pnpm } from '@sveltejs/sv-utils';

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

declare function allowBuilds(...packages: string[]): TransformFn;
declare function allowBuilds(
...args: [...packages: string[], options: AllowBuildsOptions]
): TransformFn;
declare function allowBuilds(packages: string[], options?: AllowBuildsOptions): TransformFn;
type Version = {
major?: number;
minor?: number;
Expand Down
11 changes: 9 additions & 2 deletions packages/sv-utils/src/pnpm-internals.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { execSync } from 'node:child_process';
import os from 'node:os';
import { coerceVersion } from './semver.ts';

export function detectPnpmMajor(): number | undefined {
/**
* Detects the major version of pnpm that would run in `cwd`.
* Defaults to `os.tmpdir()` so detection is not pinned by the invoker's
* `packageManager` / `devEngines.packageManager` field.
*/
export function detectPnpmMajor(cwd = os.tmpdir()): number | undefined {
try {
const out = execSync('pnpm --version', {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore']
stdio: ['ignore', 'pipe', 'ignore'],
cwd
});
return coerceVersion(out.trim()).major;
} catch {
Expand Down
50 changes: 47 additions & 3 deletions packages/sv-utils/src/pnpm.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { detectPnpmMajor } from './pnpm-internals.ts';
import { coerceVersion } from './semver.ts';
import { transforms, type TransformFn } from './tooling/transforms.ts';

type YamlMap = {
Expand All @@ -17,6 +18,24 @@ type YamlDoc = {
createNode(value: unknown, options?: { flow?: boolean }): unknown;
};

export type AllowBuildsOptions = {
/** Directory whose pnpm version should be detected. */
cwd?: string;
/** Explicit pnpm version; skips `pnpm --version` detection. */
pnpmVersion?: string | number;
};

function isAllowBuildsOptions(value: unknown): value is AllowBuildsOptions {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function resolvePnpmMajor(options?: AllowBuildsOptions): number | undefined {
if (options?.pnpmVersion !== undefined) {
return coerceVersion(String(options.pnpmVersion)).major;
}
return detectPnpmMajor(options?.cwd);
}

/**
* Returns a TransformFn for `pnpm-workspace.yaml` that adds packages to the
* pnpm "allow builds" config.
Expand All @@ -26,14 +45,39 @@ type YamlDoc = {
* migrating any legacy `onlyBuiltDependencies` list into the map;
* - on pnpm `< 11` writes to the legacy `onlyBuiltDependencies` list.
*
* Pass `{ cwd }` so detection uses the target project rather than the
* invoker's working directory. Pass `{ pnpmVersion }` to skip detection.
*
* ```ts
* if (packageManager === 'pnpm') {
* sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.allowBuilds('my-native-dep'));
* sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.allowBuilds('my-native-dep', { cwd }));
* }
* ```
*/
export function allowBuilds(...packages: string[]): TransformFn {
const major = detectPnpmMajor();
export function allowBuilds(...packages: string[]): TransformFn;
export function allowBuilds(
...args: [...packages: string[], options: AllowBuildsOptions]
): TransformFn;
export function allowBuilds(packages: string[], options?: AllowBuildsOptions): TransformFn;
export function allowBuilds(
first?: string | string[] | AllowBuildsOptions,
second?: string | AllowBuildsOptions,
...rest: Array<string | AllowBuildsOptions>
): TransformFn {
const args: Array<string | string[] | AllowBuildsOptions> = [];
if (first !== undefined) args.push(first);
if (second !== undefined) args.push(second);
args.push(...rest);

let options: AllowBuildsOptions | undefined;
const last = args.at(-1);
if (isAllowBuildsOptions(last)) {
options = last;
args.pop();
}

const packages = args.length === 1 && Array.isArray(args[0]) ? args[0] : (args as string[]);
const major = resolvePnpmMajor(options);
if (major !== undefined && major < 11) return writeLegacy(packages);
return writeAllowBuilds(packages);
}
Expand Down
60 changes: 47 additions & 13 deletions packages/sv-utils/src/tests/pnpm.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { detectPnpmMajor } from '../pnpm-internals.ts';
import { allowBuilds } from '../pnpm.ts';

const major = detectPnpmMajor();
const isPnpm11 = major === undefined || major >= 11;
describe('allowBuilds (pnpm >= 11: writes allowBuilds map)', () => {
const transform = (pkg: string) => allowBuilds(pkg, { pnpmVersion: 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');
expect(transform('esbuild')('')).toBe('allowBuilds:\n esbuild: true\n');
});

it('appends to existing allowBuilds map', () => {
Expand All @@ -16,7 +18,7 @@ describe.runIf(isPnpm11)('allowBuilds (pnpm >= 11: writes allowBuilds map)', ()
allowBuilds:
bar: true
`;
expect(allowBuilds('esbuild')(input)).toBe(`packages:
expect(transform('esbuild')(input)).toBe(`packages:
- 'packages/*'
allowBuilds:
bar: true
Expand All @@ -28,7 +30,7 @@ allowBuilds:
const input = `allowBuilds:
core-js: false
`;
expect(allowBuilds('esbuild')(input)).toBe(`allowBuilds:
expect(transform('esbuild')(input)).toBe(`allowBuilds:
core-js: false
esbuild: true
`);
Expand All @@ -41,7 +43,7 @@ onlyBuiltDependencies:
- foo
- bar
`;
expect(allowBuilds('esbuild')(input)).toBe(`packages:
expect(transform('esbuild')(input)).toBe(`packages:
- 'packages/*'
allowBuilds:
foo: true
Expand All @@ -56,7 +58,7 @@ allowBuilds:
allowBuilds:
shared: false
`;
expect(allowBuilds('newone')(input)).toBe(`allowBuilds:
expect(transform('newone')(input)).toBe(`allowBuilds:
shared: false
newone: true
`);
Expand All @@ -66,20 +68,22 @@ allowBuilds:
const input = `allowBuilds:
esbuild: true
`;
expect(allowBuilds('esbuild')(input)).toBe(input);
expect(transform('esbuild')(input)).toBe(input);
});
});

describe.runIf(!isPnpm11)('allowBuilds (pnpm < 11: writes onlyBuiltDependencies list)', () => {
describe('allowBuilds (pnpm < 11: writes onlyBuiltDependencies list)', () => {
const transform = (pkg: string) => allowBuilds(pkg, { pnpmVersion: 10 });

it('creates onlyBuiltDependencies list in empty file', () => {
expect(allowBuilds('esbuild')('')).toBe('onlyBuiltDependencies:\n - esbuild\n');
expect(transform('esbuild')('')).toBe('onlyBuiltDependencies:\n - esbuild\n');
});

it('appends to existing onlyBuiltDependencies list', () => {
const input = `onlyBuiltDependencies:
- foo
`;
expect(allowBuilds('esbuild')(input)).toBe(`onlyBuiltDependencies:
expect(transform('esbuild')(input)).toBe(`onlyBuiltDependencies:
- foo
- esbuild
`);
Expand All @@ -89,6 +93,36 @@ describe.runIf(!isPnpm11)('allowBuilds (pnpm < 11: writes onlyBuiltDependencies
const input = `onlyBuiltDependencies:
- esbuild
`;
expect(allowBuilds('esbuild')(input)).toBe(input);
expect(transform('esbuild')(input)).toBe(input);
});
});

describe('allowBuilds version detection', () => {
it('accepts an array of packages plus options', () => {
expect(allowBuilds(['esbuild', 'workerd'], { pnpmVersion: 10 })('')).toBe(
'onlyBuiltDependencies:\n - esbuild\n - workerd\n'
);
});

it('accepts trailing options after rest package names', () => {
expect(allowBuilds('esbuild', 'workerd', { pnpmVersion: 11 })('')).toBe(
'allowBuilds:\n esbuild: true\n workerd: true\n'
);
});

it('detects pnpm from the target cwd, not process.cwd()', { timeout: 30_000 }, () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sv-pnpm-'));
try {
fs.writeFileSync(
path.join(dir, 'package.json'),
JSON.stringify({ name: 'pin11', packageManager: 'pnpm@11.0.0' })
);

expect(detectPnpmMajor(process.cwd())).toBe(10);
expect(detectPnpmMajor(dir)).toBe(11);
expect(allowBuilds('esbuild', { cwd: dir })('')).toBe('allowBuilds:\n esbuild: true\n');
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
});
2 changes: 1 addition & 1 deletion packages/sv/src/addons/sveltekit-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export default defineAddon({
sv.devDependency('wrangler', '^4.97.0');

if (packageManager === 'pnpm') {
sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.allowBuilds('workerd'));
sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.allowBuilds('workerd', { cwd }));
}

// default to jsonc
Expand Down
2 changes: 1 addition & 1 deletion packages/sv/src/core/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,6 @@ export function addPnpmAllowBuilds(
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.allowBuilds(...packages)(content);
const newContent = pnpm.allowBuilds(packages, { cwd })(content);
if (newContent && newContent !== content) fs.writeFileSync(filePath, newContent, 'utf-8');
}