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
6 changes: 6 additions & 0 deletions .changeset/remove-pnpm-build-dependency.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'sv': minor
'@sveltejs/sv-utils': minor
---

feat: replace `sv.pnpmBuildDependency` with `sv.file` + `pnpm.onlyBuiltDependencies` helper and `file.findUp`
2 changes: 1 addition & 1 deletion documentation/docs/50-api/10-sv.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
15 changes: 15 additions & 0 deletions documentation/docs/50-api/20-sv-utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
}
```
3 changes: 3 additions & 0 deletions packages/sv-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
24 changes: 24 additions & 0 deletions packages/sv-utils/src/pnpm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { transforms, type TransformFn } 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);
});
}
17 changes: 15 additions & 2 deletions packages/sv/src/addons/drizzle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
dedent,
type TransformFn,
transforms,
pnpm,
resolveCommandArray,
fileExists,
createPrinter
Expand Down Expand Up @@ -89,7 +90,17 @@ 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');
}
Expand Down Expand Up @@ -124,7 +135,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')
Expand Down
8 changes: 5 additions & 3 deletions packages/sv/src/addons/tailwindcss.ts
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -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');

Expand Down
9 changes: 3 additions & 6 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,
addPnpmBuildDependencies,
addPnpmOnlyBuiltDependencies,
installDependencies,
installOption,
packageManagerPrompt
Expand Down Expand Up @@ -677,7 +677,7 @@ export async function runAddonsApply({
setupResults: {}
};

const { filesToFormat, pnpmBuildDependencies, status } = await applyAddons({
const { filesToFormat, status } = await applyAddons({
loadedAddons,
workspace,
setupResults,
Expand Down Expand Up @@ -712,10 +712,7 @@ export async function runAddonsApply({
? await packageManagerPrompt(options.cwd)
: options.install;

await addPnpmBuildDependencies(workspace.cwd, packageManager, [
'esbuild',
...pnpmBuildDependencies
]);
addPnpmOnlyBuiltDependencies(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,
addPnpmBuildDependencies,
addPnpmOnlyBuiltDependencies,
detectPackageManager,
installDependencies,
installOption,
Expand Down Expand Up @@ -384,7 +384,7 @@ async function createProject(cwd: ProjectPath, options: Options) {
}
const addOnNextSteps = getNextSteps(addOnSuccessfulAddons, workspace, answers, addonSetupResults);

await addPnpmBuildDependencies(projectPath, packageManager, ['esbuild']);
addPnpmOnlyBuiltDependencies(projectPath, packageManager, 'esbuild');
if (packageManager) {
await installDependencies(packageManager, projectPath);
await formatFiles({ packageManager, cwd: projectPath, filesToFormat: addOnFilesToFormat });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
onlyBuiltDependencies:
- '@tailwindcss/oxide'
2 changes: 0 additions & 2 deletions packages/sv/src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
11 changes: 1 addition & 10 deletions packages/sv/src/core/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,9 @@ export async function applyAddons({
options
}: ApplyAddonOptions): Promise<{
filesToFormat: string[];
pnpmBuildDependencies: string[];
status: Record<string, string[] | 'success'>;
}> {
const filesToFormat = new Set<string>();
const allPnpmBuildDependencies: string[] = [];
const status: Record<string, string[] | 'success'> = {};

const addonDefs = loadedAddons.map((l) => l.addon);
Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -114,7 +111,6 @@ export async function applyAddons({

return {
filesToFormat: hasFormatter ? Array.from(filesToFormat) : [],
pnpmBuildDependencies: allPnpmBuildDependencies,
status
};
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
};

Expand Down Expand Up @@ -256,7 +248,6 @@ async function runAddon({ addon, loaded, multiple, workspace, workspaceOptions }

return {
files: Array.from(files),
pnpmBuildDependencies,
cancels
};
}
Expand Down
71 changes: 9 additions & 62 deletions packages/sv/src/core/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import {
color,
constructCommand,
detect,
isVersionUnsupportedBelow,
parse
pnpm
} from '@sveltejs/sv-utils';
import { Option } from 'commander';
import * as find from 'empathic/find';
Expand Down Expand Up @@ -93,68 +92,16 @@ export function getUserAgent(): AgentName | undefined {
return AGENTS.includes(name) ? name : undefined;
}

export async function addPnpmBuildDependencies(
export function addPnpmOnlyBuiltDependencies(
cwd: string,
packageManager: AgentName | null | undefined,
allowedPackages: string[]
): Promise<void> {
// 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';
}
...packages: string[]
): void {
if (packageManager !== 'pnpm' || packages.length === 0) return;

// find the workspace root (if present)
const found = find.up('pnpm-workspace.yaml', { cwd });

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);
}

// save the updated package.json
const newContent = generateCode();
if (newContent !== content) fs.writeFileSync(pkgPath, newContent, 'utf-8');
}
}

async function getPnpmVersion(): Promise<string | undefined> {
let v: string | undefined = undefined;
try {
const proc = await exec('pnpm', ['--version'], { throwOnError: true });
v = proc.stdout.trim();
} catch {}
return v;
const filePath = found ?? path.join(cwd, 'pnpm-workspace.yaml');
const content = found ? fs.readFileSync(found, 'utf-8') : '';
const newContent = pnpm.onlyBuiltDependencies(...packages)(content);
if (newContent !== content) fs.writeFileSync(filePath, newContent, 'utf-8');
}
15 changes: 15 additions & 0 deletions packages/sv/src/core/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 3 additions & 4 deletions packages/sv/src/testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ import pstree, { type PS } from 'ps-tree';
import { exec, x } from 'tinyexec';
import type { TestProject } from 'vitest/node';
import { add, type AddonMap, type OptionMap } from './core/engine.ts';
import { addPnpmBuildDependencies } from './core/package-manager.ts';
import { addPnpmOnlyBuiltDependencies } from './core/package-manager.ts';
import { create } from './create/index.ts';

export { addPnpmBuildDependencies } from './core/package-manager.ts';
export type ProjectVariant = 'kit-js' | 'kit-ts' | 'vite-js' | 'vite-ts';
export const variants: ProjectVariant[] = ['kit-js', 'kit-ts', 'vite-js', 'vite-ts'];

Expand Down Expand Up @@ -326,13 +325,13 @@ export function createSetupTest(vitest: VitestContext): <Addons extends AddonMap
if (options?.preAdd) {
await options.preAdd({ addonTestCase, cwd });
}
const { pnpmBuildDependencies } = await add({
await add({
cwd,
addons,
options: kind.options,
packageManager: 'pnpm'
});
await addPnpmBuildDependencies(cwd, 'pnpm', ['esbuild', ...pnpmBuildDependencies]);
addPnpmOnlyBuiltDependencies(cwd, 'pnpm', 'esbuild');
}

execSync('pnpm install', { cwd: path.resolve(cwd, testName), stdio: 'pipe' });
Expand Down
Loading