diff --git a/.changeset/fast-panthers-smile.md b/.changeset/fast-panthers-smile.md new file mode 100644 index 00000000..a9e1376c --- /dev/null +++ b/.changeset/fast-panthers-smile.md @@ -0,0 +1,5 @@ +--- +'sv': patch +--- + +feat: support `pnpm` version `10` diff --git a/community-addon-template/tests/setup/suite.ts b/community-addon-template/tests/setup/suite.ts index 46f3f8c8..9b2dc0a5 100644 --- a/community-addon-template/tests/setup/suite.ts +++ b/community-addon-template/tests/setup/suite.ts @@ -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'; @@ -40,9 +46,18 @@ export function setupTest(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 @@ -57,7 +72,13 @@ export function setupTest(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; }; diff --git a/package.json b/package.json index c96a4259..1dab2e7d 100644 --- a/package.json +++ b/package.json @@ -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", @@ -39,5 +39,5 @@ "unplugin-isolated-decl": "^0.8.3", "vitest": "^3.0.3" }, - "packageManager": "pnpm@9.7.0" + "packageManager": "pnpm@10.1.0" } diff --git a/packages/addons/_tests/_setup/suite.ts b/packages/addons/_tests/_setup/suite.ts index 9af9de81..5c6e5a1b 100644 --- a/packages/addons/_tests/_setup/suite.ts +++ b/packages/addons/_tests/_setup/suite.ts @@ -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'); @@ -40,6 +46,15 @@ export function setupTest(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 @@ -54,7 +69,13 @@ export function setupTest(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; }; diff --git a/packages/addons/drizzle/index.ts b/packages/addons/drizzle/index.ts index 698cdc1d..0cf8fa69 100644 --- a/packages/addons/drizzle/index.ts +++ b/packages/addons/drizzle/index.ts @@ -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') diff --git a/packages/cli/commands/add/index.ts b/packages/cli/commands/add/index.ts index 32014c35..054b47b8 100644 --- a/packages/cli/commands/add/index.ts +++ b/packages/cli/commands/add/index.ts @@ -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'; @@ -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( @@ -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, @@ -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) diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts index d6a8c552..77a5d8ce 100644 --- a/packages/cli/commands/create.ts +++ b/packages/cli/commands/create.ts @@ -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 @@ -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); }; diff --git a/packages/cli/lib/install.ts b/packages/cli/lib/install.ts index 72d5173c..6f98ce43 100644 --- a/packages/cli/lib/install.ts +++ b/packages/cli/lib/install.ts @@ -33,7 +33,7 @@ export async function installAddon({ cwd, options, packageManager = 'npm' -}: InstallOptions): Promise { +}: InstallOptions): Promise> { const workspace = createWorkspace({ cwd, packageManager }); const addonSetupResults = setupAddons(Object.values(addons), workspace); @@ -51,8 +51,12 @@ export async function applyAddons({ workspace, addonSetupResults, options -}: ApplyAddonOptions): Promise { +}: ApplyAddonOptions): Promise<{ + filesToFormat: string[]; + pnpmBuildDependencies: string[]; +}> { const filesToFormat = new Set(); + const allPnpmBuildDependencies: string[] = []; const mapped = Object.entries(addons).map(([, addon]) => addon); const ordered = orderAddons(mapped, addonSetupResults); @@ -60,11 +64,20 @@ export async function applyAddons({ 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( @@ -91,7 +104,7 @@ type RunAddon = { addon: Addon>; multiple: boolean; }; -async function runAddon({ addon, multiple, workspace }: RunAddon): Promise { +async function runAddon({ addon, multiple, workspace }: RunAddon) { const files = new Set(); // apply default addon options @@ -103,6 +116,7 @@ async function runAddon({ addon, multiple, workspace }: RunAddon): Promise = []; + const pnpmBuildDependencies: string[] = []; const sv: SvApi = { file: (path, content) => { try { @@ -150,6 +164,9 @@ async function runAddon({ addon, multiple, workspace }: RunAddon): Promise { dependencies.push({ pkg, version, dev: true }); + }, + pnpmBuildDependendency: (pkg) => { + pnpmBuildDependencies.push(pkg); } }; await addon.run({ ...workspace, sv }); @@ -157,7 +174,10 @@ async function runAddon({ addon, multiple, workspace }: RunAddon): Promise !agent.includes('@')); const agentOptions: PackageManagerOptions = agents.map((pm) => ({ value: pm, label: pm })); @@ -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); +} diff --git a/packages/core/addon/config.ts b/packages/core/addon/config.ts index 9890046f..be36f777 100644 --- a/packages/core/addon/config.ts +++ b/packages/core/addon/config.ts @@ -20,10 +20,11 @@ export type Scripts = { }; 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; + file: (path: string, edit: (content: string) => string) => void; }; export type Addon = {