diff --git a/.changeset/ready-parrots-dress.md b/.changeset/ready-parrots-dress.md new file mode 100644 index 000000000..3683f0cfd --- /dev/null +++ b/.changeset/ready-parrots-dress.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/sv-utils": minor +--- + +breaking(pnpm): `pnpm.allowBuilds` now requires passing in `cwd` diff --git a/.changeset/swift-camels-crash.md b/.changeset/swift-camels-crash.md new file mode 100644 index 000000000..aabf2d56e --- /dev/null +++ b/.changeset/swift-camels-crash.md @@ -0,0 +1,5 @@ +--- +"sv": patch +--- + +feat(cli): Add `@sveltejs/enhanced-img` as a new official addon diff --git a/packages/sv-utils/api-surface.md b/packages/sv-utils/api-surface.md index 800791e10..99840074d 100644 --- a/packages/sv-utils/api-surface.md +++ b/packages/sv-utils/api-surface.md @@ -827,7 +827,7 @@ declare namespace pnpm_d_exports { export { allowBuilds }; } -declare function allowBuilds(...packages: string[]): TransformFn; +declare function allowBuilds(cwd: string, ...packages: [string, ...string[]]): TransformFn; type Version = { major?: number; minor?: number; diff --git a/packages/sv-utils/src/pnpm-internals.ts b/packages/sv-utils/src/pnpm-internals.ts index a36f3697c..10dfc2fb1 100644 --- a/packages/sv-utils/src/pnpm-internals.ts +++ b/packages/sv-utils/src/pnpm-internals.ts @@ -1,9 +1,10 @@ import { execSync } from 'node:child_process'; import { coerceVersion } from './semver.ts'; -export function detectPnpmMajor(): number | undefined { +export function detectPnpmMajor(cwd: string): number | undefined { try { const out = execSync('pnpm --version', { + cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }); diff --git a/packages/sv-utils/src/pnpm.ts b/packages/sv-utils/src/pnpm.ts index cd5e6566f..7e57bb6e5 100644 --- a/packages/sv-utils/src/pnpm.ts +++ b/packages/sv-utils/src/pnpm.ts @@ -32,8 +32,8 @@ type YamlDoc = { * } * ``` */ -export function allowBuilds(...packages: string[]): TransformFn { - const major = detectPnpmMajor(); +export function allowBuilds(cwd: string, ...packages: [string, ...string[]]): TransformFn { + const major = detectPnpmMajor(cwd); if (major !== undefined && major < 11) return writeLegacy(packages); return writeAllowBuilds(packages); } diff --git a/packages/sv-utils/src/tests/pnpm.ts b/packages/sv-utils/src/tests/pnpm.ts index e6d29d70e..ab60b8fac 100644 --- a/packages/sv-utils/src/tests/pnpm.ts +++ b/packages/sv-utils/src/tests/pnpm.ts @@ -1,13 +1,14 @@ +import process from 'node:process'; import { describe, expect, it } from 'vitest'; import { detectPnpmMajor } from '../pnpm-internals.ts'; import { allowBuilds } from '../pnpm.ts'; -const major = detectPnpmMajor(); +const major = detectPnpmMajor(process.cwd()); const isPnpm11 = major === undefined || major >= 11; describe.runIf(isPnpm11)('allowBuilds (pnpm >= 11: writes allowBuilds map)', () => { it('creates allowBuilds map in empty file', () => { - expect(allowBuilds('esbuild')('')).toBe('allowBuilds:\n esbuild: true\n'); + expect(allowBuilds(process.cwd(), 'esbuild')('')).toBe('allowBuilds:\n esbuild: true\n'); }); it('appends to existing allowBuilds map', () => { @@ -16,7 +17,7 @@ describe.runIf(isPnpm11)('allowBuilds (pnpm >= 11: writes allowBuilds map)', () allowBuilds: bar: true `; - expect(allowBuilds('esbuild')(input)).toBe(`packages: + expect(allowBuilds(process.cwd(), 'esbuild')(input)).toBe(`packages: - 'packages/*' allowBuilds: bar: true @@ -28,7 +29,7 @@ allowBuilds: const input = `allowBuilds: core-js: false `; - expect(allowBuilds('esbuild')(input)).toBe(`allowBuilds: + expect(allowBuilds(process.cwd(), 'esbuild')(input)).toBe(`allowBuilds: core-js: false esbuild: true `); @@ -41,7 +42,7 @@ onlyBuiltDependencies: - foo - bar `; - expect(allowBuilds('esbuild')(input)).toBe(`packages: + expect(allowBuilds(process.cwd(), 'esbuild')(input)).toBe(`packages: - 'packages/*' allowBuilds: foo: true @@ -56,7 +57,7 @@ allowBuilds: allowBuilds: shared: false `; - expect(allowBuilds('newone')(input)).toBe(`allowBuilds: + expect(allowBuilds(process.cwd(), 'newone')(input)).toBe(`allowBuilds: shared: false newone: true `); @@ -66,20 +67,20 @@ allowBuilds: const input = `allowBuilds: esbuild: true `; - expect(allowBuilds('esbuild')(input)).toBe(input); + expect(allowBuilds(process.cwd(), 'esbuild')(input)).toBe(input); }); }); describe.runIf(!isPnpm11)('allowBuilds (pnpm < 11: writes onlyBuiltDependencies list)', () => { it('creates onlyBuiltDependencies list in empty file', () => { - expect(allowBuilds('esbuild')('')).toBe('onlyBuiltDependencies:\n - esbuild\n'); + expect(allowBuilds(process.cwd(), 'esbuild')('')).toBe('onlyBuiltDependencies:\n - esbuild\n'); }); it('appends to existing onlyBuiltDependencies list', () => { const input = `onlyBuiltDependencies: - foo `; - expect(allowBuilds('esbuild')(input)).toBe(`onlyBuiltDependencies: + expect(allowBuilds(process.cwd(), 'esbuild')(input)).toBe(`onlyBuiltDependencies: - foo - esbuild `); @@ -89,6 +90,6 @@ describe.runIf(!isPnpm11)('allowBuilds (pnpm < 11: writes onlyBuiltDependencies const input = `onlyBuiltDependencies: - esbuild `; - expect(allowBuilds('esbuild')(input)).toBe(input); + expect(allowBuilds(process.cwd(), 'esbuild')(input)).toBe(input); }); }); diff --git a/packages/sv/api-surface-testing.md b/packages/sv/api-surface-testing.md index af04cbc6d..17f287977 100644 --- a/packages/sv/api-surface-testing.md +++ b/packages/sv/api-surface-testing.md @@ -9,6 +9,7 @@ type OfficialAddons = { vitest: Addon; playwright: Addon; tailwindcss: Addon; + enhancedImg: Addon; sveltekitAdapter: Addon; drizzle: Addon; betterAuth: Addon; diff --git a/packages/sv/api-surface.md b/packages/sv/api-surface.md index ba211be8c..a96508649 100644 --- a/packages/sv/api-surface.md +++ b/packages/sv/api-surface.md @@ -20,6 +20,7 @@ type OfficialAddons = { vitest: Addon; playwright: Addon; tailwindcss: Addon; + enhancedImg: Addon; sveltekitAdapter: Addon; drizzle: Addon; betterAuth: Addon; diff --git a/packages/sv/src/addons/enhanced-img.ts b/packages/sv/src/addons/enhanced-img.ts new file mode 100644 index 000000000..c263ea7e2 --- /dev/null +++ b/packages/sv/src/addons/enhanced-img.ts @@ -0,0 +1,31 @@ +import { color, pnpm, transforms } from '@sveltejs/sv-utils'; +import { defineAddon } from '../core/config.ts'; + +export default defineAddon({ + id: 'enhanced-img', + shortDescription: 'image optimization', + homepage: 'https://svelte.dev/docs/kit/images', + options: {}, + run: ({ sv, file, packageManager, cwd }) => { + sv.devDependency('@sveltejs/enhanced-img', '^0.11.0'); + + if (packageManager === 'pnpm') { + sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.allowBuilds(cwd, 'sharp')); + } + + sv.file( + file.viteConfig, + transforms.script(({ ast, js }) => { + js.imports.addNamed(ast, { + imports: ['enhancedImages'], + from: '@sveltejs/enhanced-img' + }); + js.vite.addPlugin(ast, { code: 'enhancedImages()', mode: 'prepend' }); + }) + ); + }, + nextSteps: () => [ + `Replace ${color.command('``')} with ${color.command('``')} for optimized images`, + `Docs: ${color.website('https://svelte.dev/docs/kit/images')}` + ] +}); diff --git a/packages/sv/src/addons/index.ts b/packages/sv/src/addons/index.ts index 6deb54db4..707b7bfa4 100644 --- a/packages/sv/src/addons/index.ts +++ b/packages/sv/src/addons/index.ts @@ -2,6 +2,7 @@ import type { Addon, AddonDefinition } from '../core/config.ts'; import aiTools from './ai-tools.ts'; import betterAuth from './better-auth.ts'; import drizzle from './drizzle.ts'; +import enhancedImg from './enhanced-img.ts'; import eslint from './eslint.ts'; import experimental from './experimental.ts'; import mdsvex from './mdsvex.ts'; @@ -19,6 +20,7 @@ type OfficialAddons = { vitest: Addon; playwright: Addon; tailwindcss: Addon; + enhancedImg: Addon; sveltekitAdapter: Addon; drizzle: Addon; betterAuth: Addon; @@ -37,6 +39,7 @@ export const officialAddons: OfficialAddons = { vitest, playwright, tailwindcss, + enhancedImg, sveltekitAdapter, drizzle, betterAuth, diff --git a/packages/sv/src/addons/sveltekit-adapter.ts b/packages/sv/src/addons/sveltekit-adapter.ts index 76e4d8589..4d6289171 100644 --- a/packages/sv/src/addons/sveltekit-adapter.ts +++ b/packages/sv/src/addons/sveltekit-adapter.ts @@ -113,7 +113,7 @@ export default defineAddon({ sv.devDependency('wrangler', '^4.97.0'); if (packageManager === 'pnpm') { - sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.allowBuilds('workerd')); + sv.file(file.findUp('pnpm-workspace.yaml'), pnpm.allowBuilds(cwd, 'workerd')); } // default to jsonc diff --git a/packages/sv/src/addons/tests/enhanced-img/test.ts b/packages/sv/src/addons/tests/enhanced-img/test.ts new file mode 100644 index 000000000..d4eedfaa1 --- /dev/null +++ b/packages/sv/src/addons/tests/enhanced-img/test.ts @@ -0,0 +1,30 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import enhancedImg from '../../enhanced-img.ts'; +import { setupTest } from '../_setup/suite.ts'; + +const { test, testCases } = setupTest( + { enhancedImg }, + { kinds: [{ type: 'default', options: { 'enhanced-img': {} } }], browser: false } +); + +test.concurrent.for(testCases)('enhanced-img $variant', (testCase, { expect, ...ctx }) => { + const cwd = ctx.cwd(testCase); + + const config = ['vite.config.ts', 'vite.config.js'] + .map((name) => join(cwd, name)) + .find((file) => existsSync(file))!; + const source = readFileSync(config, 'utf8'); + + expect(source).toMatch(`from '@sveltejs/enhanced-img'`); + expect(source).toMatch('enhancedImages()'); + + const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8')); + expect(pkg.devDependencies).toHaveProperty('@sveltejs/enhanced-img'); + + const workspacePath = join(cwd, 'pnpm-workspace.yaml'); + if (existsSync(workspacePath)) { + const yaml = readFileSync(workspacePath, 'utf8'); + expect(yaml).toMatch('sharp'); + } +}); diff --git a/packages/sv/src/cli/add.ts b/packages/sv/src/cli/add.ts index f76162ee9..081a33c67 100644 --- a/packages/sv/src/cli/add.ts +++ b/packages/sv/src/cli/add.ts @@ -774,7 +774,7 @@ export async function runAddonsApply({ packageManager = options.install; } - addPnpmAllowBuilds(workspace.cwd, packageManager, 'esbuild'); + if (packageManager === 'pnpm') addPnpmAllowBuilds(workspace.cwd, 'esbuild'); const argsFormattedAddons: string[] = []; for (const loaded of successfulAddons) { diff --git a/packages/sv/src/cli/create.ts b/packages/sv/src/cli/create.ts index a53e97fb6..fa0ab5dbe 100644 --- a/packages/sv/src/cli/create.ts +++ b/packages/sv/src/cli/create.ts @@ -5,8 +5,14 @@ import * as p from '@clack/prompts'; import { color, loadPackageJson, resolveCommandArray } from '@sveltejs/sv-utils'; import { Command, Option } from 'commander'; import * as v from 'valibot'; +import { getAddonDetails } from '../addons/index.ts'; import * as common from '../core/common.ts'; -import type { LoadedAddon, OptionValues, SetupResult } from '../core/config.ts'; +import { + createLoadedAddon, + type LoadedAddon, + type OptionValues, + type SetupResult +} from '../core/config.ts'; import { formatFiles } from '../core/formatFiles.ts'; import { AGENT_NAMES, @@ -329,6 +335,12 @@ export async function createProject(cwd: ProjectPath, options: Options) { answers = result.answers; } + if (template === 'demo' && !loadedAddons.some((a) => a.addon.id === 'enhanced-img')) { + const addon = getAddonDetails('enhanced-img'); + loadedAddons.push(createLoadedAddon(addon)); + answers['enhanced-img'] = {}; + } + createKit({ cwd: projectPath, name: projectName, @@ -403,7 +415,8 @@ export async function createProject(cwd: ProjectPath, options: Options) { const addOnNextSteps = getNextSteps(addOnSuccessfulAddons, workspace, answers, addonSetupResults); - addPnpmAllowBuilds(projectPath, packageManager, 'esbuild'); + if (packageManager === 'pnpm') addPnpmAllowBuilds(projectPath, 'esbuild'); + let depsInstalled = false; if (packageManager) { depsInstalled = await installDependencies(packageManager, projectPath); diff --git a/packages/sv/src/core/package-manager.ts b/packages/sv/src/core/package-manager.ts index aaa02837c..62b440850 100644 --- a/packages/sv/src/core/package-manager.ts +++ b/packages/sv/src/core/package-manager.ts @@ -125,16 +125,10 @@ function isInstalled(agent: AgentName): boolean { return installed; } -export function addPnpmAllowBuilds( - cwd: string, - packageManager: AgentName | null | undefined, - ...packages: string[] -): void { - if (packageManager !== 'pnpm' || packages.length === 0) return; - +export function addPnpmAllowBuilds(cwd: string, ...packages: [string, ...string[]]): void { const found = find.up('pnpm-workspace.yaml', { cwd }); const filePath = found ?? path.join(cwd, 'pnpm-workspace.yaml'); const content = found ? fs.readFileSync(found, 'utf-8') : ''; - const newContent = pnpm.allowBuilds(...packages)(content); + const newContent = pnpm.allowBuilds(cwd, ...packages)(content); if (newContent && newContent !== content) fs.writeFileSync(filePath, newContent, 'utf-8'); } diff --git a/packages/sv/src/create/templates/demo/src/lib/images/svelte-welcome.webp b/packages/sv/src/create/templates/demo/src/lib/images/svelte-welcome.webp deleted file mode 100644 index 6ec1a28d6..000000000 Binary files a/packages/sv/src/create/templates/demo/src/lib/images/svelte-welcome.webp and /dev/null differ diff --git a/packages/sv/src/create/templates/demo/src/routes/+page.svelte b/packages/sv/src/create/templates/demo/src/routes/+page.svelte index 71b168fc9..3632c279b 100644 --- a/packages/sv/src/create/templates/demo/src/routes/+page.svelte +++ b/packages/sv/src/create/templates/demo/src/routes/+page.svelte @@ -1,6 +1,4 @@ @@ -12,10 +10,7 @@

- - - Welcome - + to your new
SvelteKit app @@ -49,7 +44,7 @@ padding: 0 0 calc(100% * 495 / 2048) 0; } - .welcome img { + .welcome enhanced\:img { position: absolute; width: 100%; height: 100%; diff --git a/packages/sv/src/create/tests/create.ts b/packages/sv/src/create/tests/create.ts index 104d2689f..6a49daae6 100644 --- a/packages/sv/src/create/tests/create.ts +++ b/packages/sv/src/create/tests/create.ts @@ -3,9 +3,8 @@ import path from 'node:path'; import * as find from 'empathic/find'; import { exec, type Result } from 'tinyexec'; import { beforeAll, describe, expect, test } from 'vitest'; -import { add, officialAddons } from '../../../../sv/src/index.ts'; import { createProject } from '../../cli/create.ts'; -import { type LanguageType, type TemplateType, create } from '../index.ts'; +import { type LanguageType, type TemplateType } from '../index.ts'; const ROOT = path.dirname(find.up('pnpm-workspace.yaml', { cwd: import.meta.dirname })!); const TEMPLATES_DIR = path.resolve(ROOT, 'packages', 'sv', 'src', 'create', 'templates'); @@ -65,8 +64,16 @@ for (const template of templates.filter((t) => t !== 'addon')) { }); }); } else { - create({ cwd, name: `create-svelte-test-${template}-${types}`, template, types }); - await add({ cwd, addons: { eslint: officialAddons.eslint }, options: { eslint: {} } }); + await createProject(cwd, { + types, + addOns: true, + add: ['eslint'], + install: false, + template, + fromPlayground: undefined, + dirCheck: false, + downloadCheck: false + }); } const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf-8')); diff --git a/packages/sv/src/testing.ts b/packages/sv/src/testing.ts index 430ce7463..8ddf997e7 100644 --- a/packages/sv/src/testing.ts +++ b/packages/sv/src/testing.ts @@ -361,7 +361,7 @@ export function createSetupTest( options: kind.options, packageManager: 'pnpm' }); - addPnpmAllowBuilds(cwd, 'pnpm', 'esbuild'); + addPnpmAllowBuilds(cwd, 'esbuild'); } const installDir = path.resolve(cwd, testName);