diff --git a/.changeset/remove-pnpm-build-dependency.md b/.changeset/remove-pnpm-build-dependency.md new file mode 100644 index 000000000..41b340900 --- /dev/null +++ b/.changeset/remove-pnpm-build-dependency.md @@ -0,0 +1,6 @@ +--- +'sv': minor +'@sveltejs/sv-utils': minor +--- + +feat: replace `sv.pnpmBuildDependency` with `sv.file` + `pnpm.onlyBuiltDependencies` helper and `file.findUp` diff --git a/documentation/docs/50-api/10-sv.md b/documentation/docs/50-api/10-sv.md index 70809023a..7e74f6f57 100644 --- a/documentation/docs/50-api/10-sv.md +++ b/documentation/docs/50-api/10-sv.md @@ -48,7 +48,7 @@ export default defineAddon({ }); ``` -The `sv` object in `run` provides `file`, `dependency`, `devDependency`, `execute`, and `pnpmBuildDependency`. For file transforms (AST-based editing of scripts, Svelte components, CSS, JSON, etc.), see [`@sveltejs/sv-utils`](sv-utils). +The `sv` object in `run` provides `file`, `dependency`, `devDependency`, and `execute`. For file transforms (AST-based editing of scripts, Svelte components, CSS, JSON, etc.) and package manager helpers, see [`@sveltejs/sv-utils`](sv-utils). ## `defineAddonOptions` diff --git a/documentation/docs/50-api/20-sv-utils.md b/documentation/docs/50-api/20-sv-utils.md index 600ca9a0c..b8dcccf43 100644 --- a/documentation/docs/50-api/20-sv-utils.md +++ b/documentation/docs/50-api/20-sv-utils.md @@ -229,3 +229,18 @@ Namespaced helpers for AST manipulation: - **`json.*`** - arrayUpsert, packageScriptsUpsert - **`html.*`** - attribute manipulation - **`text.*`** - upsert lines in flat files (.env, .gitignore) + +## Package manager helpers + +### `pnpm.onlyBuiltDependencies` + +Returns a transform for `pnpm-workspace.yaml` that adds packages to the `onlyBuiltDependencies` list. Use with `sv.file` when the project uses pnpm. + +```js +// @noErrors +import { pnpm } from '@sveltejs/sv-utils'; + +if (packageManager === 'pnpm') { + sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.onlyBuiltDependencies('my-native-dep')); +} +``` diff --git a/packages/sv-utils/src/index.ts b/packages/sv-utils/src/index.ts index 29005fa4a..64c7b21e2 100644 --- a/packages/sv-utils/src/index.ts +++ b/packages/sv-utils/src/index.ts @@ -39,6 +39,9 @@ export * as text from './tooling/text.ts'; export * as json from './tooling/json.ts'; export * as svelte from './tooling/svelte/index.ts'; +// Package manager helpers +export * as pnpm from './pnpm.ts'; + // Transforms — sv-utils = what to do to content, sv = where and when to do it. export { transforms } from './tooling/transforms.ts'; diff --git a/packages/sv-utils/src/pnpm.ts b/packages/sv-utils/src/pnpm.ts new file mode 100644 index 000000000..b9cc34987 --- /dev/null +++ b/packages/sv-utils/src/pnpm.ts @@ -0,0 +1,25 @@ +import type { TransformFn } from './tooling/transforms.ts'; +import { transforms } from './tooling/transforms.ts'; + +/** + * Returns a TransformFn for `pnpm-workspace.yaml` that adds packages to `onlyBuiltDependencies`. + * + * Use with `sv.file`: + * ```ts + * if (packageManager === 'pnpm') { + * sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.onlyBuiltDependencies('my-native-dep')); + * } + * ``` + */ +export function onlyBuiltDependencies(...packages: string[]): TransformFn { + return transforms.yaml(({ data }) => { + const existing = data.get('onlyBuiltDependencies'); + const items: Array<{ value: string } | string> = existing?.items ?? []; + for (const pkg of packages) { + if (items.includes(pkg)) continue; + if (items.some((y) => typeof y === 'object' && y.value === pkg)) continue; + items.push(pkg); + } + data.set('onlyBuiltDependencies', items); + }); +} diff --git a/packages/sv/src/addons/drizzle.ts b/packages/sv/src/addons/drizzle.ts index f807f11cc..d4269a705 100644 --- a/packages/sv/src/addons/drizzle.ts +++ b/packages/sv/src/addons/drizzle.ts @@ -3,6 +3,7 @@ import { dedent, type TransformFn, transforms, + pnpm, resolveCommandArray, fileExists, createPrinter @@ -89,7 +90,7 @@ export default defineAddon({ if (!isKit) return unsupported('Requires SvelteKit'); }, - run: ({ sv, language, options, directory, dependencyVersion, cwd, cancel, file }) => { + run: ({ sv, language, options, directory, dependencyVersion, cwd, cancel, file, packageManager }) => { if (options.database === 'd1' && !dependencyVersion('@sveltejs/adapter-cloudflare')) { return cancel('Cloudflare D1 requires @sveltejs/adapter-cloudflare - add the adapter first'); } @@ -124,7 +125,9 @@ export default defineAddon({ // not a devDependency due to bundling issues sv.dependency('better-sqlite3', '^12.6.2'); sv.devDependency('@types/better-sqlite3', '^7.6.13'); - sv.pnpmBuildDependency('better-sqlite3'); + if (packageManager === 'pnpm') { + sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.onlyBuiltDependencies('better-sqlite3')); + } } if (options.sqlite === 'libsql' || options.sqlite === 'turso') diff --git a/packages/sv/src/addons/tailwindcss.ts b/packages/sv/src/addons/tailwindcss.ts index 5dd26ce2c..151d0fe66 100644 --- a/packages/sv/src/addons/tailwindcss.ts +++ b/packages/sv/src/addons/tailwindcss.ts @@ -1,4 +1,4 @@ -import { transforms } from '@sveltejs/sv-utils'; +import { pnpm, transforms } from '@sveltejs/sv-utils'; import { defineAddon, defineAddonOptions } from '../core/config.ts'; const plugins = [ @@ -30,12 +30,14 @@ export default defineAddon({ shortDescription: 'css framework', homepage: 'https://tailwindcss.com', options, - run: ({ sv, options, file, isKit, directory, dependencyVersion, language }) => { + run: ({ sv, options, file, isKit, directory, dependencyVersion, language, packageManager }) => { const prettierInstalled = Boolean(dependencyVersion('prettier')); sv.devDependency('tailwindcss', '^4.1.18'); sv.devDependency('@tailwindcss/vite', '^4.1.18'); - sv.pnpmBuildDependency('@tailwindcss/oxide'); + if (packageManager === 'pnpm') { + sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.onlyBuiltDependencies('@tailwindcss/oxide')); + } if (prettierInstalled) sv.devDependency('prettier-plugin-tailwindcss', '^0.7.2'); diff --git a/packages/sv/src/cli/add.ts b/packages/sv/src/cli/add.ts index 466613ab6..71c617449 100644 --- a/packages/sv/src/cli/add.ts +++ b/packages/sv/src/cli/add.ts @@ -677,7 +677,7 @@ export async function runAddonsApply({ setupResults: {} }; - const { filesToFormat, pnpmBuildDependencies, status } = await applyAddons({ + const { filesToFormat, status } = await applyAddons({ loadedAddons, workspace, setupResults, @@ -712,10 +712,7 @@ export async function runAddonsApply({ ? await packageManagerPrompt(options.cwd) : options.install; - await addPnpmBuildDependencies(workspace.cwd, packageManager, [ - 'esbuild', - ...pnpmBuildDependencies - ]); + await addPnpmBuildDependencies(workspace.cwd, packageManager, ['esbuild']); const argsFormattedAddons: string[] = []; for (const loaded of successfulAddons) { diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/pnpm-workspace.yaml b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/pnpm-workspace.yaml new file mode 100644 index 000000000..fd050a463 --- /dev/null +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - '@tailwindcss/oxide' diff --git a/packages/sv/src/core/config.ts b/packages/sv/src/core/config.ts index 3d00c1412..b0c02226e 100644 --- a/packages/sv/src/core/config.ts +++ b/packages/sv/src/core/config.ts @@ -21,8 +21,6 @@ export type Scripts = { }; export type SvApi = { - /** Add a package to the pnpm onlyBuiltDependencies. */ - pnpmBuildDependency: (pkg: string) => void; /** Add a package to the dependencies. */ dependency: (pkg: string, version: string) => void; /** Add a package to the dev dependencies. */ diff --git a/packages/sv/src/core/engine.ts b/packages/sv/src/core/engine.ts index b0bc4d053..8a8be6df1 100644 --- a/packages/sv/src/core/engine.ts +++ b/packages/sv/src/core/engine.ts @@ -71,11 +71,9 @@ export async function applyAddons({ options }: ApplyAddonOptions): Promise<{ filesToFormat: string[]; - pnpmBuildDependencies: string[]; status: Record; }> { const filesToFormat = new Set(); - const allPnpmBuildDependencies: string[] = []; const status: Record = {}; const addonDefs = loadedAddons.map((l) => l.addon); @@ -95,7 +93,7 @@ export async function applyAddons({ // If we don't have a formatter yet, check if the addon adds one if (!hasFormatter) hasFormatter = !!addonWorkspace.dependencyVersion('prettier'); - const { files, pnpmBuildDependencies, cancels } = await runAddon({ + const { files, cancels } = await runAddon({ workspace: addonWorkspace, workspaceOptions, addon, @@ -104,7 +102,6 @@ export async function applyAddons({ }); files.forEach((f) => filesToFormat.add(f)); - pnpmBuildDependencies.forEach((s) => allPnpmBuildDependencies.push(s)); if (cancels.length === 0) { status[addon.id] = 'success'; } else { @@ -114,7 +111,6 @@ export async function applyAddons({ return { filesToFormat: hasFormatter ? Array.from(filesToFormat) : [], - pnpmBuildDependencies: allPnpmBuildDependencies, status }; } @@ -176,7 +172,6 @@ async function runAddon({ addon, loaded, multiple, workspace, workspaceOptions } } const dependencies: Array<{ pkg: string; version: string; dev: boolean }> = []; - const pnpmBuildDependencies: string[] = []; const sv: SvApi = { file: (path, edit) => { try { @@ -225,9 +220,6 @@ async function runAddon({ addon, loaded, multiple, workspace, workspaceOptions } }, devDependency: (pkg, version) => { dependencies.push({ pkg, version, dev: true }); - }, - pnpmBuildDependency: (pkg) => { - pnpmBuildDependencies.push(pkg); } }; @@ -256,7 +248,6 @@ async function runAddon({ addon, loaded, multiple, workspace, workspaceOptions } return { files: Array.from(files), - pnpmBuildDependencies, cancels }; } diff --git a/packages/sv/src/core/package-manager.ts b/packages/sv/src/core/package-manager.ts index c0ed185c7..8dba53fc8 100644 --- a/packages/sv/src/core/package-manager.ts +++ b/packages/sv/src/core/package-manager.ts @@ -6,7 +6,6 @@ import { color, constructCommand, detect, - isVersionUnsupportedBelow, parse } from '@sveltejs/sv-utils'; import { Option } from 'commander'; @@ -98,63 +97,24 @@ export async function addPnpmBuildDependencies( packageManager: AgentName | null | undefined, allowedPackages: string[] ): Promise { - // other package managers are currently not affected by this change if (!packageManager || packageManager !== 'pnpm' || allowedPackages.length === 0) return; - let confIn: 'package.json' | 'pnpm-workspace.yaml' = 'package.json'; - const pnpmVersion = await getPnpmVersion(); - if (pnpmVersion) { - confIn = isVersionUnsupportedBelow(pnpmVersion, '10.5') - ? 'package.json' - : 'pnpm-workspace.yaml'; - } - // find the workspace root (if present) const found = find.up('pnpm-workspace.yaml', { cwd }); + const content = found ? fs.readFileSync(found, 'utf-8') : ''; + const { data, generateCode } = parse.yaml(content); - if (confIn === 'pnpm-workspace.yaml') { - const content = found ? fs.readFileSync(found, 'utf-8') : ''; - const { data, generateCode } = parse.yaml(content); - - const onlyBuiltDependencies = data.get('onlyBuiltDependencies'); - const items: Array<{ value: string } | string> = onlyBuiltDependencies?.items ?? []; - - for (const item of allowedPackages) { - if (items.includes(item)) continue; - if (items.some((y) => typeof y === 'object' && y.value === item)) continue; - items.push(item); - } - data.set('onlyBuiltDependencies', items); - - const newContent = generateCode(); - const pnpmWorkspacePath = found ?? path.join(cwd, 'pnpm-workspace.yaml'); - if (newContent !== content) fs.writeFileSync(pnpmWorkspacePath, newContent, 'utf-8'); - } else { - // else is package.json (fallback) - const rootDir = found ? path.dirname(found) : cwd; - const pkgPath = path.join(rootDir, 'package.json'); - const content = fs.readFileSync(pkgPath, 'utf-8'); - const { data, generateCode } = parse.json(content); - - // add the packages where we install scripts should be executed - data.pnpm ??= {}; - data.pnpm.onlyBuiltDependencies ??= []; - for (const allowedPackage of allowedPackages) { - if (data.pnpm.onlyBuiltDependencies.includes(allowedPackage)) continue; - data.pnpm.onlyBuiltDependencies.push(allowedPackage); - } + const onlyBuiltDependencies = data.get('onlyBuiltDependencies'); + const items: Array<{ value: string } | string> = onlyBuiltDependencies?.items ?? []; - // save the updated package.json - const newContent = generateCode(); - if (newContent !== content) fs.writeFileSync(pkgPath, newContent, 'utf-8'); + for (const item of allowedPackages) { + if (items.includes(item)) continue; + if (items.some((y) => typeof y === 'object' && y.value === item)) continue; + items.push(item); } -} + data.set('onlyBuiltDependencies', items); -async function getPnpmVersion(): Promise { - let v: string | undefined = undefined; - try { - const proc = await exec('pnpm', ['--version'], { throwOnError: true }); - v = proc.stdout.trim(); - } catch {} - return v; + const newContent = generateCode(); + const pnpmWorkspacePath = found ?? path.join(cwd, 'pnpm-workspace.yaml'); + if (newContent !== content) fs.writeFileSync(pnpmWorkspacePath, newContent, 'utf-8'); } diff --git a/packages/sv/src/core/workspace.ts b/packages/sv/src/core/workspace.ts index 987787419..092d75627 100644 --- a/packages/sv/src/core/workspace.ts +++ b/packages/sv/src/core/workspace.ts @@ -46,6 +46,12 @@ export type Workspace = { /** Get the relative path between two files */ getRelative: ({ from, to }: { from?: string; to: string }) => string; + + /** + * Find a file by walking up the directory tree from cwd. + * Returns the relative path from cwd, or the filename itself if not found. + */ + findUp: (filename: string) => string; }; isKit: boolean; directory: { @@ -155,6 +161,15 @@ export async function createWorkspace({ relativePath = `./${relativePath}`; } return relativePath; + }, + findUp(filename) { + const found = find.up(filename, { cwd: resolvedCwd }); + if (!found) return filename; + // don't escape .test-output during tests + if (resolvedCwd.includes('.test-output') && !found.includes('.test-output')) { + return filename; + } + return path.relative(resolvedCwd, found); } }, isKit, diff --git a/packages/sv/src/testing.ts b/packages/sv/src/testing.ts index 14bfb309a..4adc8a1fd 100644 --- a/packages/sv/src/testing.ts +++ b/packages/sv/src/testing.ts @@ -326,13 +326,13 @@ export function createSetupTest(vitest: VitestContext):