Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support pnpm version 10 #432

Merged
merged 18 commits into from
Feb 7, 2025
5 changes: 5 additions & 0 deletions .changeset/fast-panthers-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'sv': patch
---

feat: support `pnpm` version `10`
27 changes: 24 additions & 3 deletions community-addon-template/tests/setup/suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import path from 'node:path';
import { execSync } from 'node:child_process';
import * as vitest from 'vitest';
import { installAddon, type AddonMap, type OptionMap } from 'sv';
import { createProject, startPreview, type CreateProject, type ProjectVariant } from 'sv/testing';
import {
addPnpmBuildDependendencies,
createProject,
startPreview,
type CreateProject,
type ProjectVariant
} from 'sv/testing';
import { chromium, type Browser, type Page } from '@playwright/test';
import { fileURLToPath } from 'node:url';

Expand Down Expand Up @@ -40,9 +46,18 @@ export function setupTest<Addons extends AddonMap>(addons: Addons) {
// creates a pnpm workspace in each addon dir
fs.writeFileSync(
path.resolve(cwd, testName, 'pnpm-workspace.yaml'),
`packages:\n - '**/*'`,
"packages:\n - '**/*'",
'utf8'
);

// creates a barebones package.json in each addon dir
fs.writeFileSync(
path.resolve(cwd, testName, 'package.json'),
JSON.stringify({
name: `${testName}-workspace-root`,
private: true
})
);
});

// runs before each test case
Expand All @@ -57,7 +72,13 @@ export function setupTest<Addons extends AddonMap>(addons: Addons) {
fs.writeFileSync(metaPath, JSON.stringify({ variant, options }, null, '\t'), 'utf8');

// run addon
await installAddon({ cwd, addons, options, packageManager: 'pnpm' });
const { pnpmBuildDependencies } = await installAddon({
cwd,
addons,
options,
packageManager: 'pnpm'
});
addPnpmBuildDependendencies(cwd, 'pnpm', ['esbuild', ...pnpmBuildDependencies]);

return cwd;
};
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"description": "monorepo for sv and friends",
"engines": {
"pnpm": "^9.0.0"
"pnpm": "^10.0.0"
},
"scripts": {
"build": "rolldown --config",
Expand Down Expand Up @@ -39,5 +39,5 @@
"unplugin-isolated-decl": "^0.8.3",
"vitest": "^3.0.3"
},
"packageManager": "pnpm@9.7.0"
"packageManager": "pnpm@10.1.0"
}
25 changes: 23 additions & 2 deletions packages/addons/_tests/_setup/suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import path from 'node:path';
import { execSync } from 'node:child_process';
import * as vitest from 'vitest';
import { installAddon, type AddonMap, type OptionMap } from 'sv';
import { createProject, startPreview, type CreateProject, type ProjectVariant } from 'sv/testing';
import {
createProject,
startPreview,
addPnpmBuildDependendencies,
type CreateProject,
type ProjectVariant
} from 'sv/testing';
import { chromium, type Browser, type Page } from '@playwright/test';

const cwd = vitest.inject('testDir');
Expand Down Expand Up @@ -40,6 +46,15 @@ export function setupTest<Addons extends AddonMap>(addons: Addons) {
"packages:\n - '**/*'",
'utf8'
);

// creates a barebones package.json in each addon dir
fs.writeFileSync(
path.resolve(cwd, testName, 'package.json'),
JSON.stringify({
name: `${testName}-workspace-root`,
private: true
})
);
});

// runs before each test case
Expand All @@ -54,7 +69,13 @@ export function setupTest<Addons extends AddonMap>(addons: Addons) {
fs.writeFileSync(metaPath, JSON.stringify({ variant, options }, null, '\t'), 'utf8');

// run addon
await installAddon({ cwd, addons, options, packageManager: 'pnpm' });
const { pnpmBuildDependencies } = await installAddon({
cwd,
addons,
options,
packageManager: 'pnpm'
});
addPnpmBuildDependendencies(cwd, 'pnpm', ['esbuild', ...pnpmBuildDependencies]);

return cwd;
};
Expand Down
1 change: 1 addition & 0 deletions packages/addons/drizzle/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export default defineAddon({
if (options.sqlite === 'better-sqlite3') {
sv.dependency('better-sqlite3', '^11.8.0');
sv.devDependency('@types/better-sqlite3', '^7.6.12');
sv.pnpmBuildDependendency('better-sqlite3');
}

if (options.sqlite === 'libsql' || options.sqlite === 'turso')
Expand Down
33 changes: 21 additions & 12 deletions packages/cli/commands/add/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import * as common from '../../utils/common.ts';
import { createWorkspace } from './workspace.ts';
import { formatFiles, getHighlighter } from './utils.ts';
import { Directive, downloadPackage, getPackageJSON } from './fetch-packages.ts';
import { installDependencies, packageManagerPrompt } from '../../utils/package-manager.ts';
import {
addPnpmBuildDependendencies,
installDependencies,
packageManagerPrompt
} from '../../utils/package-manager.ts';
import { getGlobalPreconditions } from './preconditions.ts';
import { type AddonMap, applyAddons, setupAddons } from '../../lib/install.ts';

Expand Down Expand Up @@ -425,13 +429,6 @@ export async function runAddCommand(
// indicating that installing deps was skipped and no PM was selected
if (selectedAddons.length === 0) return { packageManager: null };

// prompt for package manager
let packageManager: PackageManager | undefined;
if (options.install) {
packageManager = await packageManagerPrompt(options.cwd);
if (packageManager) workspace.packageManager = packageManager;
}

// apply addons
const officialDetails = Object.keys(official).map((id) => getAddonDetails(id));
const commDetails = Object.keys(community).map(
Expand All @@ -440,7 +437,7 @@ export async function runAddCommand(
const details = officialDetails.concat(commDetails);

const addonMap: AddonMap = Object.assign({}, ...details.map((a) => ({ [a.id]: a })));
const filesToFormat = await applyAddons({
const { filesToFormat, pnpmBuildDependencies: addonPnpmBuildDependencies } = await applyAddons({
workspace,
addonSetupResults,
addons: addonMap,
Expand All @@ -449,9 +446,21 @@ export async function runAddCommand(

p.log.success('Successfully setup add-ons');

// install dependencies
if (packageManager && options.install) {
await installDependencies(packageManager, options.cwd);
// prompt for package manager and install dependencies
let packageManager: PackageManager | undefined;
if (options.install) {
packageManager = await packageManagerPrompt(options.cwd);

if (packageManager) {
workspace.packageManager = packageManager;

addPnpmBuildDependendencies(workspace.cwd, packageManager, [
'esbuild',
...addonPnpmBuildDependencies
]);

await installDependencies(packageManager, options.cwd);
}
}

// format modified/created files with prettier (if available)
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import * as common from '../utils/common.ts';
import { runAddCommand } from './add/index.ts';
import { detectSync, resolveCommand, type AgentName } from 'package-manager-detector';
import {
addPnpmBuildDependendencies,
getUserAgent,
installDependencies,
packageManagerPrompt
Expand Down Expand Up @@ -164,6 +165,7 @@ async function createProject(cwd: ProjectPath, options: Options) {
let addOnNextSteps: string | undefined;
const installDeps = async () => {
packageManager = await packageManagerPrompt(projectPath);
addPnpmBuildDependendencies(projectPath, packageManager, ['esbuild']);
if (packageManager) await installDependencies(packageManager, projectPath);
};

Expand Down
32 changes: 26 additions & 6 deletions packages/cli/lib/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function installAddon<Addons extends AddonMap>({
cwd,
options,
packageManager = 'npm'
}: InstallOptions<Addons>): Promise<string[]> {
}: InstallOptions<Addons>): Promise<ReturnType<typeof applyAddons>> {
const workspace = createWorkspace({ cwd, packageManager });
const addonSetupResults = setupAddons(Object.values(addons), workspace);

Expand All @@ -51,20 +51,33 @@ export async function applyAddons({
workspace,
addonSetupResults,
options
}: ApplyAddonOptions): Promise<string[]> {
}: ApplyAddonOptions): Promise<{
filesToFormat: string[];
pnpmBuildDependencies: string[];
}> {
const filesToFormat = new Set<string>();
const allPnpmBuildDependencies: string[] = [];

const mapped = Object.entries(addons).map(([, addon]) => addon);
const ordered = orderAddons(mapped, addonSetupResults);

for (const addon of ordered) {
workspace = createWorkspace({ ...workspace, options: options[addon.id] });

const files = await runAddon({ workspace, addon, multiple: ordered.length > 1 });
const { files, pnpmBuildDependencies } = await runAddon({
workspace,
addon,
multiple: ordered.length > 1
});

files.forEach((f) => filesToFormat.add(f));
pnpmBuildDependencies.forEach((s) => allPnpmBuildDependencies.push(s));
}

return Array.from(filesToFormat);
return {
filesToFormat: Array.from(filesToFormat),
pnpmBuildDependencies: allPnpmBuildDependencies
};
}

export function setupAddons(
Expand All @@ -91,7 +104,7 @@ type RunAddon = {
addon: Addon<Record<string, Question>>;
multiple: boolean;
};
async function runAddon({ addon, multiple, workspace }: RunAddon): Promise<string[]> {
async function runAddon({ addon, multiple, workspace }: RunAddon) {
const files = new Set<string>();

// apply default addon options
Expand All @@ -103,6 +116,7 @@ async function runAddon({ addon, multiple, workspace }: RunAddon): Promise<strin
}

const dependencies: Array<{ pkg: string; version: string; dev: boolean }> = [];
const pnpmBuildDependencies: string[] = [];
const sv: SvApi = {
file: (path, content) => {
try {
Expand Down Expand Up @@ -150,14 +164,20 @@ async function runAddon({ addon, multiple, workspace }: RunAddon): Promise<strin
},
devDependency: (pkg, version) => {
dependencies.push({ pkg, version, dev: true });
},
pnpmBuildDependendency: (pkg) => {
pnpmBuildDependencies.push(pkg);
}
};
await addon.run({ ...workspace, sv });

const pkgPath = installPackages(dependencies, workspace);
files.add(pkgPath);

return Array.from(files);
return {
files: Array.from(files),
pnpmBuildDependencies
};
}

// sorts them to their execution order
Expand Down
1 change: 1 addition & 0 deletions packages/cli/lib/testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { exec } from 'tinyexec';
import { create } from '@sveltejs/create';
import pstree, { type PS } from 'ps-tree';

export { addPnpmBuildDependendencies } from '../utils/package-manager.ts';
export type ProjectVariant = 'kit-js' | 'kit-ts' | 'vite-js' | 'vite-ts';

const TEMPLATES_DIR = '.templates';
Expand Down
34 changes: 34 additions & 0 deletions packages/cli/utils/package-manager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import * as find from 'empathic/find';
import { exec } from 'tinyexec';
import * as p from '@sveltejs/clack-prompts';
import {
Expand All @@ -8,6 +11,7 @@ import {
detectSync,
type AgentName
} from 'package-manager-detector';
import { parseJson } from '@sveltejs/cli-core/parsers';

const agents = AGENTS.filter((agent): agent is AgentName => !agent.includes('@'));
const agentOptions: PackageManagerOptions = agents.map((pm) => ({ value: pm, label: pm }));
Expand Down Expand Up @@ -67,3 +71,33 @@ export function getUserAgent(): AgentName | undefined {
const name = pmSpec.substring(0, separatorPos) as AgentName;
return AGENTS.includes(name) ? name : undefined;
}

export function addPnpmBuildDependendencies(
cwd: string,
packageManager: AgentName | null | undefined,
allowedPackages: string[]
) {
// other package managers are currently not affected by this change
if (!packageManager || packageManager !== 'pnpm') return;

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

// load the package.json
const pkgPath = path.join(path.dirname(pnpmWorkspacePath), 'package.json');
const content = fs.readFileSync(pkgPath, 'utf-8');
const { data, generateCode } = parseJson(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();
fs.writeFileSync(pkgPath, newContent);
}
3 changes: 2 additions & 1 deletion packages/core/addon/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ export type Scripts<Args extends OptionDefinition> = {
};

export type SvApi = {
file: (path: string, edit: (content: string) => string) => void;
pnpmBuildDependendency: (pkg: string) => void;
dependency: (pkg: string, version: string) => void;
devDependency: (pkg: string, version: string) => void;
execute: (args: string[], stdio: 'inherit' | 'pipe') => Promise<void>;
file: (path: string, edit: (content: string) => string) => void;
};

export type Addon<Args extends OptionDefinition> = {
Expand Down