diff --git a/.changeset/nasty-towns-crash.md b/.changeset/nasty-towns-crash.md new file mode 100644 index 000000000..9e9f474d2 --- /dev/null +++ b/.changeset/nasty-towns-crash.md @@ -0,0 +1,5 @@ +--- +'sv': patch +--- + +fix(tailwindcss): add `@tailwindcss/oxide` to approve-builds in `pnpm` diff --git a/.changeset/solid-dragons-trade.md b/.changeset/solid-dragons-trade.md new file mode 100644 index 000000000..481b024ed --- /dev/null +++ b/.changeset/solid-dragons-trade.md @@ -0,0 +1,5 @@ +--- +'sv': patch +--- + +feat(cli): pnpm config will now be stored in `pnpm-workspace.yaml` (e.g. `onlyBuiltDependencies`) diff --git a/community-addon-template/tests/setup/suite.ts b/community-addon-template/tests/setup/suite.ts index 3b7c04138..db7dd06d1 100644 --- a/community-addon-template/tests/setup/suite.ts +++ b/community-addon-template/tests/setup/suite.ts @@ -78,7 +78,7 @@ export function setupTest(addons: Addons) { options, packageManager: 'pnpm' }); - addPnpmBuildDependencies(cwd, 'pnpm', ['esbuild', ...pnpmBuildDependencies]); + await addPnpmBuildDependencies(cwd, 'pnpm', ['esbuild', ...pnpmBuildDependencies]); return cwd; }; diff --git a/package.json b/package.json index 82cbc963e..da6e3308b 100644 --- a/package.json +++ b/package.json @@ -40,10 +40,5 @@ "unplugin-isolated-decl": "^0.8.3", "vitest": "4.0.0-beta.6" }, - "packageManager": "pnpm@10.4.1", - "pnpm": { - "onlyBuiltDependencies": [ - "esbuild" - ] - } + "packageManager": "pnpm@10.17.0" } diff --git a/packages/addons/_tests/_setup/suite.ts b/packages/addons/_tests/_setup/suite.ts index eadee8f55..9eb500b5b 100644 --- a/packages/addons/_tests/_setup/suite.ts +++ b/packages/addons/_tests/_setup/suite.ts @@ -98,7 +98,7 @@ export function setupTest( options: kind.options, packageManager: 'pnpm' }); - addPnpmBuildDependencies(cwd, 'pnpm', ['esbuild', ...pnpmBuildDependencies]); + await addPnpmBuildDependencies(cwd, 'pnpm', ['esbuild', ...pnpmBuildDependencies]); } execSync('pnpm install', { cwd: path.resolve(cwd, testName), stdio: 'pipe' }); diff --git a/packages/addons/tailwindcss/index.ts b/packages/addons/tailwindcss/index.ts index 022d8d815..40a84bbfe 100644 --- a/packages/addons/tailwindcss/index.ts +++ b/packages/addons/tailwindcss/index.ts @@ -37,6 +37,7 @@ export default defineAddon({ sv.devDependency('tailwindcss', '^4.0.0'); sv.devDependency('@tailwindcss/vite', '^4.0.0'); + sv.pnpmBuildDependency('@tailwindcss/oxide'); if (prettierInstalled) sv.devDependency('prettier-plugin-tailwindcss', '^0.6.11'); diff --git a/packages/cli/commands/add/index.ts b/packages/cli/commands/add/index.ts index f5945575d..2223250c3 100644 --- a/packages/cli/commands/add/index.ts +++ b/packages/cli/commands/add/index.ts @@ -559,7 +559,7 @@ export async function runAddCommand( if (packageManager) { workspace.packageManager = packageManager; - addPnpmBuildDependencies(workspace.cwd, packageManager, [ + await addPnpmBuildDependencies(workspace.cwd, packageManager, [ 'esbuild', ...addonPnpmBuildDependencies ]); diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts index 386c801fd..acd8112f9 100644 --- a/packages/cli/commands/create.ts +++ b/packages/cli/commands/create.ts @@ -204,7 +204,7 @@ async function createProject(cwd: ProjectPath, options: Options) { const installDeps = async (install: true | AgentName) => { packageManager = install === true ? await packageManagerPrompt(projectPath) : install; - addPnpmBuildDependencies(projectPath, packageManager, ['esbuild']); + await addPnpmBuildDependencies(projectPath, packageManager, ['esbuild']); if (packageManager) await installDependencies(packageManager, projectPath); }; diff --git a/packages/cli/lib/testing.ts b/packages/cli/lib/testing.ts index 998e95424..b4c291446 100644 --- a/packages/cli/lib/testing.ts +++ b/packages/cli/lib/testing.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; import degit from 'degit'; -import { exec } from 'tinyexec'; +import { x, exec } from 'tinyexec'; import { create } from '@sveltejs/create'; export { addPnpmBuildDependencies } from '../utils/package-manager.ts'; @@ -83,7 +83,7 @@ export function createProject({ cwd, testName, templatesDir }: CreateOptions): C type PreviewOptions = { cwd: string; command?: string }; export async function startPreview({ cwd, - command = 'npm run preview' + command = 'pnpm preview' }: PreviewOptions): Promise<{ url: string; close: () => Promise }> { const [cmd, ...args] = command.split(' '); const proc = exec(cmd, args, { @@ -124,7 +124,7 @@ export async function startPreview({ async function terminate(pid: number) { try { if (process.platform === 'win32') { - await exec(`taskkill /pid ${pid} /T /F`); // on windows, use taskkill to terminate the process tree + await x('taskkill', ['/PID', `${pid}`, '/T', '/F']); // on windows, use taskkill to terminate the process tree } else { process.kill(-pid, 'SIGTERM'); // Kill the process group } diff --git a/packages/cli/utils/package-manager.ts b/packages/cli/utils/package-manager.ts index f53e48784..2c534c59e 100644 --- a/packages/cli/utils/package-manager.ts +++ b/packages/cli/utils/package-manager.ts @@ -2,7 +2,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 { x } from 'tinyexec'; import { Option } from 'commander'; import * as p from '@clack/prompts'; import { @@ -12,7 +12,8 @@ import { detect, type AgentName } from 'package-manager-detector'; -import { parseJson } from '@sveltejs/cli-core/parsers'; +import { parseJson, parseYaml } from '@sveltejs/cli-core/parsers'; +import { isVersionUnsupportedBelow } from '@sveltejs/cli-core'; export const AGENT_NAMES = AGENTS.filter((agent): agent is AgentName => !agent.includes('@')); const agentOptions: PackageManagerOptions = AGENT_NAMES.map((pm) => ({ value: pm, label: pm })); @@ -55,19 +56,16 @@ export async function installDependencies(agent: AgentName, cwd: string): Promis try { const { command, args } = constructCommand(COMMANDS[agent].install, [])!; - const proc = exec(command, args, { + + const proc = x(command, args, { nodeOptions: { cwd, stdio: 'pipe' }, throwOnError: true }); - proc.process?.stdout?.on('data', (data) => { - task.message(data.toString(), { raw: true }); - }); - proc.process?.stderr?.on('data', (data) => { - task.message(data.toString(), { raw: true }); - }); - - await proc; + for await (const line of proc) { + // line will be from stderr/stdout in the order you'd see it in a term + task.message(line, { raw: true }); + } task.success('Successfully installed dependencies'); } catch { @@ -87,35 +85,69 @@ export function getUserAgent(): AgentName | undefined { return AGENTS.includes(name) ? name : undefined; } -export function addPnpmBuildDependencies( +export async function addPnpmBuildDependencies( cwd: string, packageManager: AgentName | null | undefined, allowedPackages: string[] ) { // other package managers are currently not affected by this change - if (!packageManager || packageManager !== 'pnpm') return; + 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 pnpmWorkspacePath = find.up('pnpm-workspace.yaml', { cwd }); - let packageDirectory; - - if (pnpmWorkspacePath) packageDirectory = path.dirname(pnpmWorkspacePath); - else packageDirectory = cwd; - - // load the package.json - const pkgPath = path.join(packageDirectory, '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); + const found = find.up('pnpm-workspace.yaml', { cwd }); + const dir = found ? path.dirname(found) : cwd; + + if (confIn === 'pnpm-workspace.yaml') { + const content = found ? fs.readFileSync(found, 'utf-8') : ''; + const { data, generateCode } = parseYaml(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', new Set(items)); + + const newContent = generateCode(); + + const pnpmWorkspacePath = found ?? path.join(cwd, 'pnpm-workspace.yaml'); + if (newContent !== content) fs.writeFileSync(pnpmWorkspacePath, newContent); + } else { + // else is package.json (fallback) + const pkgPath = path.join(dir, '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(); + if (newContent !== content) fs.writeFileSync(pkgPath, newContent); } +} - // save the updated package.json - const newContent = generateCode(); - fs.writeFileSync(pkgPath, newContent); +async function getPnpmVersion(): Promise { + let v: string | undefined = undefined; + try { + const proc = await x('pnpm', ['--version'], { throwOnError: true }); + v = proc.stdout.trim(); + } catch {} + return v; } diff --git a/packages/core/package.json b/packages/core/package.json index 8897e9904..4d9009beb 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -57,6 +57,7 @@ "picocolors": "^1.1.1", "postcss": "^8.5.6", "silver-fleece": "^1.2.1", + "yaml": "^2.8.1", "zimmerframe": "^1.1.2" }, "keywords": [ diff --git a/packages/core/tests/utils.ts b/packages/core/tests/utils.ts index 37979d0be..01139fbc4 100644 --- a/packages/core/tests/utils.ts +++ b/packages/core/tests/utils.ts @@ -1,11 +1,13 @@ -import { expect, test } from 'vitest'; +import { describe, expect, test } from 'vitest'; import dedent from 'dedent'; import { parseScript, serializeScript, guessIndentString, guessQuoteStyle, - type AstTypes + type AstTypes, + serializeYaml, + parseYaml } from '../tooling/index.ts'; test('guessIndentString - one tab', () => { @@ -183,3 +185,87 @@ test('integration - preserves comments', () => { let foo = 'bar';" `); }); + +describe('yaml', () => { + test('read and write', () => { + const input = dedent`foo: + - bar + - baz`; + const output = serializeYaml(parseYaml(input)); + expect(output).toMatchInlineSnapshot(` + "foo: + - bar + - baz + " + `); + }); + + test('edit object', () => { + const input = dedent`foo: + # nice comment + - bar + - baz`; + const doc = parseYaml(input); + const foo = doc.get('foo'); + if (foo) foo.add('yop'); + else doc.set('foo', ['yop']); + expect(serializeYaml(doc)).toMatchInlineSnapshot(` + "foo: + # nice comment + - bar + - baz + - yop + " + `); + }); + + test('add to array (keeping comments)', () => { + const input = dedent`foo: + - bar + # com + - baz`; + const doc = parseYaml(input); + const toAdd = ['bar', 'yop1', 'yop2', 'yop1']; + const foo = doc.get('foo'); + const items: Array<{ value: string } | string> = foo?.items ?? []; + for (const item of toAdd) { + if (items.includes(item)) continue; + if (items.some((y) => typeof y === 'object' && y.value === item)) continue; + items.push(item); + } + doc.set('foo', new Set(items)); + expect(serializeYaml(doc)).toMatchInlineSnapshot(` + "foo: + - bar + # com + - baz + - yop1 + - yop2 + " + `); + }); + + test('create object', () => { + const input = dedent`# this is my file`; + const doc = parseYaml(input); + const foo = doc.get('foo'); + if (foo) foo.add('yop'); + else doc.set('foo', ['yop']); + expect(serializeYaml(doc)).toMatchInlineSnapshot(` + "# this is my file + + foo: + - yop + " + `); + }); + + test('array of foo', () => { + const input = dedent`foo: # nice comment - bar - baz`; + const output = serializeYaml(parseYaml(input)); + expect(output).toMatchInlineSnapshot(` + "foo: # nice comment - bar - baz + " + `); + }); +}); diff --git a/packages/core/tooling/index.ts b/packages/core/tooling/index.ts index ac42aa48f..b2fbe43f2 100644 --- a/packages/core/tooling/index.ts +++ b/packages/core/tooling/index.ts @@ -16,6 +16,7 @@ import * as fleece from 'silver-fleece'; import { print as esrapPrint } from 'esrap'; import * as acorn from 'acorn'; import { tsPlugin } from '@sveltejs/acorn-typescript'; +import * as yaml from 'yaml'; export { // html @@ -238,3 +239,11 @@ export function guessQuoteStyle(ast: TsEstree.Node): 'single' | 'double' | undef return singleCount > doubleCount ? 'single' : 'double'; } + +export function parseYaml(content: string): ReturnType { + return yaml.parseDocument(content); +} + +export function serializeYaml(data: unknown): string { + return yaml.stringify(data, { singleQuote: true }); +} diff --git a/packages/core/tooling/parsers.ts b/packages/core/tooling/parsers.ts index f7764d34f..21b6b0993 100644 --- a/packages/core/tooling/parsers.ts +++ b/packages/core/tooling/parsers.ts @@ -35,6 +35,16 @@ export function parseJson(source: string): { data: any } & ParseBase { return { data, source, generateCode }; } +export function parseYaml( + source: string +): { data: ReturnType } & ParseBase { + if (!source) source = ''; + const data = utils.parseYaml(source); + const generateCode = () => utils.serializeYaml(data); + + return { data, source, generateCode }; +} + type SvelteGenerator = (code: { script?: string; module?: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30594fb70..f62b6a131 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,7 +67,7 @@ importers: version: 0.8.3(rollup@4.46.2)(typescript@5.8.3) vitest: specifier: 4.0.0-beta.6 - version: 4.0.0-beta.6(@types/node@22.15.32)(@vitest/ui@4.0.0-beta.6) + version: 4.0.0-beta.6(@types/node@22.15.32)(@vitest/ui@4.0.0-beta.6)(yaml@2.8.1) community-addon-template: dependencies: @@ -83,7 +83,7 @@ importers: version: link:../packages/cli vitest: specifier: 4.0.0-beta.6 - version: 4.0.0-beta.6(@types/node@22.15.32)(@vitest/ui@4.0.0-beta.6) + version: 4.0.0-beta.6(@types/node@22.15.32)(@vitest/ui@4.0.0-beta.6)(yaml@2.8.1) packages/addons: dependencies: @@ -181,6 +181,9 @@ importers: silver-fleece: specifier: ^1.2.1 version: 1.2.1 + yaml: + specifier: ^2.8.1 + version: 2.8.1 zimmerframe: specifier: ^1.1.2 version: 1.1.2 @@ -2196,6 +2199,11 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2904,13 +2912,13 @@ snapshots: chai: 5.2.1 tinyrainbow: 2.0.0 - '@vitest/mocker@4.0.0-beta.6(vite@7.0.6(@types/node@22.15.32))': + '@vitest/mocker@4.0.0-beta.6(vite@7.0.6(@types/node@22.15.32)(yaml@2.8.1))': dependencies: '@vitest/spy': 4.0.0-beta.6 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 7.0.6(@types/node@22.15.32) + vite: 7.0.6(@types/node@22.15.32)(yaml@2.8.1) '@vitest/pretty-format@4.0.0-beta.6': dependencies: @@ -2939,7 +2947,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 4.0.0-beta.6(@types/node@22.15.32)(@vitest/ui@4.0.0-beta.6) + vitest: 4.0.0-beta.6(@types/node@22.15.32)(@vitest/ui@4.0.0-beta.6)(yaml@2.8.1) '@vitest/utils@4.0.0-beta.6': dependencies: @@ -4010,7 +4018,7 @@ snapshots: optionalDependencies: typescript: 5.8.3 - vite@7.0.6(@types/node@22.15.32): + vite@7.0.6(@types/node@22.15.32)(yaml@2.8.1): dependencies: esbuild: 0.25.8 fdir: 6.4.6(picomatch@4.0.3) @@ -4021,12 +4029,13 @@ snapshots: optionalDependencies: '@types/node': 22.15.32 fsevents: 2.3.3 + yaml: 2.8.1 - vitest@4.0.0-beta.6(@types/node@22.15.32)(@vitest/ui@4.0.0-beta.6): + vitest@4.0.0-beta.6(@types/node@22.15.32)(@vitest/ui@4.0.0-beta.6)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 4.0.0-beta.6 - '@vitest/mocker': 4.0.0-beta.6(vite@7.0.6(@types/node@22.15.32)) + '@vitest/mocker': 4.0.0-beta.6(vite@7.0.6(@types/node@22.15.32)(yaml@2.8.1)) '@vitest/pretty-format': 4.0.0-beta.6 '@vitest/runner': 4.0.0-beta.6 '@vitest/snapshot': 4.0.0-beta.6 @@ -4045,7 +4054,7 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.0.6(@types/node@22.15.32) + vite: 7.0.6(@types/node@22.15.32)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.15.32 @@ -4098,6 +4107,8 @@ snapshots: yaml@1.10.2: {} + yaml@2.8.1: {} + yocto-queue@0.1.0: {} zimmerframe@1.1.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 86c4b35e7..6e0eb8cb7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,6 @@ packages: - 'packages/*' - 'community-addon-template' - '!.test-tmp/**' + +onlyBuiltDependencies: + - esbuild