From aafd1c2d4e775214538890fe49ca5c4a75123cdc Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 21 Mar 2026 18:49:00 +0100 Subject: [PATCH 01/22] add `transforms` API to sv-utils Typed, parser-aware `string -> string` functions that wrap parse -> callback(ast/data) -> generateCode(). Includes transforms for script, svelte, css, json, yaml, toml, html, and text. The engine detects transforms via `isTransform()` and injects workspace context (language) automatically. --- packages/sv-utils/src/index.ts | 14 +- packages/sv-utils/src/tooling/transforms.ts | 200 ++++++++++++++++++++ packages/sv/src/core/config.ts | 12 +- packages/sv/src/core/engine.ts | 10 +- 4 files changed, 229 insertions(+), 7 deletions(-) create mode 100644 packages/sv-utils/src/tooling/transforms.ts diff --git a/packages/sv-utils/src/index.ts b/packages/sv-utils/src/index.ts index 13e736a88..7829d7ac1 100644 --- a/packages/sv-utils/src/index.ts +++ b/packages/sv-utils/src/index.ts @@ -28,8 +28,20 @@ export * as text from './tooling/text.ts'; export * as json from './tooling/json.ts'; export * as svelte from './tooling/svelte/index.ts'; +// Transforms — sv-utils = what to do to content, sv = where and when to do it. +export { + transforms, + isTransform, + type TransformFn, + type TransformContext +} from './tooling/transforms.ts'; + /** - * Will help you `parse` code into an `ast` from all supported languages. + * Low-level parsers. Prefer `transforms` for add-on file edits — it picks the + * right parser for you and handles `generateCode()` automatically. + * + * Use `parse` directly when you need error handling around parsing or + * conditional parser selection at runtime. * Then manipulate the `ast` as you want, * and finally `generateCode()` to write it back to the file. * diff --git a/packages/sv-utils/src/tooling/transforms.ts b/packages/sv-utils/src/tooling/transforms.ts new file mode 100644 index 000000000..9a0756d4b --- /dev/null +++ b/packages/sv-utils/src/tooling/transforms.ts @@ -0,0 +1,200 @@ +import type { TomlTable } from 'smol-toml'; +import type { Comments, SvelteAst } from './index.ts'; +import type { TsEstree } from './js/ts-estree.ts'; +import { + parseCss, + parseHtml, + parseJson, + parseScript, + parseSvelte, + parseToml, + parseYaml +} from './parsers.ts'; + +/** + * Context injected by the `sv` engine when running a transform via `sv.file()`. + * Can also be passed manually for standalone usage or testing. + */ +export type TransformContext = { + language: 'ts' | 'js'; +}; + +const TRANSFORM_KEY = '__transform' as const; + +export type TransformType = 'script' | 'css' | 'svelte' | 'json' | 'yaml' | 'toml' | 'text' | 'html'; + +export type TransformFn = { + (content: string, ctx?: TransformContext): string; + [TRANSFORM_KEY]: TransformType; +}; + +export function isTransform( + fn: (content: string, ctx?: TransformContext) => string +): fn is TransformFn { + return TRANSFORM_KEY in fn; +} + +/** + * File transform primitives that know their format. + * + * `sv-utils = what to do to content, sv = where and when to do it.` + * + * Each transform wraps: parse -> callback(ast/data) -> generateCode(). + * The parser choice is baked into the transform type — you can't accidentally + * parse a vite config as svelte because you never call a parser yourself. + * + * @example + * ```ts + * import { transforms } from '@sveltejs/sv-utils'; + * + * // returns a transform function (content: string) => string + * const addPlugin = transforms.script((ast) => { + * js.imports.addDefault(ast, { as: 'foo', from: 'foo' }); + * }); + * + * // use with sv.file() — the engine injects context automatically + * sv.file(files.viteConfig, transforms.script((ast) => { + * js.vite.addPlugin(ast, { code: 'kitRoutes()' }); + * })); + * + * // standalone usage / testing — pass context manually + * const result = addPlugin(fileContent, { language: 'ts' }); + * ``` + */ +export const transforms = { + /** + * Transform a JavaScript/TypeScript file. + * + * Return `false` from the callback to abort — the original content is returned unchanged. + */ + script( + cb: (ast: TsEstree.Program, comments: Comments, ctx: TransformContext) => void | false + ): TransformFn { + const fn = ((content: string, ctx?: TransformContext) => { + const { ast, comments, generateCode } = parseScript(content); + const result = cb(ast, comments, ctx ?? { language: 'ts' }); + if (result === false) return content; + return generateCode(); + }) as TransformFn; + fn[TRANSFORM_KEY] = 'script'; + return fn; + }, + + /** + * Transform a Svelte component file. + * Receives `language` from the engine context automatically. + * + * Return `false` from the callback to abort — the original content is returned unchanged. + */ + svelte(cb: (ast: SvelteAst.Root, ctx: TransformContext) => void | false): TransformFn { + const fn = ((content: string, ctx?: TransformContext) => { + const { ast, generateCode } = parseSvelte(content); + const result = cb(ast, ctx ?? { language: 'ts' }); + if (result === false) return content; + return generateCode(); + }) as TransformFn; + fn[TRANSFORM_KEY] = 'svelte'; + return fn; + }, + + /** + * Transform a CSS file. + * + * Return `false` from the callback to abort — the original content is returned unchanged. + */ + css( + cb: ( + ast: Omit, + ctx: TransformContext + ) => void | false + ): TransformFn { + const fn = ((content: string, ctx?: TransformContext) => { + const { ast, generateCode } = parseCss(content); + const result = cb(ast, ctx ?? { language: 'ts' }); + if (result === false) return content; + return generateCode(); + }) as TransformFn; + fn[TRANSFORM_KEY] = 'css'; + return fn; + }, + + /** + * Transform a JSON file. + * + * Return `false` from the callback to abort — the original content is returned unchanged. + */ + json(cb: (data: T, ctx: TransformContext) => void | false): TransformFn { + const fn = ((content: string, ctx?: TransformContext) => { + const { data, generateCode } = parseJson(content); + const result = cb(data as T, ctx ?? { language: 'ts' }); + if (result === false) return content; + return generateCode(); + }) as TransformFn; + fn[TRANSFORM_KEY] = 'json'; + return fn; + }, + + /** + * Transform a YAML file. + * + * Return `false` from the callback to abort — the original content is returned unchanged. + */ + yaml( + cb: (data: ReturnType['data'], ctx: TransformContext) => void | false + ): TransformFn { + const fn = ((content: string, ctx?: TransformContext) => { + const { data, generateCode } = parseYaml(content); + const result = cb(data, ctx ?? { language: 'ts' }); + if (result === false) return content; + return generateCode(); + }) as TransformFn; + fn[TRANSFORM_KEY] = 'yaml'; + return fn; + }, + + /** + * Transform a TOML file. + * + * Return `false` from the callback to abort — the original content is returned unchanged. + */ + toml(cb: (data: TomlTable, ctx: TransformContext) => void | false): TransformFn { + const fn = ((content: string, ctx?: TransformContext) => { + const { data, generateCode } = parseToml(content); + const result = cb(data, ctx ?? { language: 'ts' }); + if (result === false) return content; + return generateCode(); + }) as TransformFn; + fn[TRANSFORM_KEY] = 'toml'; + return fn; + }, + + /** + * Transform an HTML file (e.g. app.html). + * + * Return `false` from the callback to abort — the original content is returned unchanged. + */ + html( + cb: (ast: SvelteAst.Fragment, ctx: TransformContext) => void | false + ): TransformFn { + const fn = ((content: string, ctx?: TransformContext) => { + const { ast, generateCode } = parseHtml(content); + const result = cb(ast, ctx ?? { language: 'ts' }); + if (result === false) return content; + return generateCode(); + }) as TransformFn; + fn[TRANSFORM_KEY] = 'html'; + return fn; + }, + + /** + * Transform a plain text file (.env, .gitignore, etc.). + * No parsing — just string in, string out. + */ + text(cb: (content: string, ctx: TransformContext) => string): TransformFn { + const fn = ((content: string, ctx?: TransformContext) => { + return cb(content, ctx ?? { language: 'ts' }); + }) as TransformFn; + fn[TRANSFORM_KEY] = 'text'; + return fn; + } +}; diff --git a/packages/sv/src/core/config.ts b/packages/sv/src/core/config.ts index 5ebd7fd0c..1cae858f4 100644 --- a/packages/sv/src/core/config.ts +++ b/packages/sv/src/core/config.ts @@ -1,3 +1,4 @@ +import type { TransformFn } from '@sveltejs/sv-utils'; import type { officialAddons } from '../addons/index.ts'; import type { OptionDefinition, OptionValues, Question } from './options.ts'; import type { Workspace, WorkspaceOptions } from './workspace.ts'; @@ -29,8 +30,15 @@ export type SvApi = { devDependency: (pkg: string, version: string) => void; /** Execute a command in the workspace. */ execute: (args: string[], stdio: 'inherit' | 'pipe') => Promise; - /** Edit a file in the workspace. (will create it if it doesn't exist) */ - file: (path: string, edit: (content: string) => string) => void; + /** Edit a file in the workspace. (will create it if it doesn't exist) + * + * Accepts either a raw edit function or a typed transform from `@sveltejs/sv-utils`. + * When using a transform, the engine automatically injects workspace context (language, etc.). + */ + file: { + (path: string, edit: TransformFn): void; + (path: string, edit: (content: string) => string): void; + }; }; export type Addon = { diff --git a/packages/sv/src/core/engine.ts b/packages/sv/src/core/engine.ts index 0d081a737..672e4f463 100644 --- a/packages/sv/src/core/engine.ts +++ b/packages/sv/src/core/engine.ts @@ -1,5 +1,5 @@ import * as p from '@clack/prompts'; -import { color, resolveCommand, type AgentName } from '@sveltejs/sv-utils'; +import { color, isTransform, resolveCommand, type AgentName } from '@sveltejs/sv-utils'; import { NonZeroExitError, exec } from 'tinyexec'; import { createLoadedAddon } from '../cli/add.ts'; import { @@ -171,12 +171,14 @@ 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, content) => { + file: (path, edit) => { try { const exists = fileExists(workspace.cwd, path); let fileContent = exists ? readFile(workspace.cwd, path) : ''; - // process file - fileContent = content(fileContent); + // process file — inject workspace context for typed transforms + fileContent = isTransform(edit) + ? edit(fileContent, { language: workspace.language }) + : edit(fileContent); if (!fileContent) return fileContent; writeFile(workspace, path, fileContent); From 3e3760cffb8c37f6b66e32b31d2c3527cc486ee6 Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 21 Mar 2026 18:55:16 +0100 Subject: [PATCH 02/22] migrate all addons from `parse` to `transforms` Mechanical migration: every `sv.file()` callback that followed the parse -> mutate -> generateCode pattern now uses the typed transform. `parse` is retained only where transforms don't fit: - prettier.ts: try/catch around JSON parsing - sveltekit-adapter.ts: conditional parser (json vs toml) and standalone parse outside sv.file --- packages/sv/src/addons/better-auth.ts | 49 ++++-------- packages/sv/src/addons/common.ts | 65 +++++++--------- packages/sv/src/addons/devtools-json.ts | 19 +++-- packages/sv/src/addons/drizzle.ts | 36 +++------ packages/sv/src/addons/eslint.ts | 26 ++----- packages/sv/src/addons/mcp.ts | 53 ++++++------- packages/sv/src/addons/mdsvex.ts | 71 +++++++++--------- packages/sv/src/addons/paraglide.ts | 83 +++++++-------------- packages/sv/src/addons/playwright.ts | 20 ++--- packages/sv/src/addons/prettier.ts | 16 ++-- packages/sv/src/addons/sveltekit-adapter.ts | 41 +++------- packages/sv/src/addons/tailwindcss.ts | 57 +++++--------- packages/sv/src/addons/vitest-addon.ts | 18 ++--- 13 files changed, 203 insertions(+), 351 deletions(-) diff --git a/packages/sv/src/addons/better-auth.ts b/packages/sv/src/addons/better-auth.ts index f38dc58f3..18c9fe19b 100644 --- a/packages/sv/src/addons/better-auth.ts +++ b/packages/sv/src/addons/better-auth.ts @@ -7,7 +7,7 @@ import { text, js, json, - parse, + transforms, resolveCommand, createPrinter } from '@sveltejs/sv-utils'; @@ -55,8 +55,7 @@ export default defineAddon({ sv.devDependency('better-auth', '~1.4.21'); sv.devDependency('@better-auth/cli', '~1.4.21'); - sv.file(`drizzle.config.${language}`, (content) => { - const { ast, generateCode } = parse.script(content); + sv.file(`drizzle.config.${language}`, transforms.script((ast) => { const isProp = (name: string, node: AstTypes.Property) => node.key.type === 'Identifier' && node.key.name === name; @@ -83,15 +82,12 @@ export default defineAddon({ if (!drizzleDialect) { throw new Error('Failed to detect DB dialect in your `drizzle.config.[js|ts]` file'); } - return generateCode(); - }); + })); sv.file('.env', (content) => generateEnvFileContent(content, demoGithub, false)); sv.file('.env.example', (content) => generateEnvFileContent(content, demoGithub, true)); - sv.file(`${kit?.libDirectory}/server/auth.${language}`, (content) => { - const { ast, generateCode, comments } = parse.script(content); - + sv.file(`${kit?.libDirectory}/server/auth.${language}`, transforms.script((ast, comments) => { js.imports.addNamed(ast, { from: '$lib/server/db', imports: [d1 ? 'getDb' : 'db'] }); js.imports.addNamed(ast, { from: '$app/server', imports: ['getRequestEvent'] }); js.imports.addNamed(ast, { from: '$env/dynamic/private', imports: ['env'] }); @@ -161,22 +157,18 @@ export default defineAddon({ });`; } js.common.appendFromString(ast, { code: authConfig, comments }); - - return generateCode(); - }); + })); const authConfigPath = `${kit?.libDirectory}/server/auth.${language}`; const authSchemaPath = `${kit?.libDirectory}/server/db/auth.schema.${language}`; - sv.file(files.package, (content) => { - const { data, generateCode } = parse.json(content); + sv.file(files.package, transforms.json((data) => { json.packageScriptsUpsert( data, 'auth:schema', `better-auth generate --config ${authConfigPath} --output ${authSchemaPath} --yes` ); - return generateCode(); - }); + })); sv.file(`${kit?.libDirectory}/server/db/auth.schema.${language}`, (content) => { if (content) return content; @@ -185,17 +177,11 @@ export default defineAddon({ `; }); - sv.file(`${kit?.libDirectory}/server/db/schema.${language}`, (content) => { - const { ast, generateCode } = parse.script(content); - + sv.file(`${kit?.libDirectory}/server/db/schema.${language}`, transforms.script((ast) => { js.exports.addNamespace(ast, { from: './auth.schema' }); + })); - return generateCode(); - }); - - sv.file('src/app.d.ts', (content) => { - const { ast, comments, generateCode } = parse.script(content); - + sv.file('src/app.d.ts', transforms.script((ast, comments) => { if (d1) js.imports.addNamed(ast, { imports: ['createAuth'], from: '$lib/server/auth' }); js.imports.addNamed(ast, { imports: ['User', 'Session'], @@ -232,12 +218,9 @@ export default defineAddon({ js.common.createTypeProperty('auth', 'ReturnType', false) ); } - return generateCode(); - }); - - sv.file(`src/hooks.server.${language}`, (content) => { - const { ast, generateCode, comments } = parse.script(content); + })); + sv.file(`src/hooks.server.${language}`, transforms.script((ast, comments) => { js.imports.addNamed(ast, { imports: ['svelteKitHandler'], from: 'better-auth/svelte-kit' }); js.imports.addNamed(ast, { imports: [d1 ? 'createAuth' : 'auth'], from: '$lib/server/auth' }); js.imports.addNamed(ast, { imports: ['building'], from: '$app/environment' }); @@ -271,14 +254,10 @@ export default defineAddon({ handleContent, comments }); - - return generateCode(); - }); + })); if (hasDemo) { - sv.file(`${kit?.routesDirectory}/demo/+page.svelte`, (content) => { - return addToDemoPage(content, 'better-auth', language); - }); + sv.file(`${kit?.routesDirectory}/demo/+page.svelte`, addToDemoPage('better-auth')); sv.file( `${kit!.routesDirectory}/demo/better-auth/login/+page.server.${language}`, diff --git a/packages/sv/src/addons/common.ts b/packages/sv/src/addons/common.ts index 23605fe11..d39401988 100644 --- a/packages/sv/src/addons/common.ts +++ b/packages/sv/src/addons/common.ts @@ -1,9 +1,7 @@ -import { type SvelteAst, js, parse, svelte } from '@sveltejs/sv-utils'; +import { type SvelteAst, type TransformFn, js, svelte, transforms } from '@sveltejs/sv-utils'; import process from 'node:process'; -export function addEslintConfigPrettier(content: string): string { - const { ast, generateCode } = parse.script(content); - +export const addEslintConfigPrettier = transforms.script((ast) => { // if a default import for `eslint-plugin-svelte` already exists, then we'll use their specifier's name instead const importNodes = ast.body.filter((n) => n.type === 'ImportDeclaration'); const sveltePluginImport = importNodes.find( @@ -28,7 +26,7 @@ export function addEslintConfigPrettier(content: string): string { const defaultExport = js.exports.createDefault(ast, { fallback: fallbackConfig }); const eslintConfig = defaultExport.value; if (eslintConfig.type !== 'ArrayExpression' && eslintConfig.type !== 'CallExpression') - return content; + return false; const prettier = js.common.parseExpression('prettier'); const sveltePrettierConfig = js.common.parseExpression(`${svelteImportName}.configs.prettier`); @@ -57,43 +55,34 @@ export function addEslintConfigPrettier(content: string): string { // append to the end as a fallback elements.push(...nodesToInsert); } - - return generateCode(); -} - -export function addToDemoPage( - existingContent: string, - path: string, - language: 'ts' | 'js' -): string { - const { ast, generateCode } = parse.svelte(existingContent); - - for (const node of ast.fragment.nodes) { - if (node.type === 'RegularElement') { - const hrefAttribute = node.attributes.find( - (x) => x.type === 'Attribute' && x.name === 'href' - ) as SvelteAst.Attribute; - if (!hrefAttribute || !hrefAttribute.value) continue; - - if (!Array.isArray(hrefAttribute.value)) continue; - - const hasDemo = hrefAttribute.value.some( - // we use includes as it could be "/demo/${path}" or "resolve("demo/${path}")" or "resolve('demo/${path}')" - (x) => x.type === 'Text' && x.data.includes(`/demo/${path}`) - ); - if (hasDemo) { - return existingContent; +}); + +export function addToDemoPage(path: string): TransformFn { + return transforms.svelte((ast, { language }) => { + for (const node of ast.fragment.nodes) { + if (node.type === 'RegularElement') { + const hrefAttribute = node.attributes.find( + (x) => x.type === 'Attribute' && x.name === 'href' + ) as SvelteAst.Attribute; + if (!hrefAttribute || !hrefAttribute.value) continue; + + if (!Array.isArray(hrefAttribute.value)) continue; + + const hasDemo = hrefAttribute.value.some( + // we use includes as it could be "/demo/${path}" or "resolve("demo/${path}")" or "resolve('demo/${path}')" + (x) => x.type === 'Text' && x.data.includes(`/demo/${path}`) + ); + if (hasDemo) { + return false; + } } } - } - - svelte.ensureScript(ast, { language }); - js.imports.addNamed(ast.instance.content, { imports: ['resolve'], from: '$app/paths' }); - svelte.addFragment(ast, `${path}`, { mode: 'prepend' }); - ast.fragment.nodes.unshift(); + svelte.ensureScript(ast, { language }); + js.imports.addNamed(ast.instance.content, { imports: ['resolve'], from: '$app/paths' }); - return generateCode(); + svelte.addFragment(ast, `${path}`, { mode: 'prepend' }); + }); } /** diff --git a/packages/sv/src/addons/devtools-json.ts b/packages/sv/src/addons/devtools-json.ts index e4d8dc46a..3772aa5a3 100644 --- a/packages/sv/src/addons/devtools-json.ts +++ b/packages/sv/src/addons/devtools-json.ts @@ -1,4 +1,4 @@ -import { js, parse } from '@sveltejs/sv-utils'; +import { js, transforms } from '@sveltejs/sv-utils'; import { defineAddon } from '../core/config.ts'; export default defineAddon({ @@ -11,14 +11,13 @@ export default defineAddon({ sv.devDependency('vite-plugin-devtools-json', '^1.0.0'); // add the vite plugin - sv.file(files.viteConfig, (content) => { - const { ast, generateCode } = parse.script(content); - - const vitePluginName = 'devtoolsJson'; - js.imports.addDefault(ast, { as: vitePluginName, from: 'vite-plugin-devtools-json' }); - js.vite.addPlugin(ast, { code: `${vitePluginName}()` }); - - return generateCode(); - }); + sv.file( + files.viteConfig, + transforms.script((ast) => { + const vitePluginName = 'devtoolsJson'; + js.imports.addDefault(ast, { as: vitePluginName, from: 'vite-plugin-devtools-json' }); + js.vite.addPlugin(ast, { code: `${vitePluginName}()` }); + }) + ); } }); diff --git a/packages/sv/src/addons/drizzle.ts b/packages/sv/src/addons/drizzle.ts index 9a242e1be..06c9c120b 100644 --- a/packages/sv/src/addons/drizzle.ts +++ b/packages/sv/src/addons/drizzle.ts @@ -1,4 +1,4 @@ -import { color, dedent, text, js, parse, resolveCommand, json } from '@sveltejs/sv-utils'; +import { color, dedent, text, js, transforms, resolveCommand, json } from '@sveltejs/sv-utils'; import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; @@ -186,17 +186,13 @@ export default defineAddon({ }); } - sv.file(files.package, (content) => { - const { data, generateCode } = parse.json(content); - + sv.file(files.package, transforms.json((data) => { if (options.docker) json.packageScriptsUpsert(data, 'db:start', 'docker compose up'); json.packageScriptsUpsert(data, 'db:push', 'drizzle-kit push'); json.packageScriptsUpsert(data, 'db:generate', 'drizzle-kit generate'); json.packageScriptsUpsert(data, 'db:migrate', 'drizzle-kit migrate'); json.packageScriptsUpsert(data, 'db:studio', 'drizzle-kit studio'); - - return generateCode(); - }); + })); const hasPrettier = Boolean(dependencyVersion('prettier')); if (hasPrettier) { @@ -212,12 +208,10 @@ export default defineAddon({ }); } - sv.file(paths['drizzle config'], (content) => { + sv.file(paths['drizzle config'], transforms.script((ast) => { const d1 = options.database === 'd1'; const turso = options.sqlite === 'turso'; - const { ast, generateCode } = parse.script(content); - js.imports.addNamed(ast, { from: 'drizzle-kit', imports: { defineConfig: 'defineConfig' } }); if (d1) { @@ -273,13 +267,9 @@ export default defineAddon({ }) `) }); + })); - return generateCode(); - }); - - sv.file(paths['database schema'], (content) => { - const { ast, generateCode } = parse.script(content); - + sv.file(paths['database schema'], transforms.script((ast) => { let taskSchemaExpression; if (options.database === 'sqlite' || options.database === 'd1') { js.imports.addNamed(ast, { @@ -328,13 +318,9 @@ export default defineAddon({ name: 'task', fallback: taskIdentifier }); + })); - return generateCode(); - }); - - sv.file(paths.database, (content) => { - const { ast, generateCode } = parse.script(content); - + sv.file(paths.database, transforms.script((ast) => { if (options.database === 'd1') { js.imports.addNamespace(ast, { from: './schema', as: 'schema' }); js.imports.addNamed(ast, { from: 'drizzle-orm/d1', imports: ['drizzle'] }); @@ -345,7 +331,7 @@ export default defineAddon({ ast.body.push(getDbFn); - return generateCode(); + return; } js.imports.addNamed(ast, { from: '$env/dynamic/private', imports: ['env'] }); @@ -463,9 +449,7 @@ export default defineAddon({ name: 'db', fallback: db }); - - return generateCode(); - }); + })); }, nextSteps: ({ options, packageManager, cwd }) => { diff --git a/packages/sv/src/addons/eslint.ts b/packages/sv/src/addons/eslint.ts index f57eec50f..358d0fa26 100644 --- a/packages/sv/src/addons/eslint.ts +++ b/packages/sv/src/addons/eslint.ts @@ -1,5 +1,5 @@ import { log } from '@clack/prompts'; -import { type AstTypes, js, parse, json } from '@sveltejs/sv-utils'; +import { type AstTypes, js, transforms, json } from '@sveltejs/sv-utils'; import { defineAddon } from '../core/config.ts'; import { addEslintConfigPrettier, getNodeTypesVersion } from './common.ts'; @@ -23,17 +23,11 @@ export default defineAddon({ if (prettierInstalled) sv.devDependency('eslint-config-prettier', '^10.1.8'); - sv.file(files.package, (content) => { - const { data, generateCode } = parse.json(content); - + sv.file(files.package, transforms.json((data) => { json.packageScriptsUpsert(data, 'lint', 'eslint .'); + })); - return generateCode(); - }); - - sv.file(files.eslintConfig, (content) => { - const { ast, comments, generateCode } = parse.script(content); - + sv.file(files.eslintConfig, transforms.script((ast, comments) => { const eslintConfigs: Array = []; js.imports.addDefault(ast, { from: './svelte.config.js', as: 'svelteConfig' }); const gitIgnorePathStatement = js.common.parseStatement( @@ -123,7 +117,7 @@ export default defineAddon({ // if it's not the config we created, then we'll leave it alone and exit out if (defaultExport !== exportExpression) { log.warn('An eslint config is already defined. Skipping initialization.'); - return content; + return false; } if (typescript) js.imports.addDefault(ast, { from: 'typescript-eslint', as: 'ts' }); @@ -136,15 +130,11 @@ export default defineAddon({ imports: ['includeIgnoreFile'] }); js.imports.addDefault(ast, { from: 'node:path', as: 'path' }); + })); - return generateCode(); - }); - - sv.file(files.vscodeExtensions, (content) => { - const { data, generateCode } = parse.json(content); + sv.file(files.vscodeExtensions, transforms.json((data) => { json.arrayUpsert(data, 'recommendations', 'dbaeumer.vscode-eslint'); - return generateCode(); - }); + })); if (prettierInstalled) { sv.file(files.eslintConfig, addEslintConfigPrettier); diff --git a/packages/sv/src/addons/mcp.ts b/packages/sv/src/addons/mcp.ts index 4d10915d7..04e943c9b 100644 --- a/packages/sv/src/addons/mcp.ts +++ b/packages/sv/src/addons/mcp.ts @@ -1,5 +1,5 @@ import { log } from '@clack/prompts'; -import { color, parse } from '@sveltejs/sv-utils'; +import { color, transforms } from '@sveltejs/sv-utils'; import { defineAddon, defineAddonOptions } from '../core/config.ts'; import { getSharedFiles } from '../create/utils.ts'; @@ -150,37 +150,38 @@ export default defineAddon({ }); } - sv.file(configPath, (content) => { - const { data, generateCode } = parse.json(content); - - if (schema) { - data['$schema'] = schema; - } + sv.file( + configPath, + transforms.json((data) => { + if (schema) { + data['$schema'] = schema; + } - if (customData) { - for (const [key, value] of Object.entries(customData)) { - data[key] = value; + if (customData) { + for (const [key, value] of Object.entries(customData)) { + data[key] = value; + } } - } - if (mcpOptions) { - const key = mcpOptions.serversKey ?? 'mcpServers'; - data[key] ??= {}; - data[key].svelte = - options.setup === 'local' ? getLocalConfig(mcpOptions) : getRemoteConfig(mcpOptions); - } - return generateCode(); - }); + if (mcpOptions) { + const key = mcpOptions.serversKey ?? 'mcpServers'; + data[key] ??= {}; + data[key].svelte = + options.setup === 'local' ? getLocalConfig(mcpOptions) : getRemoteConfig(mcpOptions); + } + }) + ); if (extraFiles) { for (const extra of extraFiles) { - sv.file(extra.path, (content) => { - const { data, generateCode } = parse.json(content); - for (const [key, value] of Object.entries(extra.data)) { - data[key] = value; - } - return generateCode(); - }); + sv.file( + extra.path, + transforms.json((data) => { + for (const [key, value] of Object.entries(extra.data)) { + data[key] = value; + } + }) + ); } } } diff --git a/packages/sv/src/addons/mdsvex.ts b/packages/sv/src/addons/mdsvex.ts index 30583f07a..3b3b5b534 100644 --- a/packages/sv/src/addons/mdsvex.ts +++ b/packages/sv/src/addons/mdsvex.ts @@ -1,4 +1,4 @@ -import { js, parse } from '@sveltejs/sv-utils'; +import { js, transforms } from '@sveltejs/sv-utils'; import { defineAddon } from '../core/config.ts'; export default defineAddon({ @@ -9,43 +9,42 @@ export default defineAddon({ run: ({ sv, files }) => { sv.devDependency('mdsvex', '^0.12.6'); - sv.file(files.svelteConfig, (content) => { - const { ast, generateCode } = parse.script(content); + sv.file( + files.svelteConfig, + transforms.script((ast) => { + js.imports.addNamed(ast, { from: 'mdsvex', imports: ['mdsvex'] }); - js.imports.addNamed(ast, { from: 'mdsvex', imports: ['mdsvex'] }); - - const { value: exportDefault } = js.exports.createDefault(ast, { - fallback: js.object.create({}) - }); - - // preprocess - let preprocessorArray = js.object.property(exportDefault, { - name: 'preprocess', - fallback: js.array.create() - }); - const isArray = preprocessorArray.type === 'ArrayExpression'; - - if (!isArray) { - const previousElement = preprocessorArray; - preprocessorArray = js.array.create(); - js.array.append(preprocessorArray, previousElement); - js.object.overrideProperties(exportDefault, { - preprocess: preprocessorArray + const { value: exportDefault } = js.exports.createDefault(ast, { + fallback: js.object.create({}) }); - } - - const mdsvexCall = js.functions.createCall({ name: 'mdsvex', args: [] }); - js.array.append(preprocessorArray, mdsvexCall); - // extensions - const extensionsArray = js.object.property(exportDefault, { - name: 'extensions', - fallback: js.array.create() - }); - js.array.append(extensionsArray, '.svelte'); - js.array.append(extensionsArray, '.svx'); - - return generateCode(); - }); + // preprocess + let preprocessorArray = js.object.property(exportDefault, { + name: 'preprocess', + fallback: js.array.create() + }); + const isArray = preprocessorArray.type === 'ArrayExpression'; + + if (!isArray) { + const previousElement = preprocessorArray; + preprocessorArray = js.array.create(); + js.array.append(preprocessorArray, previousElement); + js.object.overrideProperties(exportDefault, { + preprocess: preprocessorArray + }); + } + + const mdsvexCall = js.functions.createCall({ name: 'mdsvex', args: [] }); + js.array.append(preprocessorArray, mdsvexCall); + + // extensions + const extensionsArray = js.object.property(exportDefault, { + name: 'extensions', + fallback: js.array.create() + }); + js.array.append(extensionsArray, '.svelte'); + js.array.append(extensionsArray, '.svx'); + }) + ); } }); diff --git a/packages/sv/src/addons/paraglide.ts b/packages/sv/src/addons/paraglide.ts index 6bcde3224..11d1dea3f 100644 --- a/packages/sv/src/addons/paraglide.ts +++ b/packages/sv/src/addons/paraglide.ts @@ -1,5 +1,5 @@ import { log } from '@clack/prompts'; -import { color, html, js, parse, svelte, type SvelteAst, text } from '@sveltejs/sv-utils'; +import { color, html, js, svelte, type SvelteAst, text, transforms } from '@sveltejs/sv-utils'; import { defineAddon, defineAddonOptions } from '../core/config.ts'; import { addToDemoPage } from './common.ts'; @@ -61,24 +61,19 @@ export default defineAddon({ sv.devDependency('@inlang/paraglide-js', '^2.10.0'); // add the vite plugin - sv.file(files.viteConfig, (content) => { - const { ast, generateCode } = parse.script(content); - + sv.file(files.viteConfig, transforms.script((ast) => { const vitePluginName = 'paraglideVitePlugin'; js.imports.addNamed(ast, { imports: [vitePluginName], from: '@inlang/paraglide-js' }); js.vite.addPlugin(ast, { - code: `${vitePluginName}({ - project: './project.inlang', - outdir: './${paraglideOutDir}' + code: `${vitePluginName}({ + project: './project.inlang', + outdir: './${paraglideOutDir}' })` }); - - return generateCode(); - }); + })); // reroute hook - sv.file(`src/hooks.${language}`, (content) => { - const { ast, generateCode } = parse.script(content); + sv.file(`src/hooks.${language}`, transforms.script((ast) => { js.imports.addNamed(ast, { from: '$lib/paraglide/runtime', imports: ['deLocalizeUrl'] @@ -100,13 +95,10 @@ export default defineAddon({ if (existingExport.declaration !== rerouteIdentifier) { log.warn('Adding the reroute hook automatically failed. Add it manually'); } - - return generateCode(); - }); + })); // handle hook - sv.file(`src/hooks.server.${language}`, (content) => { - const { ast, generateCode, comments } = parse.script(content); + sv.file(`src/hooks.server.${language}`, transforms.script((ast, comments) => { js.imports.addNamed(ast, { from: '$lib/paraglide/server', imports: ['paraglideMiddleware'] @@ -128,14 +120,10 @@ export default defineAddon({ handleContent: hookHandleContent, comments }); - - return generateCode(); - }); + })); // add the lang and dir attributes placeholder to app.html - sv.file('src/app.html', (content) => { - const { ast, generateCode } = parse.html(content); - + sv.file('src/app.html', transforms.html((ast) => { const htmlNode = ast.nodes.find( (child): child is SvelteAst.RegularElement => child.type === 'RegularElement' && child.name === 'html' @@ -144,13 +132,11 @@ export default defineAddon({ log.warn( "Could not find node in app.html. You'll need to add the language placeholder manually" ); - return generateCode(); + return; } html.addAttribute(htmlNode, 'lang', '%paraglide.lang%'); html.addAttribute(htmlNode, 'dir', '%paraglide.dir%'); - - return generateCode(); - }); + })); sv.file(files.gitignore, (content) => { if (!content) return content; @@ -164,22 +150,19 @@ export default defineAddon({ sv.file('project.inlang/settings.json', (content) => { if (content) return content; - const { data, generateCode } = parse.json(content); - - for (const key in DEFAULT_INLANG_PROJECT) { - data[key] = DEFAULT_INLANG_PROJECT[key as keyof typeof DEFAULT_INLANG_PROJECT]; - } - const { validLanguageTags } = parseLanguageTagInput(options.languageTags); - const baseLocale = validLanguageTags[0]; - - data.baseLocale = baseLocale; - data.locales = validLanguageTags; + return transforms.json((data) => { + for (const key in DEFAULT_INLANG_PROJECT) { + data[key] = DEFAULT_INLANG_PROJECT[key as keyof typeof DEFAULT_INLANG_PROJECT]; + } + const { validLanguageTags } = parseLanguageTagInput(options.languageTags); + const baseLocale = validLanguageTags[0]; - return generateCode(); + data.baseLocale = baseLocale; + data.locales = validLanguageTags; + })(content); }); - sv.file(`${kit.routesDirectory}/+layout.svelte`, (content) => { - const { ast, generateCode } = parse.svelte(content); + sv.file(`${kit.routesDirectory}/+layout.svelte`, transforms.svelte((ast, { language }) => { svelte.ensureScript(ast, { language }); js.imports.addNamed(ast.instance.content, { imports: ['locales', 'localizeHref'], @@ -194,17 +177,13 @@ export default defineAddon({ {/each} ` ); - return generateCode(); - }); + })); if (options.demo) { - sv.file(`${kit.routesDirectory}/demo/+page.svelte`, (content) => { - return addToDemoPage(content, 'paraglide', language); - }); + sv.file(`${kit.routesDirectory}/demo/+page.svelte`, addToDemoPage('paraglide')); // add usage example - sv.file(`${kit.routesDirectory}/demo/paraglide/+page.svelte`, (content) => { - const { ast, generateCode } = parse.svelte(content); + sv.file(`${kit.routesDirectory}/demo/paraglide/+page.svelte`, transforms.svelte((ast, { language }) => { svelte.ensureScript(ast, { language }); js.imports.addNamed(ast.instance.content, { @@ -233,19 +212,15 @@ export default defineAddon({ '

If you use VSCode, install the Sherlock i18n extension for a better i18n experience.

'; svelte.addFragment(ast, templateCode); - - return generateCode(); - }); + })); } const { validLanguageTags } = parseLanguageTagInput(options.languageTags); for (const languageTag of validLanguageTags) { - sv.file(`messages/${languageTag}.json`, (content) => { - const { data, generateCode } = parse.json(content); + sv.file(`messages/${languageTag}.json`, transforms.json((data) => { data['$schema'] = 'https://inlang.com/schema/inlang-message-format'; data.hello_world = `Hello, {name} from ${languageTag}!`; - return generateCode(); - }); + })); } }, diff --git a/packages/sv/src/addons/playwright.ts b/packages/sv/src/addons/playwright.ts index 75216fe83..382bd3bcb 100644 --- a/packages/sv/src/addons/playwright.ts +++ b/packages/sv/src/addons/playwright.ts @@ -1,5 +1,5 @@ import { log } from '@clack/prompts'; -import { color, dedent, js, parse, json, text } from '@sveltejs/sv-utils'; +import { color, dedent, js, transforms, json, text } from '@sveltejs/sv-utils'; import { defineAddon } from '../core/config.ts'; import { addToDemoPage } from './common.ts'; @@ -11,14 +11,10 @@ export default defineAddon({ run: ({ sv, language, files, kit }) => { sv.devDependency('@playwright/test', '^1.58.2'); - sv.file(files.package, (content) => { - const { data, generateCode } = parse.json(content); - + sv.file(files.package, transforms.json((data) => { json.packageScriptsUpsert(data, 'test:e2e', 'playwright test'); json.packageScriptsUpsert(data, 'test', 'npm run test:e2e'); - - return generateCode(); - }); + })); sv.file(files.gitignore, (content) => { if (!content) return content; @@ -29,9 +25,7 @@ export default defineAddon({ const testRoute = kit ? '/demo/playwright' : '/'; if (kit) { - sv.file(`${kit.routesDirectory}/demo/+page.svelte`, (content) => { - return addToDemoPage(content, 'playwright', language); - }); + sv.file(`${kit.routesDirectory}/demo/+page.svelte`, addToDemoPage('playwright')); sv.file(`${testDir}/+page.svelte`, (content) => { if (content) return content; @@ -55,8 +49,7 @@ export default defineAddon({ `; }); - sv.file(`playwright.config.${language}`, (content) => { - const { ast, generateCode } = parse.script(content); + sv.file(`playwright.config.${language}`, transforms.script((ast) => { const defineConfig = js.common.parseExpression('defineConfig({})'); const { value: defaultExport } = js.exports.createDefault(ast, { fallback: defineConfig }); @@ -79,8 +72,7 @@ export default defineAddon({ } else { log.warn('Unexpected playwright config for playwright add-on. Could not update.'); } - return generateCode(); - }); + })); }, nextSteps: ({ kit }) => { diff --git a/packages/sv/src/addons/prettier.ts b/packages/sv/src/addons/prettier.ts index c1c7d307a..3c2e3edb5 100644 --- a/packages/sv/src/addons/prettier.ts +++ b/packages/sv/src/addons/prettier.ts @@ -1,5 +1,5 @@ import { log } from '@clack/prompts'; -import { color, dedent, parse, json } from '@sveltejs/sv-utils'; +import { color, dedent, parse, transforms, json } from '@sveltejs/sv-utils'; import { defineAddon } from '../core/config.ts'; import { addEslintConfigPrettier } from './common.ts'; @@ -68,20 +68,14 @@ export default defineAddon({ const eslintVersion = dependencyVersion('eslint'); const eslintInstalled = hasEslint(eslintVersion); - sv.file(files.package, (content) => { - const { data, generateCode } = parse.json(content); - + sv.file(files.package, transforms.json((data) => { json.packageScriptsUpsert(data, 'lint', 'prettier --check .', { mode: 'prepend' }); json.packageScriptsUpsert(data, 'format', 'prettier --write .'); + })); - return generateCode(); - }); - - sv.file(files.vscodeExtensions, (content) => { - const { data, generateCode } = parse.json(content); + sv.file(files.vscodeExtensions, transforms.json((data) => { json.arrayUpsert(data, 'recommendations', 'esbenp.prettier-vscode'); - return generateCode(); - }); + })); if (eslintVersion?.startsWith(SUPPORTED_ESLINT_VERSION) === false) { log.warn( diff --git a/packages/sv/src/addons/sveltekit-adapter.ts b/packages/sv/src/addons/sveltekit-adapter.ts index a05057391..5bc56e4bf 100644 --- a/packages/sv/src/addons/sveltekit-adapter.ts +++ b/packages/sv/src/addons/sveltekit-adapter.ts @@ -1,4 +1,4 @@ -import { color, js, resolveCommand, json, sanitizeName, text, parse } from '@sveltejs/sv-utils'; +import { color, js, resolveCommand, json, sanitizeName, text, parse, transforms } from '@sveltejs/sv-utils'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { defineAddon, defineAddonOptions } from '../core/config.ts'; @@ -45,8 +45,7 @@ export default defineAddon({ const adapter = adapters.find((a) => a.id === options.adapter)!; // removes previously installed adapters - sv.file(files.package, (content) => { - const { data, generateCode } = parse.json(content); + sv.file(files.package, transforms.json((data) => { const devDeps = data['devDependencies']; for (const pkg of Object.keys(devDeps)) { @@ -63,15 +62,11 @@ export default defineAddon({ : 'wrangler pages dev .svelte-kit/cloudflare --port 4173'; data.scripts.preview = preview; } - - return generateCode(); - }); + })); sv.devDependency(adapter.package, adapter.version); - sv.file(files.svelteConfig, (content) => { - const { ast, comments, generateCode } = parse.script(content); - + sv.file(files.svelteConfig, transforms.script((ast, comments) => { // finds any existing adapter's import declaration const importDecls = ast.body.filter((n) => n.type === 'ImportDeclaration'); const adapterImportDecl = importDecls.find( @@ -117,9 +112,7 @@ export default defineAddon({ c.loc.end.line <= cfgKitValue.loc.end.line ); } - - return generateCode(); - }); + })); if (adapter.package === '@sveltejs/adapter-cloudflare') { sv.devDependency('wrangler', '^4.63.0'); @@ -180,28 +173,18 @@ export default defineAddon({ }); // Setup wrangler types command - sv.file(files.package, (content) => { - const { data, generateCode } = parse.json(content); - + sv.file(files.package, transforms.json((data) => { json.packageScriptsUpsert(data, 'gen', 'wrangler types'); - - return generateCode(); - }); + })); // Add Cloudflare generated types to tsconfig - sv.file(`${jsconfig ? 'jsconfig' : 'tsconfig'}.json`, (content) => { - const { data, generateCode } = parse.json(content); - + sv.file(`${jsconfig ? 'jsconfig' : 'tsconfig'}.json`, transforms.json((data) => { data.compilerOptions ??= {}; data.compilerOptions.types ??= []; data.compilerOptions.types.push('./worker-configuration.d.ts'); + })); - return generateCode(); - }); - - sv.file('src/app.d.ts', (content) => { - const { ast, comments, generateCode } = parse.script(content); - + sv.file('src/app.d.ts', transforms.script((ast, comments) => { const platform = js.kit.addGlobalAppInterface(ast, { name: 'Platform' }); if (!platform) { throw new Error('Failed detecting `platform` interface in `src/app.d.ts`'); @@ -216,9 +199,7 @@ export default defineAddon({ js.common.createTypeProperty('caches', 'CacheStorage'), js.common.createTypeProperty('cf', 'IncomingRequestCfProperties', true) ); - - return generateCode(); - }); + })); } } }, diff --git a/packages/sv/src/addons/tailwindcss.ts b/packages/sv/src/addons/tailwindcss.ts index 00941ab25..a0cce3d24 100644 --- a/packages/sv/src/addons/tailwindcss.ts +++ b/packages/sv/src/addons/tailwindcss.ts @@ -1,4 +1,4 @@ -import { css, js, parse, svelte, json } from '@sveltejs/sv-utils'; +import { css, js, transforms, svelte, json } from '@sveltejs/sv-utils'; import { defineAddon, defineAddonOptions } from '../core/config.ts'; const plugins = [ @@ -30,7 +30,7 @@ export default defineAddon({ shortDescription: 'css framework', homepage: 'https://tailwindcss.com', options, - run: ({ sv, options, files, kit, dependencyVersion, language }) => { + run: ({ sv, options, files, kit, dependencyVersion }) => { const prettierInstalled = Boolean(dependencyVersion('prettier')); sv.devDependency('tailwindcss', '^4.1.18'); @@ -46,19 +46,13 @@ export default defineAddon({ } // add the vite plugin - sv.file(files.viteConfig, (content) => { - const { ast, generateCode } = parse.script(content); - + sv.file(files.viteConfig, transforms.script((ast) => { const vitePluginName = 'tailwindcss'; js.imports.addDefault(ast, { as: vitePluginName, from: '@tailwindcss/vite' }); js.vite.addPlugin(ast, { code: `${vitePluginName}()`, mode: 'prepend' }); + })); - return generateCode(); - }); - - sv.file(files.stylesheet, (content) => { - const { ast, generateCode } = parse.css(content); - + sv.file(files.stylesheet, transforms.css((ast) => { // since we are prepending all the `AtRule` let's add them in reverse order, // so they appear in the expected order in the final file @@ -77,63 +71,46 @@ export default defineAddon({ params: `'tailwindcss'`, append: false }); - - return generateCode(); - }); + })); if (!kit) { const appSvelte = 'src/App.svelte'; const stylesheetRelative = files.getRelative({ from: appSvelte, to: files.stylesheet }); - sv.file(appSvelte, (content) => { - const { ast, generateCode } = parse.svelte(content); + sv.file(appSvelte, transforms.svelte((ast, { language }) => { svelte.ensureScript(ast, { language }); js.imports.addEmpty(ast.instance.content, { from: stylesheetRelative }); - return generateCode(); - }); + })); } else { const layoutSvelte = `${kit?.routesDirectory}/+layout.svelte`; const stylesheetRelative = files.getRelative({ from: layoutSvelte, to: files.stylesheet }); - sv.file(layoutSvelte, (content) => { - const { ast, generateCode } = parse.svelte(content); + sv.file(layoutSvelte, transforms.svelte((ast, { language }) => { svelte.ensureScript(ast, { language }); js.imports.addEmpty(ast.instance.content, { from: stylesheetRelative }); - if (content.length === 0) { + if (ast.fragment.nodes.length === 0) { const svelteVersion = dependencyVersion('svelte'); if (!svelteVersion) throw new Error('Failed to determine svelte version'); svelte.addSlot(ast, { svelteVersion }); } - - return generateCode(); - }); + })); } - sv.file(files.vscodeSettings, (content) => { - const { data, generateCode } = parse.json(content); - + sv.file(files.vscodeSettings, transforms.json((data) => { data['files.associations'] ??= {}; data['files.associations']['*.css'] = 'tailwindcss'; + })); - return generateCode(); - }); - - sv.file(files.vscodeExtensions, (content) => { - const { data, generateCode } = parse.json(content); + sv.file(files.vscodeExtensions, transforms.json((data) => { json.arrayUpsert(data, 'recommendations', 'bradlc.vscode-tailwindcss'); - return generateCode(); - }); + })); if (prettierInstalled) { - sv.file(files.prettierrc, (content) => { - const { data, generateCode } = parse.json(content); - + sv.file(files.prettierrc, transforms.json((data) => { json.arrayUpsert(data, 'plugins', 'prettier-plugin-tailwindcss'); data.tailwindStylesheet ??= files.getRelative({ to: files.stylesheet }); - - return generateCode(); - }); + })); } } }); diff --git a/packages/sv/src/addons/vitest-addon.ts b/packages/sv/src/addons/vitest-addon.ts index 50f632980..8022a9cb6 100644 --- a/packages/sv/src/addons/vitest-addon.ts +++ b/packages/sv/src/addons/vitest-addon.ts @@ -1,4 +1,4 @@ -import { color, dedent, js, parse, json } from '@sveltejs/sv-utils'; +import { color, dedent, js, transforms, json } from '@sveltejs/sv-utils'; import { defineAddon, defineAddonOptions } from '../core/config.ts'; const options = defineAddonOptions() @@ -40,15 +40,11 @@ export default defineAddon({ sv.devDependency('playwright', '^1.58.2'); } - sv.file(files.package, (content) => { - const { data, generateCode } = parse.json(content); - + sv.file(files.package, transforms.json((data) => { json.packageScriptsUpsert(data, 'test:unit', 'vitest'); // we use `--run` so that vitest doesn't run in watch mode when running `npm run test` json.packageScriptsUpsert(data, 'test', 'npm run test:unit -- --run', { mode: 'prepend' }); - - return generateCode(); - }); + })); const examplesDir = (kit ? kit.libDirectory : 'src/lib') + '/vitest-examples'; const typed = language === 'ts'; @@ -119,9 +115,7 @@ export default defineAddon({ }); } - sv.file(files.viteConfig, (content) => { - const { ast, generateCode } = parse.script(content); - + sv.file(files.viteConfig, transforms.script((ast) => { const clientObjectExpression = js.object.create({ extends: `./${files.viteConfig}`, test: { @@ -177,9 +171,7 @@ export default defineAddon({ // Remove the old import js.imports.remove(ast, { name: importName, from: 'vite', statement }); } - - return generateCode(); - }); + })); }, nextSteps: ({ language, options }) => { From 65ee4704279fa2b9dad463279014eb07125e6987 Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 21 Mar 2026 19:04:22 +0100 Subject: [PATCH 03/22] fmt --- packages/sv-utils/src/tooling/transforms.ts | 14 +- packages/sv/src/addons/better-auth.ts | 257 ++++++------ packages/sv/src/addons/drizzle.ts | 417 ++++++++++---------- packages/sv/src/addons/eslint.ts | 225 ++++++----- packages/sv/src/addons/paraglide.ts | 235 ++++++----- packages/sv/src/addons/playwright.ts | 62 +-- packages/sv/src/addons/prettier.ts | 20 +- packages/sv/src/addons/sveltekit-adapter.ts | 193 +++++---- packages/sv/src/addons/tailwindcss.ts | 121 +++--- packages/sv/src/addons/vitest-addon.ts | 128 +++--- 10 files changed, 904 insertions(+), 768 deletions(-) diff --git a/packages/sv-utils/src/tooling/transforms.ts b/packages/sv-utils/src/tooling/transforms.ts index 9a0756d4b..f539e19e9 100644 --- a/packages/sv-utils/src/tooling/transforms.ts +++ b/packages/sv-utils/src/tooling/transforms.ts @@ -21,7 +21,15 @@ export type TransformContext = { const TRANSFORM_KEY = '__transform' as const; -export type TransformType = 'script' | 'css' | 'svelte' | 'json' | 'yaml' | 'toml' | 'text' | 'html'; +export type TransformType = + | 'script' + | 'css' + | 'svelte' + | 'json' + | 'yaml' + | 'toml' + | 'text' + | 'html'; export type TransformFn = { (content: string, ctx?: TransformContext): string; @@ -173,9 +181,7 @@ export const transforms = { * * Return `false` from the callback to abort — the original content is returned unchanged. */ - html( - cb: (ast: SvelteAst.Fragment, ctx: TransformContext) => void | false - ): TransformFn { + html(cb: (ast: SvelteAst.Fragment, ctx: TransformContext) => void | false): TransformFn { const fn = ((content: string, ctx?: TransformContext) => { const { ast, generateCode } = parseHtml(content); const result = cb(ast, ctx ?? { language: 'ts' }); diff --git a/packages/sv/src/addons/better-auth.ts b/packages/sv/src/addons/better-auth.ts index 18c9fe19b..898c8e638 100644 --- a/packages/sv/src/addons/better-auth.ts +++ b/packages/sv/src/addons/better-auth.ts @@ -55,70 +55,75 @@ export default defineAddon({ sv.devDependency('better-auth', '~1.4.21'); sv.devDependency('@better-auth/cli', '~1.4.21'); - sv.file(`drizzle.config.${language}`, transforms.script((ast) => { - const isProp = (name: string, node: AstTypes.Property) => - node.key.type === 'Identifier' && node.key.name === name; - - // tsgo can't infer visitor node types from zimmerframe's distributive conditional - Walker.walk(ast as AstTypes.Node, null, { - Property(node: AstTypes.Property) { - if ( - isProp('dialect', node) && - node.value.type === 'Literal' && - typeof node.value.value === 'string' - ) { - drizzleDialect = node.value.value as Dialect; - } - if ( - isProp('driver', node) && - node.value.type === 'Literal' && - node.value.value === 'd1-http' - ) { - d1 = true; + sv.file( + `drizzle.config.${language}`, + transforms.script((ast) => { + const isProp = (name: string, node: AstTypes.Property) => + node.key.type === 'Identifier' && node.key.name === name; + + // tsgo can't infer visitor node types from zimmerframe's distributive conditional + Walker.walk(ast as AstTypes.Node, null, { + Property(node: AstTypes.Property) { + if ( + isProp('dialect', node) && + node.value.type === 'Literal' && + typeof node.value.value === 'string' + ) { + drizzleDialect = node.value.value as Dialect; + } + if ( + isProp('driver', node) && + node.value.type === 'Literal' && + node.value.value === 'd1-http' + ) { + d1 = true; + } } - } - }); + }); - if (!drizzleDialect) { - throw new Error('Failed to detect DB dialect in your `drizzle.config.[js|ts]` file'); - } - })); + if (!drizzleDialect) { + throw new Error('Failed to detect DB dialect in your `drizzle.config.[js|ts]` file'); + } + }) + ); sv.file('.env', (content) => generateEnvFileContent(content, demoGithub, false)); sv.file('.env.example', (content) => generateEnvFileContent(content, demoGithub, true)); - sv.file(`${kit?.libDirectory}/server/auth.${language}`, transforms.script((ast, comments) => { - js.imports.addNamed(ast, { from: '$lib/server/db', imports: [d1 ? 'getDb' : 'db'] }); - js.imports.addNamed(ast, { from: '$app/server', imports: ['getRequestEvent'] }); - js.imports.addNamed(ast, { from: '$env/dynamic/private', imports: ['env'] }); - js.imports.addNamed(ast, { from: 'better-auth/svelte-kit', imports: ['sveltekitCookies'] }); - js.imports.addNamed(ast, { - from: 'better-auth/adapters/drizzle', - imports: ['drizzleAdapter'] - }); - js.imports.addNamed(ast, { from: 'better-auth/minimal', imports: ['betterAuth'] }); - - const dialectMap: Record = { - mysql: 'mysql', - postgresql: 'pg', - sqlite: 'sqlite', - turso: 'sqlite' - }; - const provider = dialectMap[drizzleDialect]; - - const githubProvider = demoGithub - ? ` + sv.file( + `${kit?.libDirectory}/server/auth.${language}`, + transforms.script((ast, comments) => { + js.imports.addNamed(ast, { from: '$lib/server/db', imports: [d1 ? 'getDb' : 'db'] }); + js.imports.addNamed(ast, { from: '$app/server', imports: ['getRequestEvent'] }); + js.imports.addNamed(ast, { from: '$env/dynamic/private', imports: ['env'] }); + js.imports.addNamed(ast, { from: 'better-auth/svelte-kit', imports: ['sveltekitCookies'] }); + js.imports.addNamed(ast, { + from: 'better-auth/adapters/drizzle', + imports: ['drizzleAdapter'] + }); + js.imports.addNamed(ast, { from: 'better-auth/minimal', imports: ['betterAuth'] }); + + const dialectMap: Record = { + mysql: 'mysql', + postgresql: 'pg', + sqlite: 'sqlite', + turso: 'sqlite' + }; + const provider = dialectMap[drizzleDialect]; + + const githubProvider = demoGithub + ? ` socialProviders: { github: { clientId: env.GITHUB_CLIENT_ID, clientSecret: env.GITHUB_CLIENT_SECRET, }, },` - : ''; + : ''; - let authConfig = ''; - if (d1) { - authConfig = dedent` + let authConfig = ''; + if (d1) { + authConfig = dedent` const authConfig = { baseURL: env.ORIGIN, secret: env.BETTER_AUTH_SECRET, @@ -142,8 +147,8 @@ export default defineAddon({ * To access \`auth\` at runtime, use \`event.locals.auth\`. */ export const auth = createAuth(${language === 'ts' ? 'null!' : 'null'});`; - } else { - authConfig = dedent` + } else { + authConfig = dedent` export const auth = betterAuth({ baseURL: env.ORIGIN, secret: env.BETTER_AUTH_SECRET, @@ -155,20 +160,24 @@ export default defineAddon({ sveltekitCookies(getRequestEvent) // make sure this is the last plugin in the array ], });`; - } - js.common.appendFromString(ast, { code: authConfig, comments }); - })); + } + js.common.appendFromString(ast, { code: authConfig, comments }); + }) + ); const authConfigPath = `${kit?.libDirectory}/server/auth.${language}`; const authSchemaPath = `${kit?.libDirectory}/server/db/auth.schema.${language}`; - sv.file(files.package, transforms.json((data) => { - json.packageScriptsUpsert( - data, - 'auth:schema', - `better-auth generate --config ${authConfigPath} --output ${authSchemaPath} --yes` - ); - })); + sv.file( + files.package, + transforms.json((data) => { + json.packageScriptsUpsert( + data, + 'auth:schema', + `better-auth generate --config ${authConfigPath} --output ${authSchemaPath} --yes` + ); + }) + ); sv.file(`${kit?.libDirectory}/server/db/auth.schema.${language}`, (content) => { if (content) return content; @@ -177,62 +186,73 @@ export default defineAddon({ `; }); - sv.file(`${kit?.libDirectory}/server/db/schema.${language}`, transforms.script((ast) => { - js.exports.addNamespace(ast, { from: './auth.schema' }); - })); - - sv.file('src/app.d.ts', transforms.script((ast, comments) => { - if (d1) js.imports.addNamed(ast, { imports: ['createAuth'], from: '$lib/server/auth' }); - js.imports.addNamed(ast, { - imports: ['User', 'Session'], - from: 'better-auth/minimal', - isType: true - }); - - const locals = js.kit.addGlobalAppInterface(ast, { name: 'Locals' }); - if (!locals) { - throw new Error('Failed detecting `locals` interface in `src/app.d.ts`'); - } - - // remove the commented out placeholder since we're adding the real one - comments.remove((c) => c.type === 'Line' && c.value.trim() === 'interface Locals {}'); + sv.file( + `${kit?.libDirectory}/server/db/schema.${language}`, + transforms.script((ast) => { + js.exports.addNamespace(ast, { from: './auth.schema' }); + }) + ); + + sv.file( + 'src/app.d.ts', + transforms.script((ast, comments) => { + if (d1) js.imports.addNamed(ast, { imports: ['createAuth'], from: '$lib/server/auth' }); + js.imports.addNamed(ast, { + imports: ['User', 'Session'], + from: 'better-auth/minimal', + isType: true + }); + + const locals = js.kit.addGlobalAppInterface(ast, { name: 'Locals' }); + if (!locals) { + throw new Error('Failed detecting `locals` interface in `src/app.d.ts`'); + } - const user = locals.body.body.find((prop) => - js.common.hasTypeProperty(prop, { name: 'user' }) - ); - const session = locals.body.body.find((prop) => - js.common.hasTypeProperty(prop, { name: 'session' }) - ); - const auth = locals.body.body.find((prop) => - js.common.hasTypeProperty(prop, { name: 'auth' }) - ); + // remove the commented out placeholder since we're adding the real one + comments.remove((c) => c.type === 'Line' && c.value.trim() === 'interface Locals {}'); - if (!user) { - locals.body.body.push(js.common.createTypeProperty('user', 'User', true)); - } - if (!session) { - locals.body.body.push(js.common.createTypeProperty('session', 'Session', true)); - } - if (d1 && !auth) { - locals.body.body.push( - js.common.createTypeProperty('auth', 'ReturnType', false) + const user = locals.body.body.find((prop) => + js.common.hasTypeProperty(prop, { name: 'user' }) + ); + const session = locals.body.body.find((prop) => + js.common.hasTypeProperty(prop, { name: 'session' }) + ); + const auth = locals.body.body.find((prop) => + js.common.hasTypeProperty(prop, { name: 'auth' }) ); - } - })); - - sv.file(`src/hooks.server.${language}`, transforms.script((ast, comments) => { - js.imports.addNamed(ast, { imports: ['svelteKitHandler'], from: 'better-auth/svelte-kit' }); - js.imports.addNamed(ast, { imports: [d1 ? 'createAuth' : 'auth'], from: '$lib/server/auth' }); - js.imports.addNamed(ast, { imports: ['building'], from: '$app/environment' }); - const d1HandleSetup = d1 - ? dedent` + if (!user) { + locals.body.body.push(js.common.createTypeProperty('user', 'User', true)); + } + if (!session) { + locals.body.body.push(js.common.createTypeProperty('session', 'Session', true)); + } + if (d1 && !auth) { + locals.body.body.push( + js.common.createTypeProperty('auth', 'ReturnType', false) + ); + } + }) + ); + + sv.file( + `src/hooks.server.${language}`, + transforms.script((ast, comments) => { + js.imports.addNamed(ast, { imports: ['svelteKitHandler'], from: 'better-auth/svelte-kit' }); + js.imports.addNamed(ast, { + imports: [d1 ? 'createAuth' : 'auth'], + from: '$lib/server/auth' + }); + js.imports.addNamed(ast, { imports: ['building'], from: '$app/environment' }); + + const d1HandleSetup = d1 + ? dedent` if (!event.platform?.env?.DB) throw new Error('D1 binding "DB" not found — are you running with wrangler?'); event.locals.auth = createAuth(event.platform.env.DB); const { auth } = event.locals;\n` - : ''; + : ''; - const handleContent = dedent` + const handleContent = dedent` async ({ event, resolve }) => {${d1HandleSetup} // Fetch current session from Better Auth const session = await auth.api.getSession({ @@ -248,13 +268,14 @@ export default defineAddon({ export const handle = sequence(handleBetterAuth, handleSession); `; - js.kit.addHooksHandle(ast, { - language, - newHandleName: 'handleBetterAuth', - handleContent, - comments - }); - })); + js.kit.addHooksHandle(ast, { + language, + newHandleName: 'handleBetterAuth', + handleContent, + comments + }); + }) + ); if (hasDemo) { sv.file(`${kit?.routesDirectory}/demo/+page.svelte`, addToDemoPage('better-auth')); diff --git a/packages/sv/src/addons/drizzle.ts b/packages/sv/src/addons/drizzle.ts index 06c9c120b..09e0e9c90 100644 --- a/packages/sv/src/addons/drizzle.ts +++ b/packages/sv/src/addons/drizzle.ts @@ -186,13 +186,16 @@ export default defineAddon({ }); } - sv.file(files.package, transforms.json((data) => { - if (options.docker) json.packageScriptsUpsert(data, 'db:start', 'docker compose up'); - json.packageScriptsUpsert(data, 'db:push', 'drizzle-kit push'); - json.packageScriptsUpsert(data, 'db:generate', 'drizzle-kit generate'); - json.packageScriptsUpsert(data, 'db:migrate', 'drizzle-kit migrate'); - json.packageScriptsUpsert(data, 'db:studio', 'drizzle-kit studio'); - })); + sv.file( + files.package, + transforms.json((data) => { + if (options.docker) json.packageScriptsUpsert(data, 'db:start', 'docker compose up'); + json.packageScriptsUpsert(data, 'db:push', 'drizzle-kit push'); + json.packageScriptsUpsert(data, 'db:generate', 'drizzle-kit generate'); + json.packageScriptsUpsert(data, 'db:migrate', 'drizzle-kit migrate'); + json.packageScriptsUpsert(data, 'db:studio', 'drizzle-kit studio'); + }) + ); const hasPrettier = Boolean(dependencyVersion('prettier')); if (hasPrettier) { @@ -208,53 +211,58 @@ export default defineAddon({ }); } - sv.file(paths['drizzle config'], transforms.script((ast) => { - const d1 = options.database === 'd1'; - const turso = options.sqlite === 'turso'; - - js.imports.addNamed(ast, { from: 'drizzle-kit', imports: { defineConfig: 'defineConfig' } }); + sv.file( + paths['drizzle config'], + transforms.script((ast) => { + const d1 = options.database === 'd1'; + const turso = options.sqlite === 'turso'; - if (d1) { - ast.body.push( - js.common.parseStatement( - "if (!process.env.CLOUDFLARE_ACCOUNT_ID) throw new Error('CLOUDFLARE_ACCOUNT_ID is not set');" - ), - js.common.parseStatement( - "if (!process.env.CLOUDFLARE_DATABASE_ID) throw new Error('CLOUDFLARE_DATABASE_ID is not set');" - ), - js.common.parseStatement( - "if (!process.env.CLOUDFLARE_D1_TOKEN) throw new Error('CLOUDFLARE_D1_TOKEN is not set');" - ) - ); - } else { - ast.body.push( - js.common.parseStatement( - "if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set');" - ) - ); - } - - const getDialect = (): string => { - if (d1) return 'sqlite'; - if (turso) return 'turso'; - return options.database; - }; + js.imports.addNamed(ast, { + from: 'drizzle-kit', + imports: { defineConfig: 'defineConfig' } + }); - const getCredentials = (): string => { - const creds: string[] = []; if (d1) { - creds.push('accountId: process.env.CLOUDFLARE_ACCOUNT_ID,'); - creds.push('databaseId: process.env.CLOUDFLARE_DATABASE_ID,'); - creds.push('token: process.env.CLOUDFLARE_D1_TOKEN,'); + ast.body.push( + js.common.parseStatement( + "if (!process.env.CLOUDFLARE_ACCOUNT_ID) throw new Error('CLOUDFLARE_ACCOUNT_ID is not set');" + ), + js.common.parseStatement( + "if (!process.env.CLOUDFLARE_DATABASE_ID) throw new Error('CLOUDFLARE_DATABASE_ID is not set');" + ), + js.common.parseStatement( + "if (!process.env.CLOUDFLARE_D1_TOKEN) throw new Error('CLOUDFLARE_D1_TOKEN is not set');" + ) + ); + } else { + ast.body.push( + js.common.parseStatement( + "if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set');" + ) + ); } - if (turso) creds.push('authToken: process.env.DATABASE_AUTH_TOKEN,'); - if (!d1) creds.push('url: process.env.DATABASE_URL,'); - - return creds.join('\n'); - }; - js.exports.createDefault(ast, { - fallback: js.common.parseExpression(` + const getDialect = (): string => { + if (d1) return 'sqlite'; + if (turso) return 'turso'; + return options.database; + }; + + const getCredentials = (): string => { + const creds: string[] = []; + if (d1) { + creds.push('accountId: process.env.CLOUDFLARE_ACCOUNT_ID,'); + creds.push('databaseId: process.env.CLOUDFLARE_DATABASE_ID,'); + creds.push('token: process.env.CLOUDFLARE_D1_TOKEN,'); + } + if (turso) creds.push('authToken: process.env.DATABASE_AUTH_TOKEN,'); + if (!d1) creds.push('url: process.env.DATABASE_URL,'); + + return creds.join('\n'); + }; + + js.exports.createDefault(ast, { + fallback: js.common.parseExpression(` defineConfig({ schema: "./src/lib/server/db/schema.${language}", dialect: "${getDialect()}", @@ -266,190 +274,197 @@ export default defineAddon({ strict: true }) `) - }); - })); - - sv.file(paths['database schema'], transforms.script((ast) => { - let taskSchemaExpression; - if (options.database === 'sqlite' || options.database === 'd1') { - js.imports.addNamed(ast, { - from: 'drizzle-orm/sqlite-core', - imports: ['integer', 'sqliteTable', 'text'] }); + }) + ); - taskSchemaExpression = js.common.parseExpression(`sqliteTable('task', { + sv.file( + paths['database schema'], + transforms.script((ast) => { + let taskSchemaExpression; + if (options.database === 'sqlite' || options.database === 'd1') { + js.imports.addNamed(ast, { + from: 'drizzle-orm/sqlite-core', + imports: ['integer', 'sqliteTable', 'text'] + }); + + taskSchemaExpression = js.common.parseExpression(`sqliteTable('task', { id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()), title: text('title').notNull(), priority: integer('priority').notNull().default(1) })`); - } - if (options.database === 'mysql') { - js.imports.addNamed(ast, { - from: 'drizzle-orm/mysql-core', - imports: ['mysqlTable', 'serial', 'int', 'text'] - }); + } + if (options.database === 'mysql') { + js.imports.addNamed(ast, { + from: 'drizzle-orm/mysql-core', + imports: ['mysqlTable', 'serial', 'int', 'text'] + }); - taskSchemaExpression = js.common.parseExpression(`mysqlTable('task', { + taskSchemaExpression = js.common.parseExpression(`mysqlTable('task', { id: serial('id').primaryKey(), title: text('title').notNull(), priority: int('priority').notNull().default(1) })`); - } - if (options.database === 'postgresql') { - js.imports.addNamed(ast, { - from: 'drizzle-orm/pg-core', - imports: ['pgTable', 'serial', 'integer', 'text'] - }); + } + if (options.database === 'postgresql') { + js.imports.addNamed(ast, { + from: 'drizzle-orm/pg-core', + imports: ['pgTable', 'serial', 'integer', 'text'] + }); - taskSchemaExpression = js.common.parseExpression(`pgTable('task', { + taskSchemaExpression = js.common.parseExpression(`pgTable('task', { id: serial('id').primaryKey(), title: text('title').notNull(), priority: integer('priority').notNull().default(1) })`); - } - - if (!taskSchemaExpression) throw new Error('unreachable state...'); - const taskIdentifier = js.variables.declaration(ast, { - kind: 'const', - name: 'task', - value: taskSchemaExpression - }); - js.exports.createNamed(ast, { - name: 'task', - fallback: taskIdentifier - }); - })); - - sv.file(paths.database, transforms.script((ast) => { - if (options.database === 'd1') { - js.imports.addNamespace(ast, { from: './schema', as: 'schema' }); - js.imports.addNamed(ast, { from: 'drizzle-orm/d1', imports: ['drizzle'] }); + } - const getDbFn = js.common.parseStatement( - `export const getDb = (d1${typescript ? ': D1Database' : ''}) => drizzle(d1, { schema });` - ); + if (!taskSchemaExpression) throw new Error('unreachable state...'); + const taskIdentifier = js.variables.declaration(ast, { + kind: 'const', + name: 'task', + value: taskSchemaExpression + }); + js.exports.createNamed(ast, { + name: 'task', + fallback: taskIdentifier + }); + }) + ); - ast.body.push(getDbFn); + sv.file( + paths.database, + transforms.script((ast) => { + if (options.database === 'd1') { + js.imports.addNamespace(ast, { from: './schema', as: 'schema' }); + js.imports.addNamed(ast, { from: 'drizzle-orm/d1', imports: ['drizzle'] }); - return; - } + const getDbFn = js.common.parseStatement( + `export const getDb = (d1${typescript ? ': D1Database' : ''}) => drizzle(d1, { schema });` + ); - js.imports.addNamed(ast, { from: '$env/dynamic/private', imports: ['env'] }); - js.imports.addNamespace(ast, { from: './schema', as: 'schema' }); + ast.body.push(getDbFn); - // env var checks - const dbURLCheck = js.common.parseStatement( - "if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set');" - ); - ast.body.push(dbURLCheck); + return; + } - let clientExpression; - // SQLite - if (options.sqlite === 'better-sqlite3') { - js.imports.addDefault(ast, { from: 'better-sqlite3', as: 'Database' }); - js.imports.addNamed(ast, { - from: 'drizzle-orm/better-sqlite3', - imports: ['drizzle'] - }); + js.imports.addNamed(ast, { from: '$env/dynamic/private', imports: ['env'] }); + js.imports.addNamespace(ast, { from: './schema', as: 'schema' }); - clientExpression = js.common.parseExpression('new Database(env.DATABASE_URL)'); - } - if (options.sqlite === 'libsql' || options.sqlite === 'turso') { - js.imports.addNamed(ast, { - from: '@libsql/client', - imports: ['createClient'] - }); - js.imports.addNamed(ast, { - from: 'drizzle-orm/libsql', - imports: ['drizzle'] - }); + // env var checks + const dbURLCheck = js.common.parseStatement( + "if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set');" + ); + ast.body.push(dbURLCheck); + + let clientExpression; + // SQLite + if (options.sqlite === 'better-sqlite3') { + js.imports.addDefault(ast, { from: 'better-sqlite3', as: 'Database' }); + js.imports.addNamed(ast, { + from: 'drizzle-orm/better-sqlite3', + imports: ['drizzle'] + }); + + clientExpression = js.common.parseExpression('new Database(env.DATABASE_URL)'); + } + if (options.sqlite === 'libsql' || options.sqlite === 'turso') { + js.imports.addNamed(ast, { + from: '@libsql/client', + imports: ['createClient'] + }); + js.imports.addNamed(ast, { + from: 'drizzle-orm/libsql', + imports: ['drizzle'] + }); + + if (options.sqlite === 'turso') { + ast.body.push( + js.common.parseStatement( + "if (!env.DATABASE_AUTH_TOKEN) throw new Error('DATABASE_AUTH_TOKEN is not set');" + ) + ); + clientExpression = js.common.parseExpression( + 'createClient({ url: env.DATABASE_URL, authToken: env.DATABASE_AUTH_TOKEN })' + ); + } else { + clientExpression = js.common.parseExpression('createClient({ url: env.DATABASE_URL })'); + } + } + // MySQL + if (options.mysql === 'mysql2' || options.mysql === 'planetscale') { + js.imports.addDefault(ast, { from: 'mysql2/promise', as: 'mysql' }); + js.imports.addNamed(ast, { + from: 'drizzle-orm/mysql2', + imports: ['drizzle'] + }); + + clientExpression = js.common.parseExpression('mysql.createPool(env.DATABASE_URL)'); + } + // PostgreSQL + if (options.postgresql === 'neon') { + js.imports.addNamed(ast, { + from: '@neondatabase/serverless', + imports: ['neon'] + }); + js.imports.addNamed(ast, { + from: 'drizzle-orm/neon-http', + imports: ['drizzle'] + }); + + clientExpression = js.common.parseExpression('neon(env.DATABASE_URL)'); + } + if (options.postgresql === 'postgres.js') { + js.imports.addDefault(ast, { from: 'postgres', as: 'postgres' }); + js.imports.addNamed(ast, { + from: 'drizzle-orm/postgres-js', + imports: ['drizzle'] + }); - if (options.sqlite === 'turso') { - ast.body.push( - js.common.parseStatement( - "if (!env.DATABASE_AUTH_TOKEN) throw new Error('DATABASE_AUTH_TOKEN is not set');" - ) - ); - clientExpression = js.common.parseExpression( - 'createClient({ url: env.DATABASE_URL, authToken: env.DATABASE_AUTH_TOKEN })' - ); - } else { - clientExpression = js.common.parseExpression('createClient({ url: env.DATABASE_URL })'); + clientExpression = js.common.parseExpression('postgres(env.DATABASE_URL)'); } - } - // MySQL - if (options.mysql === 'mysql2' || options.mysql === 'planetscale') { - js.imports.addDefault(ast, { from: 'mysql2/promise', as: 'mysql' }); - js.imports.addNamed(ast, { - from: 'drizzle-orm/mysql2', - imports: ['drizzle'] - }); - clientExpression = js.common.parseExpression('mysql.createPool(env.DATABASE_URL)'); - } - // PostgreSQL - if (options.postgresql === 'neon') { - js.imports.addNamed(ast, { - from: '@neondatabase/serverless', - imports: ['neon'] - }); - js.imports.addNamed(ast, { - from: 'drizzle-orm/neon-http', - imports: ['drizzle'] - }); + if (!clientExpression) throw new Error('unreachable state...'); + ast.body.push( + js.variables.declaration(ast, { + kind: 'const', + name: 'client', + value: clientExpression + }) + ); - clientExpression = js.common.parseExpression('neon(env.DATABASE_URL)'); - } - if (options.postgresql === 'postgres.js') { - js.imports.addDefault(ast, { from: 'postgres', as: 'postgres' }); - js.imports.addNamed(ast, { - from: 'drizzle-orm/postgres-js', - imports: ['drizzle'] + // create drizzle function call + const drizzleCall = js.functions.createCall({ + name: 'drizzle', + args: ['client'], + useIdentifiers: true }); - clientExpression = js.common.parseExpression('postgres(env.DATABASE_URL)'); - } + // add schema to support `db.query` + const paramObject = js.object.create({ + schema: js.variables.createIdentifier('schema') + }); + if (options.database === 'mysql') { + const mode = options.mysql === 'planetscale' ? 'planetscale' : 'default'; + js.object.property(paramObject, { + name: 'mode', + fallback: js.common.createLiteral(mode) + }); + } + drizzleCall.arguments.push(paramObject); - if (!clientExpression) throw new Error('unreachable state...'); - ast.body.push( - js.variables.declaration(ast, { + // create `db` export + const db = js.variables.declaration(ast, { kind: 'const', - name: 'client', - value: clientExpression - }) - ); - - // create drizzle function call - const drizzleCall = js.functions.createCall({ - name: 'drizzle', - args: ['client'], - useIdentifiers: true - }); - - // add schema to support `db.query` - const paramObject = js.object.create({ - schema: js.variables.createIdentifier('schema') - }); - if (options.database === 'mysql') { - const mode = options.mysql === 'planetscale' ? 'planetscale' : 'default'; - js.object.property(paramObject, { - name: 'mode', - fallback: js.common.createLiteral(mode) + name: 'db', + value: drizzleCall }); - } - drizzleCall.arguments.push(paramObject); - - // create `db` export - const db = js.variables.declaration(ast, { - kind: 'const', - name: 'db', - value: drizzleCall - }); - js.exports.createNamed(ast, { - name: 'db', - fallback: db - }); - })); + js.exports.createNamed(ast, { + name: 'db', + fallback: db + }); + }) + ); }, nextSteps: ({ options, packageManager, cwd }) => { diff --git a/packages/sv/src/addons/eslint.ts b/packages/sv/src/addons/eslint.ts index 358d0fa26..157622ddc 100644 --- a/packages/sv/src/addons/eslint.ts +++ b/packages/sv/src/addons/eslint.ts @@ -23,118 +23,127 @@ export default defineAddon({ if (prettierInstalled) sv.devDependency('eslint-config-prettier', '^10.1.8'); - sv.file(files.package, transforms.json((data) => { - json.packageScriptsUpsert(data, 'lint', 'eslint .'); - })); - - sv.file(files.eslintConfig, transforms.script((ast, comments) => { - const eslintConfigs: Array = []; - js.imports.addDefault(ast, { from: './svelte.config.js', as: 'svelteConfig' }); - const gitIgnorePathStatement = js.common.parseStatement( - "\nconst gitignorePath = path.resolve(import.meta.dirname, '.gitignore');" - ); - js.common.appendStatement(ast, { statement: gitIgnorePathStatement }); - - const ignoresConfig = js.common.parseExpression('includeIgnoreFile(gitignorePath)'); - eslintConfigs.push(ignoresConfig); - - const jsConfig = js.common.parseExpression('js.configs.recommended'); - eslintConfigs.push(jsConfig); - - if (typescript) { - const tsConfig = js.common.parseExpression('ts.configs.recommended'); - eslintConfigs.push(tsConfig); - } - - const svelteConfig = js.common.parseExpression('svelte.configs.recommended'); - eslintConfigs.push(svelteConfig); - - const globalsBrowser = js.common.createSpread(js.common.parseExpression('globals.browser')); - const globalsNode = js.common.createSpread(js.common.parseExpression('globals.node')); - const globalsObjLiteral = js.object.create({}); - globalsObjLiteral.properties = [globalsBrowser, globalsNode]; - const rules = js.object.create({ '"no-undef"': 'off' }); - - if (rules.properties[0].type !== 'Property') { - throw new Error('rules.properties[0].type !== "Property"'); - } - comments.add(rules.properties[0].key, { - type: 'Line', - value: - ' typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.' - }); - comments.add(rules.properties[0].key, { - type: 'Line', - value: - ' see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors' - }); - - const globalsConfig = js.object.create({ - languageOptions: { - globals: globalsObjLiteral - }, - rules: typescript ? rules : undefined - }); - - eslintConfigs.push(globalsConfig); - - if (typescript) { - const svelteTSParserConfig = js.object.create({ - files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], - languageOptions: { - parserOptions: { - projectService: true, - extraFileExtensions: ['.svelte'], - parser: js.variables.createIdentifier('ts.parser'), - svelteConfig: js.variables.createIdentifier('svelteConfig') - } - } + sv.file( + files.package, + transforms.json((data) => { + json.packageScriptsUpsert(data, 'lint', 'eslint .'); + }) + ); + + sv.file( + files.eslintConfig, + transforms.script((ast, comments) => { + const eslintConfigs: Array = []; + js.imports.addDefault(ast, { from: './svelte.config.js', as: 'svelteConfig' }); + const gitIgnorePathStatement = js.common.parseStatement( + "\nconst gitignorePath = path.resolve(import.meta.dirname, '.gitignore');" + ); + js.common.appendStatement(ast, { statement: gitIgnorePathStatement }); + + const ignoresConfig = js.common.parseExpression('includeIgnoreFile(gitignorePath)'); + eslintConfigs.push(ignoresConfig); + + const jsConfig = js.common.parseExpression('js.configs.recommended'); + eslintConfigs.push(jsConfig); + + if (typescript) { + const tsConfig = js.common.parseExpression('ts.configs.recommended'); + eslintConfigs.push(tsConfig); + } + + const svelteConfig = js.common.parseExpression('svelte.configs.recommended'); + eslintConfigs.push(svelteConfig); + + const globalsBrowser = js.common.createSpread(js.common.parseExpression('globals.browser')); + const globalsNode = js.common.createSpread(js.common.parseExpression('globals.node')); + const globalsObjLiteral = js.object.create({}); + globalsObjLiteral.properties = [globalsBrowser, globalsNode]; + const rules = js.object.create({ '"no-undef"': 'off' }); + + if (rules.properties[0].type !== 'Property') { + throw new Error('rules.properties[0].type !== "Property"'); + } + comments.add(rules.properties[0].key, { + type: 'Line', + value: + ' typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.' }); - eslintConfigs.push(svelteTSParserConfig); - } else { - const svelteTSParserConfig = js.object.create({ - files: ['**/*.svelte', '**/*.svelte.js'], + comments.add(rules.properties[0].key, { + type: 'Line', + value: + ' see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors' + }); + + const globalsConfig = js.object.create({ languageOptions: { - parserOptions: { - svelteConfig: js.variables.createIdentifier('svelteConfig') + globals: globalsObjLiteral + }, + rules: typescript ? rules : undefined + }); + + eslintConfigs.push(globalsConfig); + + if (typescript) { + const svelteTSParserConfig = js.object.create({ + files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], + languageOptions: { + parserOptions: { + projectService: true, + extraFileExtensions: ['.svelte'], + parser: js.variables.createIdentifier('ts.parser'), + svelteConfig: js.variables.createIdentifier('svelteConfig') + } } - } + }); + eslintConfigs.push(svelteTSParserConfig); + } else { + const svelteTSParserConfig = js.object.create({ + files: ['**/*.svelte', '**/*.svelte.js'], + languageOptions: { + parserOptions: { + svelteConfig: js.variables.createIdentifier('svelteConfig') + } + } + }); + eslintConfigs.push(svelteTSParserConfig); + } + + const exportExpression = js.functions.createCall({ name: 'defineConfig', args: [] }); + if (typescript) { + exportExpression.arguments.push(...eslintConfigs); + } else { + const eslintArray = js.array.create(); + eslintConfigs.map((x) => js.array.append(eslintArray, x)); + exportExpression.arguments.push(eslintArray); + } + const { value: defaultExport } = js.exports.createDefault(ast, { + fallback: exportExpression + }); + // if it's not the config we created, then we'll leave it alone and exit out + if (defaultExport !== exportExpression) { + log.warn('An eslint config is already defined. Skipping initialization.'); + return false; + } + + if (typescript) js.imports.addDefault(ast, { from: 'typescript-eslint', as: 'ts' }); + js.imports.addDefault(ast, { from: 'globals', as: 'globals' }); + js.imports.addNamed(ast, { from: 'eslint/config', imports: ['defineConfig'] }); + js.imports.addDefault(ast, { from: 'eslint-plugin-svelte', as: 'svelte' }); + js.imports.addDefault(ast, { from: '@eslint/js', as: 'js' }); + js.imports.addNamed(ast, { + from: '@eslint/compat', + imports: ['includeIgnoreFile'] }); - eslintConfigs.push(svelteTSParserConfig); - } - - const exportExpression = js.functions.createCall({ name: 'defineConfig', args: [] }); - if (typescript) { - exportExpression.arguments.push(...eslintConfigs); - } else { - const eslintArray = js.array.create(); - eslintConfigs.map((x) => js.array.append(eslintArray, x)); - exportExpression.arguments.push(eslintArray); - } - const { value: defaultExport } = js.exports.createDefault(ast, { - fallback: exportExpression - }); - // if it's not the config we created, then we'll leave it alone and exit out - if (defaultExport !== exportExpression) { - log.warn('An eslint config is already defined. Skipping initialization.'); - return false; - } - - if (typescript) js.imports.addDefault(ast, { from: 'typescript-eslint', as: 'ts' }); - js.imports.addDefault(ast, { from: 'globals', as: 'globals' }); - js.imports.addNamed(ast, { from: 'eslint/config', imports: ['defineConfig'] }); - js.imports.addDefault(ast, { from: 'eslint-plugin-svelte', as: 'svelte' }); - js.imports.addDefault(ast, { from: '@eslint/js', as: 'js' }); - js.imports.addNamed(ast, { - from: '@eslint/compat', - imports: ['includeIgnoreFile'] - }); - js.imports.addDefault(ast, { from: 'node:path', as: 'path' }); - })); - - sv.file(files.vscodeExtensions, transforms.json((data) => { - json.arrayUpsert(data, 'recommendations', 'dbaeumer.vscode-eslint'); - })); + js.imports.addDefault(ast, { from: 'node:path', as: 'path' }); + }) + ); + + sv.file( + files.vscodeExtensions, + transforms.json((data) => { + json.arrayUpsert(data, 'recommendations', 'dbaeumer.vscode-eslint'); + }) + ); if (prettierInstalled) { sv.file(files.eslintConfig, addEslintConfigPrettier); diff --git a/packages/sv/src/addons/paraglide.ts b/packages/sv/src/addons/paraglide.ts index 11d1dea3f..a4c2feec0 100644 --- a/packages/sv/src/addons/paraglide.ts +++ b/packages/sv/src/addons/paraglide.ts @@ -61,82 +61,94 @@ export default defineAddon({ sv.devDependency('@inlang/paraglide-js', '^2.10.0'); // add the vite plugin - sv.file(files.viteConfig, transforms.script((ast) => { - const vitePluginName = 'paraglideVitePlugin'; - js.imports.addNamed(ast, { imports: [vitePluginName], from: '@inlang/paraglide-js' }); - js.vite.addPlugin(ast, { - code: `${vitePluginName}({ + sv.file( + files.viteConfig, + transforms.script((ast) => { + const vitePluginName = 'paraglideVitePlugin'; + js.imports.addNamed(ast, { imports: [vitePluginName], from: '@inlang/paraglide-js' }); + js.vite.addPlugin(ast, { + code: `${vitePluginName}({ project: './project.inlang', outdir: './${paraglideOutDir}' })` - }); - })); + }); + }) + ); // reroute hook - sv.file(`src/hooks.${language}`, transforms.script((ast) => { - js.imports.addNamed(ast, { - from: '$lib/paraglide/runtime', - imports: ['deLocalizeUrl'] - }); - - const expression = js.common.parseExpression( - '(request) => deLocalizeUrl(request.url).pathname' - ); - const rerouteIdentifier = js.variables.declaration(ast, { - kind: 'const', - name: 'reroute', - value: expression - }); - - const existingExport = js.exports.createNamed(ast, { - name: 'reroute', - fallback: rerouteIdentifier - }); - if (existingExport.declaration !== rerouteIdentifier) { - log.warn('Adding the reroute hook automatically failed. Add it manually'); - } - })); + sv.file( + `src/hooks.${language}`, + transforms.script((ast) => { + js.imports.addNamed(ast, { + from: '$lib/paraglide/runtime', + imports: ['deLocalizeUrl'] + }); + + const expression = js.common.parseExpression( + '(request) => deLocalizeUrl(request.url).pathname' + ); + const rerouteIdentifier = js.variables.declaration(ast, { + kind: 'const', + name: 'reroute', + value: expression + }); + + const existingExport = js.exports.createNamed(ast, { + name: 'reroute', + fallback: rerouteIdentifier + }); + if (existingExport.declaration !== rerouteIdentifier) { + log.warn('Adding the reroute hook automatically failed. Add it manually'); + } + }) + ); // handle hook - sv.file(`src/hooks.server.${language}`, transforms.script((ast, comments) => { - js.imports.addNamed(ast, { - from: '$lib/paraglide/server', - imports: ['paraglideMiddleware'] - }); - js.imports.addNamed(ast, { - from: '$lib/paraglide/runtime', - imports: ['getTextDirection'] - }); - - const hookHandleContent = `({ event, resolve }) => paraglideMiddleware(event.request, ({ request, locale }) => { + sv.file( + `src/hooks.server.${language}`, + transforms.script((ast, comments) => { + js.imports.addNamed(ast, { + from: '$lib/paraglide/server', + imports: ['paraglideMiddleware'] + }); + js.imports.addNamed(ast, { + from: '$lib/paraglide/runtime', + imports: ['getTextDirection'] + }); + + const hookHandleContent = `({ event, resolve }) => paraglideMiddleware(event.request, ({ request, locale }) => { event.request = request; return resolve(event, { transformPageChunk: ({ html }) => html.replace('%paraglide.lang%', locale).replace('%paraglide.dir%', getTextDirection(locale)) }); });`; - js.kit.addHooksHandle(ast, { - language, - newHandleName: 'handleParaglide', - handleContent: hookHandleContent, - comments - }); - })); + js.kit.addHooksHandle(ast, { + language, + newHandleName: 'handleParaglide', + handleContent: hookHandleContent, + comments + }); + }) + ); // add the lang and dir attributes placeholder to app.html - sv.file('src/app.html', transforms.html((ast) => { - const htmlNode = ast.nodes.find( - (child): child is SvelteAst.RegularElement => - child.type === 'RegularElement' && child.name === 'html' - ); - if (!htmlNode) { - log.warn( - "Could not find node in app.html. You'll need to add the language placeholder manually" + sv.file( + 'src/app.html', + transforms.html((ast) => { + const htmlNode = ast.nodes.find( + (child): child is SvelteAst.RegularElement => + child.type === 'RegularElement' && child.name === 'html' ); - return; - } - html.addAttribute(htmlNode, 'lang', '%paraglide.lang%'); - html.addAttribute(htmlNode, 'dir', '%paraglide.dir%'); - })); + if (!htmlNode) { + log.warn( + "Could not find node in app.html. You'll need to add the language placeholder manually" + ); + return; + } + html.addAttribute(htmlNode, 'lang', '%paraglide.lang%'); + html.addAttribute(htmlNode, 'dir', '%paraglide.dir%'); + }) + ); sv.file(files.gitignore, (content) => { if (!content) return content; @@ -162,65 +174,74 @@ export default defineAddon({ })(content); }); - sv.file(`${kit.routesDirectory}/+layout.svelte`, transforms.svelte((ast, { language }) => { - svelte.ensureScript(ast, { language }); - js.imports.addNamed(ast.instance.content, { - imports: ['locales', 'localizeHref'], - from: '$lib/paraglide/runtime' - }); - js.imports.addNamed(ast.instance.content, { imports: ['page'], from: '$app/state' }); - svelte.addFragment( - ast, - `
+ sv.file( + `${kit.routesDirectory}/+layout.svelte`, + transforms.svelte((ast, { language }) => { + svelte.ensureScript(ast, { language }); + js.imports.addNamed(ast.instance.content, { + imports: ['locales', 'localizeHref'], + from: '$lib/paraglide/runtime' + }); + js.imports.addNamed(ast.instance.content, { imports: ['page'], from: '$app/state' }); + svelte.addFragment( + ast, + `
{#each locales as locale} {locale} {/each}
` - ); - })); + ); + }) + ); if (options.demo) { sv.file(`${kit.routesDirectory}/demo/+page.svelte`, addToDemoPage('paraglide')); // add usage example - sv.file(`${kit.routesDirectory}/demo/paraglide/+page.svelte`, transforms.svelte((ast, { language }) => { - svelte.ensureScript(ast, { language }); - - js.imports.addNamed(ast.instance.content, { - imports: { m: 'm' }, - from: '$lib/paraglide/messages.js' - }); - js.imports.addNamed(ast.instance.content, { - imports: { - setLocale: 'setLocale' - }, - from: '$lib/paraglide/runtime' - }); - - // add localized message - let templateCode = "

{m.hello_world({ name: 'SvelteKit User' })}

"; - - // add links to other localized pages, the first one is the default - // language, thus it does not require any localized route - const { validLanguageTags } = parseLanguageTagInput(options.languageTags); - const links = validLanguageTags - .map((x) => ``) - .join(''); - templateCode += `
${links}
`; - - templateCode += - '

If you use VSCode, install the Sherlock i18n extension for a better i18n experience.

'; - - svelte.addFragment(ast, templateCode); - })); + sv.file( + `${kit.routesDirectory}/demo/paraglide/+page.svelte`, + transforms.svelte((ast, { language }) => { + svelte.ensureScript(ast, { language }); + + js.imports.addNamed(ast.instance.content, { + imports: { m: 'm' }, + from: '$lib/paraglide/messages.js' + }); + js.imports.addNamed(ast.instance.content, { + imports: { + setLocale: 'setLocale' + }, + from: '$lib/paraglide/runtime' + }); + + // add localized message + let templateCode = "

{m.hello_world({ name: 'SvelteKit User' })}

"; + + // add links to other localized pages, the first one is the default + // language, thus it does not require any localized route + const { validLanguageTags } = parseLanguageTagInput(options.languageTags); + const links = validLanguageTags + .map((x) => ``) + .join(''); + templateCode += `
${links}
`; + + templateCode += + '

If you use VSCode, install the Sherlock i18n extension for a better i18n experience.

'; + + svelte.addFragment(ast, templateCode); + }) + ); } const { validLanguageTags } = parseLanguageTagInput(options.languageTags); for (const languageTag of validLanguageTags) { - sv.file(`messages/${languageTag}.json`, transforms.json((data) => { - data['$schema'] = 'https://inlang.com/schema/inlang-message-format'; - data.hello_world = `Hello, {name} from ${languageTag}!`; - })); + sv.file( + `messages/${languageTag}.json`, + transforms.json((data) => { + data['$schema'] = 'https://inlang.com/schema/inlang-message-format'; + data.hello_world = `Hello, {name} from ${languageTag}!`; + }) + ); } }, diff --git a/packages/sv/src/addons/playwright.ts b/packages/sv/src/addons/playwright.ts index 382bd3bcb..f2ce91562 100644 --- a/packages/sv/src/addons/playwright.ts +++ b/packages/sv/src/addons/playwright.ts @@ -11,10 +11,13 @@ export default defineAddon({ run: ({ sv, language, files, kit }) => { sv.devDependency('@playwright/test', '^1.58.2'); - sv.file(files.package, transforms.json((data) => { - json.packageScriptsUpsert(data, 'test:e2e', 'playwright test'); - json.packageScriptsUpsert(data, 'test', 'npm run test:e2e'); - })); + sv.file( + files.package, + transforms.json((data) => { + json.packageScriptsUpsert(data, 'test:e2e', 'playwright test'); + json.packageScriptsUpsert(data, 'test', 'npm run test:e2e'); + }) + ); sv.file(files.gitignore, (content) => { if (!content) return content; @@ -49,30 +52,33 @@ export default defineAddon({ `; }); - sv.file(`playwright.config.${language}`, transforms.script((ast) => { - const defineConfig = js.common.parseExpression('defineConfig({})'); - const { value: defaultExport } = js.exports.createDefault(ast, { fallback: defineConfig }); - - const config = { - webServer: { - command: 'npm run build && npm run preview', - port: 4173 - }, - testMatch: '**/*.e2e.{ts,js}' - }; - - if ( - defaultExport.type === 'CallExpression' && - defaultExport.arguments[0]?.type === 'ObjectExpression' - ) { - js.imports.addNamed(ast, { imports: ['defineConfig'], from: '@playwright/test' }); - js.object.overrideProperties(defaultExport.arguments[0], config); - } else if (defaultExport.type === 'ObjectExpression') { - js.object.overrideProperties(defaultExport, config); - } else { - log.warn('Unexpected playwright config for playwright add-on. Could not update.'); - } - })); + sv.file( + `playwright.config.${language}`, + transforms.script((ast) => { + const defineConfig = js.common.parseExpression('defineConfig({})'); + const { value: defaultExport } = js.exports.createDefault(ast, { fallback: defineConfig }); + + const config = { + webServer: { + command: 'npm run build && npm run preview', + port: 4173 + }, + testMatch: '**/*.e2e.{ts,js}' + }; + + if ( + defaultExport.type === 'CallExpression' && + defaultExport.arguments[0]?.type === 'ObjectExpression' + ) { + js.imports.addNamed(ast, { imports: ['defineConfig'], from: '@playwright/test' }); + js.object.overrideProperties(defaultExport.arguments[0], config); + } else if (defaultExport.type === 'ObjectExpression') { + js.object.overrideProperties(defaultExport, config); + } else { + log.warn('Unexpected playwright config for playwright add-on. Could not update.'); + } + }) + ); }, nextSteps: ({ kit }) => { diff --git a/packages/sv/src/addons/prettier.ts b/packages/sv/src/addons/prettier.ts index 3c2e3edb5..2da846ac1 100644 --- a/packages/sv/src/addons/prettier.ts +++ b/packages/sv/src/addons/prettier.ts @@ -68,14 +68,20 @@ export default defineAddon({ const eslintVersion = dependencyVersion('eslint'); const eslintInstalled = hasEslint(eslintVersion); - sv.file(files.package, transforms.json((data) => { - json.packageScriptsUpsert(data, 'lint', 'prettier --check .', { mode: 'prepend' }); - json.packageScriptsUpsert(data, 'format', 'prettier --write .'); - })); + sv.file( + files.package, + transforms.json((data) => { + json.packageScriptsUpsert(data, 'lint', 'prettier --check .', { mode: 'prepend' }); + json.packageScriptsUpsert(data, 'format', 'prettier --write .'); + }) + ); - sv.file(files.vscodeExtensions, transforms.json((data) => { - json.arrayUpsert(data, 'recommendations', 'esbenp.prettier-vscode'); - })); + sv.file( + files.vscodeExtensions, + transforms.json((data) => { + json.arrayUpsert(data, 'recommendations', 'esbenp.prettier-vscode'); + }) + ); if (eslintVersion?.startsWith(SUPPORTED_ESLINT_VERSION) === false) { log.warn( diff --git a/packages/sv/src/addons/sveltekit-adapter.ts b/packages/sv/src/addons/sveltekit-adapter.ts index 5bc56e4bf..07af0647d 100644 --- a/packages/sv/src/addons/sveltekit-adapter.ts +++ b/packages/sv/src/addons/sveltekit-adapter.ts @@ -1,4 +1,13 @@ -import { color, js, resolveCommand, json, sanitizeName, text, parse, transforms } from '@sveltejs/sv-utils'; +import { + color, + js, + resolveCommand, + json, + sanitizeName, + text, + parse, + transforms +} from '@sveltejs/sv-utils'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { defineAddon, defineAddonOptions } from '../core/config.ts'; @@ -45,74 +54,81 @@ export default defineAddon({ const adapter = adapters.find((a) => a.id === options.adapter)!; // removes previously installed adapters - sv.file(files.package, transforms.json((data) => { - const devDeps = data['devDependencies']; - - for (const pkg of Object.keys(devDeps)) { - if (pkg.startsWith('@sveltejs/adapter-')) { - delete devDeps[pkg]; + sv.file( + files.package, + transforms.json((data) => { + const devDeps = data['devDependencies']; + + for (const pkg of Object.keys(devDeps)) { + if (pkg.startsWith('@sveltejs/adapter-')) { + delete devDeps[pkg]; + } } - } - // in sk 3, we will keep "preview": "vite preview" like any other adapter - if (options.adapter === 'cloudflare') { - const preview = - options.cfTarget === 'workers' - ? 'wrangler dev .svelte-kit/cloudflare/_worker.js --port 4173' - : 'wrangler pages dev .svelte-kit/cloudflare --port 4173'; - data.scripts.preview = preview; - } - })); + // in sk 3, we will keep "preview": "vite preview" like any other adapter + if (options.adapter === 'cloudflare') { + const preview = + options.cfTarget === 'workers' + ? 'wrangler dev .svelte-kit/cloudflare/_worker.js --port 4173' + : 'wrangler pages dev .svelte-kit/cloudflare --port 4173'; + data.scripts.preview = preview; + } + }) + ); sv.devDependency(adapter.package, adapter.version); - sv.file(files.svelteConfig, transforms.script((ast, comments) => { - // finds any existing adapter's import declaration - const importDecls = ast.body.filter((n) => n.type === 'ImportDeclaration'); - const adapterImportDecl = importDecls.find( - (importDecl) => - typeof importDecl.source.value === 'string' && - importDecl.source.value.startsWith('@sveltejs/adapter-') && - importDecl.importKind === 'value' - ); + sv.file( + files.svelteConfig, + transforms.script((ast, comments) => { + // finds any existing adapter's import declaration + const importDecls = ast.body.filter((n) => n.type === 'ImportDeclaration'); + const adapterImportDecl = importDecls.find( + (importDecl) => + typeof importDecl.source.value === 'string' && + importDecl.source.value.startsWith('@sveltejs/adapter-') && + importDecl.importKind === 'value' + ); - let adapterName = 'adapter'; - if (adapterImportDecl) { - // replaces the import's source with the new adapter - adapterImportDecl.source.value = adapter.package; - // reset raw value, so that the string is re-generated - adapterImportDecl.source.raw = undefined; - - adapterName = adapterImportDecl.specifiers?.find((s) => s.type === 'ImportDefaultSpecifier') - ?.local?.name as string; - } else { - js.imports.addDefault(ast, { from: adapter.package, as: adapterName }); - } + let adapterName = 'adapter'; + if (adapterImportDecl) { + // replaces the import's source with the new adapter + adapterImportDecl.source.value = adapter.package; + // reset raw value, so that the string is re-generated + adapterImportDecl.source.raw = undefined; + + adapterName = adapterImportDecl.specifiers?.find( + (s) => s.type === 'ImportDefaultSpecifier' + )?.local?.name as string; + } else { + js.imports.addDefault(ast, { from: adapter.package, as: adapterName }); + } - const { value: config } = js.exports.createDefault(ast, { fallback: js.object.create({}) }); + const { value: config } = js.exports.createDefault(ast, { fallback: js.object.create({}) }); - // override the adapter property - js.object.overrideProperties(config, { - kit: { - adapter: js.functions.createCall({ name: adapterName, args: [], useIdentifiers: true }) - } - }); + // override the adapter property + js.object.overrideProperties(config, { + kit: { + adapter: js.functions.createCall({ name: adapterName, args: [], useIdentifiers: true }) + } + }); - // reset the comment for non-auto adapters - if (adapter.package !== '@sveltejs/adapter-auto') { - const fallback = js.object.create({}); - const cfgKitValue = js.object.property(config, { name: 'kit', fallback }); - - // removes any existing adapter auto comments - comments.remove( - (c) => - c.loc && - cfgKitValue.loc && - c.loc.start.line >= cfgKitValue.loc.start.line && - c.loc.end.line <= cfgKitValue.loc.end.line - ); - } - })); + // reset the comment for non-auto adapters + if (adapter.package !== '@sveltejs/adapter-auto') { + const fallback = js.object.create({}); + const cfgKitValue = js.object.property(config, { name: 'kit', fallback }); + + // removes any existing adapter auto comments + comments.remove( + (c) => + c.loc && + cfgKitValue.loc && + c.loc.start.line >= cfgKitValue.loc.start.line && + c.loc.end.line <= cfgKitValue.loc.end.line + ); + } + }) + ); if (adapter.package === '@sveltejs/adapter-cloudflare') { sv.devDependency('wrangler', '^4.63.0'); @@ -173,33 +189,42 @@ export default defineAddon({ }); // Setup wrangler types command - sv.file(files.package, transforms.json((data) => { - json.packageScriptsUpsert(data, 'gen', 'wrangler types'); - })); + sv.file( + files.package, + transforms.json((data) => { + json.packageScriptsUpsert(data, 'gen', 'wrangler types'); + }) + ); // Add Cloudflare generated types to tsconfig - sv.file(`${jsconfig ? 'jsconfig' : 'tsconfig'}.json`, transforms.json((data) => { - data.compilerOptions ??= {}; - data.compilerOptions.types ??= []; - data.compilerOptions.types.push('./worker-configuration.d.ts'); - })); - - sv.file('src/app.d.ts', transforms.script((ast, comments) => { - const platform = js.kit.addGlobalAppInterface(ast, { name: 'Platform' }); - if (!platform) { - throw new Error('Failed detecting `platform` interface in `src/app.d.ts`'); - } - - // remove the commented out placeholder since we're adding the real one - comments.remove((c) => c.type === 'Line' && c.value.trim() === 'interface Platform {}'); + sv.file( + `${jsconfig ? 'jsconfig' : 'tsconfig'}.json`, + transforms.json((data) => { + data.compilerOptions ??= {}; + data.compilerOptions.types ??= []; + data.compilerOptions.types.push('./worker-configuration.d.ts'); + }) + ); - platform.body.body.push( - js.common.createTypeProperty('env', 'Env'), - js.common.createTypeProperty('ctx', 'ExecutionContext'), - js.common.createTypeProperty('caches', 'CacheStorage'), - js.common.createTypeProperty('cf', 'IncomingRequestCfProperties', true) - ); - })); + sv.file( + 'src/app.d.ts', + transforms.script((ast, comments) => { + const platform = js.kit.addGlobalAppInterface(ast, { name: 'Platform' }); + if (!platform) { + throw new Error('Failed detecting `platform` interface in `src/app.d.ts`'); + } + + // remove the commented out placeholder since we're adding the real one + comments.remove((c) => c.type === 'Line' && c.value.trim() === 'interface Platform {}'); + + platform.body.body.push( + js.common.createTypeProperty('env', 'Env'), + js.common.createTypeProperty('ctx', 'ExecutionContext'), + js.common.createTypeProperty('caches', 'CacheStorage'), + js.common.createTypeProperty('cf', 'IncomingRequestCfProperties', true) + ); + }) + ); } } }, diff --git a/packages/sv/src/addons/tailwindcss.ts b/packages/sv/src/addons/tailwindcss.ts index a0cce3d24..30a225715 100644 --- a/packages/sv/src/addons/tailwindcss.ts +++ b/packages/sv/src/addons/tailwindcss.ts @@ -46,71 +46,92 @@ export default defineAddon({ } // add the vite plugin - sv.file(files.viteConfig, transforms.script((ast) => { - const vitePluginName = 'tailwindcss'; - js.imports.addDefault(ast, { as: vitePluginName, from: '@tailwindcss/vite' }); - js.vite.addPlugin(ast, { code: `${vitePluginName}()`, mode: 'prepend' }); - })); - - sv.file(files.stylesheet, transforms.css((ast) => { - // since we are prepending all the `AtRule` let's add them in reverse order, - // so they appear in the expected order in the final file - - for (const plugin of plugins) { - if (!options.plugins.includes(plugin.id)) continue; + sv.file( + files.viteConfig, + transforms.script((ast) => { + const vitePluginName = 'tailwindcss'; + js.imports.addDefault(ast, { as: vitePluginName, from: '@tailwindcss/vite' }); + js.vite.addPlugin(ast, { code: `${vitePluginName}()`, mode: 'prepend' }); + }) + ); + + sv.file( + files.stylesheet, + transforms.css((ast) => { + // since we are prepending all the `AtRule` let's add them in reverse order, + // so they appear in the expected order in the final file + + for (const plugin of plugins) { + if (!options.plugins.includes(plugin.id)) continue; + + css.addAtRule(ast, { + name: 'plugin', + params: `'${plugin.package}'`, + append: false + }); + } css.addAtRule(ast, { - name: 'plugin', - params: `'${plugin.package}'`, + name: 'import', + params: `'tailwindcss'`, append: false }); - } - - css.addAtRule(ast, { - name: 'import', - params: `'tailwindcss'`, - append: false - }); - })); + }) + ); if (!kit) { const appSvelte = 'src/App.svelte'; const stylesheetRelative = files.getRelative({ from: appSvelte, to: files.stylesheet }); - sv.file(appSvelte, transforms.svelte((ast, { language }) => { - svelte.ensureScript(ast, { language }); - js.imports.addEmpty(ast.instance.content, { from: stylesheetRelative }); - })); + sv.file( + appSvelte, + transforms.svelte((ast, { language }) => { + svelte.ensureScript(ast, { language }); + js.imports.addEmpty(ast.instance.content, { from: stylesheetRelative }); + }) + ); } else { const layoutSvelte = `${kit?.routesDirectory}/+layout.svelte`; const stylesheetRelative = files.getRelative({ from: layoutSvelte, to: files.stylesheet }); - sv.file(layoutSvelte, transforms.svelte((ast, { language }) => { - svelte.ensureScript(ast, { language }); - js.imports.addEmpty(ast.instance.content, { from: stylesheetRelative }); - - if (ast.fragment.nodes.length === 0) { - const svelteVersion = dependencyVersion('svelte'); - if (!svelteVersion) throw new Error('Failed to determine svelte version'); - svelte.addSlot(ast, { - svelteVersion - }); - } - })); + sv.file( + layoutSvelte, + transforms.svelte((ast, { language }) => { + svelte.ensureScript(ast, { language }); + js.imports.addEmpty(ast.instance.content, { from: stylesheetRelative }); + + if (ast.fragment.nodes.length === 0) { + const svelteVersion = dependencyVersion('svelte'); + if (!svelteVersion) throw new Error('Failed to determine svelte version'); + svelte.addSlot(ast, { + svelteVersion + }); + } + }) + ); } - sv.file(files.vscodeSettings, transforms.json((data) => { - data['files.associations'] ??= {}; - data['files.associations']['*.css'] = 'tailwindcss'; - })); - - sv.file(files.vscodeExtensions, transforms.json((data) => { - json.arrayUpsert(data, 'recommendations', 'bradlc.vscode-tailwindcss'); - })); + sv.file( + files.vscodeSettings, + transforms.json((data) => { + data['files.associations'] ??= {}; + data['files.associations']['*.css'] = 'tailwindcss'; + }) + ); + + sv.file( + files.vscodeExtensions, + transforms.json((data) => { + json.arrayUpsert(data, 'recommendations', 'bradlc.vscode-tailwindcss'); + }) + ); if (prettierInstalled) { - sv.file(files.prettierrc, transforms.json((data) => { - json.arrayUpsert(data, 'plugins', 'prettier-plugin-tailwindcss'); - data.tailwindStylesheet ??= files.getRelative({ to: files.stylesheet }); - })); + sv.file( + files.prettierrc, + transforms.json((data) => { + json.arrayUpsert(data, 'plugins', 'prettier-plugin-tailwindcss'); + data.tailwindStylesheet ??= files.getRelative({ to: files.stylesheet }); + }) + ); } } }); diff --git a/packages/sv/src/addons/vitest-addon.ts b/packages/sv/src/addons/vitest-addon.ts index 8022a9cb6..949bfe5a9 100644 --- a/packages/sv/src/addons/vitest-addon.ts +++ b/packages/sv/src/addons/vitest-addon.ts @@ -40,11 +40,14 @@ export default defineAddon({ sv.devDependency('playwright', '^1.58.2'); } - sv.file(files.package, transforms.json((data) => { - json.packageScriptsUpsert(data, 'test:unit', 'vitest'); - // we use `--run` so that vitest doesn't run in watch mode when running `npm run test` - json.packageScriptsUpsert(data, 'test', 'npm run test:unit -- --run', { mode: 'prepend' }); - })); + sv.file( + files.package, + transforms.json((data) => { + json.packageScriptsUpsert(data, 'test:unit', 'vitest'); + // we use `--run` so that vitest doesn't run in watch mode when running `npm run test` + json.packageScriptsUpsert(data, 'test', 'npm run test:unit -- --run', { mode: 'prepend' }); + }) + ); const examplesDir = (kit ? kit.libDirectory : 'src/lib') + '/vitest-examples'; const typed = language === 'ts'; @@ -115,63 +118,66 @@ export default defineAddon({ }); } - sv.file(files.viteConfig, transforms.script((ast) => { - const clientObjectExpression = js.object.create({ - extends: `./${files.viteConfig}`, - test: { - name: 'client', - browser: { - enabled: true, - provider: js.functions.createCall({ name: 'playwright', args: [] }), - instances: [{ browser: 'chromium', headless: true }] - }, - include: ['src/**/*.svelte.{test,spec}.{js,ts}'], - exclude: ['src/lib/server/**'] - } - }); - - const serverObjectExpression = js.object.create({ - extends: `./${files.viteConfig}`, - test: { - name: 'server', - environment: 'node', - include: ['src/**/*.{test,spec}.{js,ts}'], - exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'] - } - }); - - const viteConfig = js.vite.getConfig(ast); - - const testObject = js.object.property(viteConfig, { - name: 'test', - fallback: js.object.create({ - expect: { - requireAssertions: true + sv.file( + files.viteConfig, + transforms.script((ast) => { + const clientObjectExpression = js.object.create({ + extends: `./${files.viteConfig}`, + test: { + name: 'client', + browser: { + enabled: true, + provider: js.functions.createCall({ name: 'playwright', args: [] }), + instances: [{ browser: 'chromium', headless: true }] + }, + include: ['src/**/*.svelte.{test,spec}.{js,ts}'], + exclude: ['src/lib/server/**'] } - }) - }); - - const workspaceArray = js.object.property(testObject, { - name: 'projects', - fallback: js.array.create() - }); - - if (componentTesting) js.array.append(workspaceArray, clientObjectExpression); - if (unitTesting) js.array.append(workspaceArray, serverObjectExpression); - - // Manage imports - if (componentTesting) - js.imports.addNamed(ast, { imports: ['playwright'], from: '@vitest/browser-playwright' }); - const importName = 'defineConfig'; - const { statement, alias } = js.imports.find(ast, { name: importName, from: 'vite' }); - if (statement) { - // Switch the import from 'vite' to 'vitest/config' (keeping the alias) - js.imports.addNamed(ast, { imports: { defineConfig: alias }, from: 'vitest/config' }); - - // Remove the old import - js.imports.remove(ast, { name: importName, from: 'vite', statement }); - } - })); + }); + + const serverObjectExpression = js.object.create({ + extends: `./${files.viteConfig}`, + test: { + name: 'server', + environment: 'node', + include: ['src/**/*.{test,spec}.{js,ts}'], + exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'] + } + }); + + const viteConfig = js.vite.getConfig(ast); + + const testObject = js.object.property(viteConfig, { + name: 'test', + fallback: js.object.create({ + expect: { + requireAssertions: true + } + }) + }); + + const workspaceArray = js.object.property(testObject, { + name: 'projects', + fallback: js.array.create() + }); + + if (componentTesting) js.array.append(workspaceArray, clientObjectExpression); + if (unitTesting) js.array.append(workspaceArray, serverObjectExpression); + + // Manage imports + if (componentTesting) + js.imports.addNamed(ast, { imports: ['playwright'], from: '@vitest/browser-playwright' }); + const importName = 'defineConfig'; + const { statement, alias } = js.imports.find(ast, { name: importName, from: 'vite' }); + if (statement) { + // Switch the import from 'vite' to 'vitest/config' (keeping the alias) + js.imports.addNamed(ast, { imports: { defineConfig: alias }, from: 'vitest/config' }); + + // Remove the old import + js.imports.remove(ast, { name: importName, from: 'vite', statement }); + } + }) + ); }, nextSteps: ({ language, options }) => { From 1d7cef4e752f9016c6ffaff9bdfedee1f0773a23 Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 21 Mar 2026 19:52:42 +0100 Subject: [PATCH 04/22] ++ --- packages/sv-utils/src/index.ts | 14 +++---- packages/sv-utils/src/tests/transforms.ts | 44 +++++++++++++++++++++ packages/sv-utils/src/tooling/transforms.ts | 8 +++- packages/sv/src/addons/better-auth.ts | 3 ++ packages/sv/src/addons/paraglide.ts | 11 +++--- packages/sv/src/core/engine.ts | 1 + 6 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 packages/sv-utils/src/tests/transforms.ts diff --git a/packages/sv-utils/src/index.ts b/packages/sv-utils/src/index.ts index 7829d7ac1..fedb434f8 100644 --- a/packages/sv-utils/src/index.ts +++ b/packages/sv-utils/src/index.ts @@ -58,13 +58,13 @@ export { * ``` */ export const parse = { - css: parseCss as typeof parseCss, - html: parseHtml as typeof parseHtml, - json: parseJson as typeof parseJson, - script: parseScript as typeof parseScript, - svelte: parseSvelte as typeof parseSvelte, - toml: parseToml as typeof parseToml, - yaml: parseYaml as typeof parseYaml + css: parseCss, + html: parseHtml, + json: parseJson, + script: parseScript, + svelte: parseSvelte, + toml: parseToml, + yaml: parseYaml }; // Utilities diff --git a/packages/sv-utils/src/tests/transforms.ts b/packages/sv-utils/src/tests/transforms.ts new file mode 100644 index 000000000..0fcf30aa9 --- /dev/null +++ b/packages/sv-utils/src/tests/transforms.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; +import { isTransform, transforms } from '../tooling/transforms.ts'; + +describe('transforms', () => { + describe('json', () => { + it('roundtrip: parse → mutate → generateCode', () => { + const input = '{"name":"old"}'; + const fn = transforms.json((data) => { + data.name = 'new'; + }); + const result = fn(input); + expect(JSON.parse(result)).toEqual({ name: 'new' }); + }); + + it('abort: returns original content on false', () => { + const input = '{"name":"old"}'; + const fn = transforms.json(() => false); + expect(fn(input)).toBe(input); + }); + }); + + describe('text', () => { + it('transforms content', () => { + const fn = transforms.text((content) => content + '\nappended'); + expect(fn('line1')).toBe('line1\nappended'); + }); + + it('abort: returns original content on false', () => { + const input = 'original'; + const fn = transforms.text(() => false); + expect(fn(input)).toBe(input); + }); + }); + + describe('isTransform', () => { + it('returns true for branded transform', () => { + expect(isTransform(transforms.json(() => {}))).toBe(true); + }); + + it('returns false for plain function', () => { + expect(isTransform((content: string) => content)).toBe(false); + }); + }); +}); diff --git a/packages/sv-utils/src/tooling/transforms.ts b/packages/sv-utils/src/tooling/transforms.ts index f539e19e9..d94e85e1c 100644 --- a/packages/sv-utils/src/tooling/transforms.ts +++ b/packages/sv-utils/src/tooling/transforms.ts @@ -195,10 +195,14 @@ export const transforms = { /** * Transform a plain text file (.env, .gitignore, etc.). * No parsing — just string in, string out. + * + * Return `false` from the callback to abort — the original content is returned unchanged. */ - text(cb: (content: string, ctx: TransformContext) => string): TransformFn { + text(cb: (content: string, ctx: TransformContext) => string | false): TransformFn { const fn = ((content: string, ctx?: TransformContext) => { - return cb(content, ctx ?? { language: 'ts' }); + const result = cb(content, ctx ?? { language: 'ts' }); + if (result === false) return content; + return result; }) as TransformFn; fn[TRANSFORM_KEY] = 'text'; return fn; diff --git a/packages/sv/src/addons/better-auth.ts b/packages/sv/src/addons/better-auth.ts index 898c8e638..2893cf86f 100644 --- a/packages/sv/src/addons/better-auth.ts +++ b/packages/sv/src/addons/better-auth.ts @@ -55,6 +55,7 @@ export default defineAddon({ sv.devDependency('better-auth', '~1.4.21'); sv.devDependency('@better-auth/cli', '~1.4.21'); + // Read-only: extract dialect info from drizzle config without modifying it sv.file( `drizzle.config.${language}`, transforms.script((ast) => { @@ -84,6 +85,8 @@ export default defineAddon({ if (!drizzleDialect) { throw new Error('Failed to detect DB dialect in your `drizzle.config.[js|ts]` file'); } + + return false; }) ); diff --git a/packages/sv/src/addons/paraglide.ts b/packages/sv/src/addons/paraglide.ts index a4c2feec0..4a1ce1efd 100644 --- a/packages/sv/src/addons/paraglide.ts +++ b/packages/sv/src/addons/paraglide.ts @@ -159,10 +159,11 @@ export default defineAddon({ return content; }); - sv.file('project.inlang/settings.json', (content) => { - if (content) return content; + sv.file( + 'project.inlang/settings.json', + transforms.json((data) => { + if (Object.keys(data).length > 0) return false; - return transforms.json((data) => { for (const key in DEFAULT_INLANG_PROJECT) { data[key] = DEFAULT_INLANG_PROJECT[key as keyof typeof DEFAULT_INLANG_PROJECT]; } @@ -171,8 +172,8 @@ export default defineAddon({ data.baseLocale = baseLocale; data.locales = validLanguageTags; - })(content); - }); + }) + ); sv.file( `${kit.routesDirectory}/+layout.svelte`, diff --git a/packages/sv/src/core/engine.ts b/packages/sv/src/core/engine.ts index 672e4f463..4267a2bb7 100644 --- a/packages/sv/src/core/engine.ts +++ b/packages/sv/src/core/engine.ts @@ -179,6 +179,7 @@ async function runAddon({ addon, loaded, multiple, workspace, workspaceOptions } fileContent = isTransform(edit) ? edit(fileContent, { language: workspace.language }) : edit(fileContent); + // skip writing when the edit returns an empty string (e.g. no content to create) if (!fileContent) return fileContent; writeFile(workspace, path, fileContent); From 6f83ec3c153796bb030b772eb78ebe4d4c060872 Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 21 Mar 2026 20:02:32 +0100 Subject: [PATCH 05/22] rmv some parse --- packages/sv-utils/src/index.ts | 14 +- packages/sv-utils/src/tests/transforms.ts | 17 +++ packages/sv-utils/src/tooling/transforms.ts | 23 +++- packages/sv/src/addons/better-auth.ts | 134 +++++++++++--------- packages/sv/src/addons/drizzle.ts | 60 ++++----- packages/sv/src/addons/mcp.ts | 19 +-- packages/sv/src/addons/paraglide.ts | 15 ++- packages/sv/src/addons/playwright.ts | 45 ++++--- packages/sv/src/addons/prettier.ts | 95 +++++++------- packages/sv/src/addons/sveltekit-adapter.ts | 15 ++- packages/sv/src/addons/vitest-addon.ts | 110 +++++++++------- 11 files changed, 314 insertions(+), 233 deletions(-) diff --git a/packages/sv-utils/src/index.ts b/packages/sv-utils/src/index.ts index fedb434f8..7829d7ac1 100644 --- a/packages/sv-utils/src/index.ts +++ b/packages/sv-utils/src/index.ts @@ -58,13 +58,13 @@ export { * ``` */ export const parse = { - css: parseCss, - html: parseHtml, - json: parseJson, - script: parseScript, - svelte: parseSvelte, - toml: parseToml, - yaml: parseYaml + css: parseCss as typeof parseCss, + html: parseHtml as typeof parseHtml, + json: parseJson as typeof parseJson, + script: parseScript as typeof parseScript, + svelte: parseSvelte as typeof parseSvelte, + toml: parseToml as typeof parseToml, + yaml: parseYaml as typeof parseYaml }; // Utilities diff --git a/packages/sv-utils/src/tests/transforms.ts b/packages/sv-utils/src/tests/transforms.ts index 0fcf30aa9..1fcf7157c 100644 --- a/packages/sv-utils/src/tests/transforms.ts +++ b/packages/sv-utils/src/tests/transforms.ts @@ -17,6 +17,23 @@ describe('transforms', () => { const fn = transforms.json(() => false); expect(fn(input)).toBe(input); }); + + it('onParseError: calls handler and returns original content', () => { + const input = 'not: json'; + let caught = false; + const fn = transforms.json(() => {}, { + onParseError: () => { + caught = true; + } + }); + expect(fn(input)).toBe(input); + expect(caught).toBe(true); + }); + + it('throws on parse error without onParseError', () => { + const fn = transforms.json(() => {}); + expect(() => fn('not: json')).toThrow(); + }); }); describe('text', () => { diff --git a/packages/sv-utils/src/tooling/transforms.ts b/packages/sv-utils/src/tooling/transforms.ts index d94e85e1c..fbe81612e 100644 --- a/packages/sv-utils/src/tooling/transforms.ts +++ b/packages/sv-utils/src/tooling/transforms.ts @@ -130,13 +130,28 @@ export const transforms = { * Transform a JSON file. * * Return `false` from the callback to abort — the original content is returned unchanged. + * + * Pass `onParseError` to gracefully handle files that aren't valid JSON + * (e.g. `.prettierrc` which may be YAML). */ - json(cb: (data: T, ctx: TransformContext) => void | false): TransformFn { + json( + cb: (data: T, ctx: TransformContext) => void | false, + options?: { onParseError?: (error: unknown) => void } + ): TransformFn { const fn = ((content: string, ctx?: TransformContext) => { - const { data, generateCode } = parseJson(content); - const result = cb(data as T, ctx ?? { language: 'ts' }); + let parsed; + try { + parsed = parseJson(content); + } catch (error) { + if (options?.onParseError) { + options.onParseError(error); + return content; + } + throw error; + } + const result = cb(parsed.data as T, ctx ?? { language: 'ts' }); if (result === false) return content; - return generateCode(); + return parsed.generateCode(); }) as TransformFn; fn[TRANSFORM_KEY] = 'json'; return fn; diff --git a/packages/sv/src/addons/better-auth.ts b/packages/sv/src/addons/better-auth.ts index 2893cf86f..0e35d1959 100644 --- a/packages/sv/src/addons/better-auth.ts +++ b/packages/sv/src/addons/better-auth.ts @@ -90,8 +90,8 @@ export default defineAddon({ }) ); - sv.file('.env', (content) => generateEnvFileContent(content, demoGithub, false)); - sv.file('.env.example', (content) => generateEnvFileContent(content, demoGithub, true)); + sv.file('.env', transforms.text((content) => generateEnvFileContent(content, demoGithub, false))); + sv.file('.env.example', transforms.text((content) => generateEnvFileContent(content, demoGithub, true))); sv.file( `${kit?.libDirectory}/server/auth.${language}`, @@ -182,12 +182,15 @@ export default defineAddon({ }) ); - sv.file(`${kit?.libDirectory}/server/db/auth.schema.${language}`, (content) => { - if (content) return content; - return dedent` - // If you see this file, you have not run the auth:schema script yet, but you should! - `; - }); + sv.file( + `${kit?.libDirectory}/server/db/auth.schema.${language}`, + transforms.text((content) => { + if (content) return false; + return dedent` + // If you see this file, you have not run the auth:schema script yet, but you should! + `; + }) + ); sv.file( `${kit?.libDirectory}/server/db/schema.${language}`, @@ -285,11 +288,11 @@ export default defineAddon({ sv.file( `${kit!.routesDirectory}/demo/better-auth/login/+page.server.${language}`, - (content) => { + transforms.text((content) => { if (content) { const filePath = `${kit!.routesDirectory}/demo/better-auth/login/+page.server.${language}`; log.warn(`Existing ${color.warning(filePath)} file. Could not update.`); - return content; + return false; } const [ts] = createPrinter(language === 'ts'); @@ -386,29 +389,31 @@ export default defineAddon({ export const actions${ts(': Actions')} = {${signInEmailAction}${signInSocialAction} }; `; - } + }) ); - sv.file(`${kit!.routesDirectory}/demo/better-auth/login/+page.svelte`, (content) => { - if (content) { - const filePath = `${kit!.routesDirectory}/demo/better-auth/login/+page.svelte`; - log.warn(`Existing ${color.warning(filePath)} file. Could not update.`); - return content; - } + sv.file( + `${kit!.routesDirectory}/demo/better-auth/login/+page.svelte`, + transforms.text((content) => { + if (content) { + const filePath = `${kit!.routesDirectory}/demo/better-auth/login/+page.svelte`; + log.warn(`Existing ${color.warning(filePath)} file. Could not update.`); + return false; + } - const tailwind = dependencyVersion('@tailwindcss/vite') !== undefined; - const input = tailwind - ? ' class="mt-1 px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"' - : ''; - const btn = tailwind - ? ' class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition"' - : ''; + const tailwind = dependencyVersion('@tailwindcss/vite') !== undefined; + const input = tailwind + ? ' class="mt-1 px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"' + : ''; + const btn = tailwind + ? ' class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition"' + : ''; - const svelte5 = !!dependencyVersion('svelte')?.startsWith('5'); - const [ts, s5] = createPrinter(language === 'ts', svelte5); + const svelte5 = !!dependencyVersion('svelte')?.startsWith('5'); + const [ts, s5] = createPrinter(language === 'ts', svelte5); - const passwordForm = demoPassword - ? ` + const passwordForm = demoPassword + ? `