From d96649bf17ea2e1040ad82e739a5230150f381d0 Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Fri, 5 Jun 2026 20:30:26 +0200 Subject: [PATCH 01/10] feat(create): provide svelte config in `vite.config.js` during create --- .../shared/+demo+checkjs/svelte.config.js | 13 ------------ .../shared/+demo+typescript/svelte.config.js | 13 ------------ .../shared/+demo-typescript/svelte.config.js | 13 ------------ .../shared/+typescript/svelte.config.js | 17 ---------------- .../shared/-typescript/svelte.config.js | 17 ---------------- packages/sv/src/create/shared/vite.config.js | 20 +++++++++++++++++++ packages/sv/src/create/shared/vite.config.ts | 6 ------ .../create/templates/demo/svelte.config.js | 12 ----------- .../src/create/templates/demo/vite.config.js | 7 ++++++- .../create/templates/library/svelte.config.js | 12 ----------- .../create/templates/library/vite.config.js | 7 ++++++- .../create/templates/minimal/svelte.config.js | 12 ----------- .../create/templates/minimal/vite.config.js | 7 ++++++- 13 files changed, 38 insertions(+), 118 deletions(-) delete mode 100644 packages/sv/src/create/shared/+demo+checkjs/svelte.config.js delete mode 100644 packages/sv/src/create/shared/+demo+typescript/svelte.config.js delete mode 100644 packages/sv/src/create/shared/+demo-typescript/svelte.config.js delete mode 100644 packages/sv/src/create/shared/+typescript/svelte.config.js delete mode 100644 packages/sv/src/create/shared/-typescript/svelte.config.js create mode 100644 packages/sv/src/create/shared/vite.config.js delete mode 100644 packages/sv/src/create/shared/vite.config.ts delete mode 100644 packages/sv/src/create/templates/demo/svelte.config.js delete mode 100644 packages/sv/src/create/templates/library/svelte.config.js delete mode 100644 packages/sv/src/create/templates/minimal/svelte.config.js diff --git a/packages/sv/src/create/shared/+demo+checkjs/svelte.config.js b/packages/sv/src/create/shared/+demo+checkjs/svelte.config.js deleted file mode 100644 index 10c4eeb27..000000000 --- a/packages/sv/src/create/shared/+demo+checkjs/svelte.config.js +++ /dev/null @@ -1,13 +0,0 @@ -import adapter from '@sveltejs/adapter-auto'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://svelte.dev/docs/kit/adapters for more information about adapters. - adapter: adapter() - } -}; - -export default config; diff --git a/packages/sv/src/create/shared/+demo+typescript/svelte.config.js b/packages/sv/src/create/shared/+demo+typescript/svelte.config.js deleted file mode 100644 index 10c4eeb27..000000000 --- a/packages/sv/src/create/shared/+demo+typescript/svelte.config.js +++ /dev/null @@ -1,13 +0,0 @@ -import adapter from '@sveltejs/adapter-auto'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://svelte.dev/docs/kit/adapters for more information about adapters. - adapter: adapter() - } -}; - -export default config; diff --git a/packages/sv/src/create/shared/+demo-typescript/svelte.config.js b/packages/sv/src/create/shared/+demo-typescript/svelte.config.js deleted file mode 100644 index 10c4eeb27..000000000 --- a/packages/sv/src/create/shared/+demo-typescript/svelte.config.js +++ /dev/null @@ -1,13 +0,0 @@ -import adapter from '@sveltejs/adapter-auto'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://svelte.dev/docs/kit/adapters for more information about adapters. - adapter: adapter() - } -}; - -export default config; diff --git a/packages/sv/src/create/shared/+typescript/svelte.config.js b/packages/sv/src/create/shared/+typescript/svelte.config.js deleted file mode 100644 index 0c3412e9f..000000000 --- a/packages/sv/src/create/shared/+typescript/svelte.config.js +++ /dev/null @@ -1,17 +0,0 @@ -import adapter from '@sveltejs/adapter-auto'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - compilerOptions: { - // Force runes mode for the project, except for libraries. Can be removed in svelte 6. - runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true) - }, - kit: { - // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://svelte.dev/docs/kit/adapters for more information about adapters. - adapter: adapter() - } -}; - -export default config; diff --git a/packages/sv/src/create/shared/-typescript/svelte.config.js b/packages/sv/src/create/shared/-typescript/svelte.config.js deleted file mode 100644 index 0c3412e9f..000000000 --- a/packages/sv/src/create/shared/-typescript/svelte.config.js +++ /dev/null @@ -1,17 +0,0 @@ -import adapter from '@sveltejs/adapter-auto'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - compilerOptions: { - // Force runes mode for the project, except for libraries. Can be removed in svelte 6. - runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true) - }, - kit: { - // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://svelte.dev/docs/kit/adapters for more information about adapters. - adapter: adapter() - } -}; - -export default config; diff --git a/packages/sv/src/create/shared/vite.config.js b/packages/sv/src/create/shared/vite.config.js new file mode 100644 index 000000000..cb76b810c --- /dev/null +++ b/packages/sv/src/create/shared/vite.config.js @@ -0,0 +1,20 @@ +import adapter from '@sveltejs/adapter-auto'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + compilerOptions: { + // Force runes mode for the project, except for libraries. Can be removed in svelte 6. + runes: ({ filename }) => + filename.split(/[/\\]/).includes('node_modules') ? undefined : true + }, + + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter() + }) + ] +}); diff --git a/packages/sv/src/create/shared/vite.config.ts b/packages/sv/src/create/shared/vite.config.ts deleted file mode 100644 index bbf8c7da4..000000000 --- a/packages/sv/src/create/shared/vite.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - plugins: [sveltekit()] -}); diff --git a/packages/sv/src/create/templates/demo/svelte.config.js b/packages/sv/src/create/templates/demo/svelte.config.js deleted file mode 100644 index 7b52576cd..000000000 --- a/packages/sv/src/create/templates/demo/svelte.config.js +++ /dev/null @@ -1,12 +0,0 @@ -import adapter from '@sveltejs/adapter-auto'; - -// This config is ignored and replaced with one of the configs in the shared folder when a project is created. - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter() - } -}; - -export default config; diff --git a/packages/sv/src/create/templates/demo/vite.config.js b/packages/sv/src/create/templates/demo/vite.config.js index 218b4759e..34aae435c 100644 --- a/packages/sv/src/create/templates/demo/vite.config.js +++ b/packages/sv/src/create/templates/demo/vite.config.js @@ -1,9 +1,14 @@ +import adapter from '@sveltejs/adapter-auto'; import { sveltekit } from '@sveltejs/kit/vite'; import path from 'node:path'; import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [sveltekit()], + plugins: [ + sveltekit({ + adapter: adapter() + }) + ], server: { fs: { diff --git a/packages/sv/src/create/templates/library/svelte.config.js b/packages/sv/src/create/templates/library/svelte.config.js deleted file mode 100644 index a894776b5..000000000 --- a/packages/sv/src/create/templates/library/svelte.config.js +++ /dev/null @@ -1,12 +0,0 @@ -import adapter from '@sveltejs/adapter-auto'; - -// This config is ignored and replaced with one of the configs in the shared folder when a project is created. - -/** @type {import('@sveltejs/package').Config} */ -const config = { - kit: { - adapter: adapter() - } -}; - -export default config; diff --git a/packages/sv/src/create/templates/library/vite.config.js b/packages/sv/src/create/templates/library/vite.config.js index bbf8c7da4..83ba361b7 100644 --- a/packages/sv/src/create/templates/library/vite.config.js +++ b/packages/sv/src/create/templates/library/vite.config.js @@ -1,6 +1,11 @@ +import adapter from '@sveltejs/adapter-auto'; import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [sveltekit()] + plugins: [ + sveltekit({ + adapter: adapter() + }) + ] }); diff --git a/packages/sv/src/create/templates/minimal/svelte.config.js b/packages/sv/src/create/templates/minimal/svelte.config.js deleted file mode 100644 index 7b52576cd..000000000 --- a/packages/sv/src/create/templates/minimal/svelte.config.js +++ /dev/null @@ -1,12 +0,0 @@ -import adapter from '@sveltejs/adapter-auto'; - -// This config is ignored and replaced with one of the configs in the shared folder when a project is created. - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter() - } -}; - -export default config; diff --git a/packages/sv/src/create/templates/minimal/vite.config.js b/packages/sv/src/create/templates/minimal/vite.config.js index bbf8c7da4..83ba361b7 100644 --- a/packages/sv/src/create/templates/minimal/vite.config.js +++ b/packages/sv/src/create/templates/minimal/vite.config.js @@ -1,6 +1,11 @@ +import adapter from '@sveltejs/adapter-auto'; import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [sveltekit()] + plugins: [ + sveltekit({ + adapter: adapter() + }) + ] }); From 02e5095a86174d9fdb60799dac0d0c619e2fc40b Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 6 Jun 2026 09:26:04 +0200 Subject: [PATCH 02/10] minimumReleaseAge: 1440 --- pnpm-workspace.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index eb65c71f1..d244eccf2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,4 @@ -minimumReleaseAge: 2880 +minimumReleaseAge: 1440 minimumReleaseAgeExclude: - '@sveltejs/*' - svelte From 664030f5c2d2116a8bc9be5c03c8bd4ed9d4dff3 Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 6 Jun 2026 10:10:00 +0200 Subject: [PATCH 03/10] feat(sv-utils): add svelteConfig helper for editing the svelte config Reads/edits config wherever it lives - a svelte.config.{js,ts} default export or the object passed to sveltekit() in vite.config.{js,ts}. --- .changeset/svelte-config-in-vite.md | 6 + documentation/docs/50-api/20-sv-utils.md | 41 ++++ packages/sv-utils/api-surface.md | 75 +++++- packages/sv-utils/src/index.ts | 9 + packages/sv-utils/src/svelte-config.ts | 228 ++++++++++++++++++ packages/sv-utils/src/tests/svelte-config.ts | 217 +++++++++++++++++ .../sv-utils/src/tooling/js/svelte-config.ts | 138 +++++++++++ 7 files changed, 709 insertions(+), 5 deletions(-) create mode 100644 .changeset/svelte-config-in-vite.md create mode 100644 packages/sv-utils/src/svelte-config.ts create mode 100644 packages/sv-utils/src/tests/svelte-config.ts create mode 100644 packages/sv-utils/src/tooling/js/svelte-config.ts diff --git a/.changeset/svelte-config-in-vite.md b/.changeset/svelte-config-in-vite.md new file mode 100644 index 000000000..56ba713b7 --- /dev/null +++ b/.changeset/svelte-config-in-vite.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/sv-utils': minor +'sv': minor +--- + +Add the `svelteConfig` helper and read/edit the svelte config wherever it lives - a `svelte.config.{js,ts}` default export or the object passed to `sveltekit()` in `vite.config.{js,ts}` diff --git a/documentation/docs/50-api/20-sv-utils.md b/documentation/docs/50-api/20-sv-utils.md index 062863068..39e7e7a84 100644 --- a/documentation/docs/50-api/20-sv-utils.md +++ b/documentation/docs/50-api/20-sv-utils.md @@ -230,6 +230,47 @@ Namespaced helpers for AST manipulation: - **`html.*`** - attribute manipulation - **`text.*`** - upsert lines in flat files (.env, .gitignore) +## Svelte config + +A SvelteKit project's config can live in two places: the default export of a `svelte.config.{js,ts}`, or the object passed to `sveltekit()` in a `vite.config.{js,ts}`. `svelteConf` edits it wherever it is, so add-ons don't have to care which. + +### `svelteConf` + +Locates the config and hands your callback two object expressions to edit: + +- **`config`** - the svelte-level config (`preprocess`, `extensions`, `compilerOptions`, `vitePlugin`). +- **`kit`** - the kit-level config (`adapter`, `alias`, `files`, `typescript`, …). + +In a `svelte.config.js`, `kit` is the nested `kit: { … }` object. In a `vite.config.js`, `sveltekit()` takes a flattened `KitConfig & SvelteConfig`, so `config` and `kit` point at the same object. You write the same code either way: + +```js +// @noErrors +import { svelteConf } from '@sveltejs/sv-utils'; + +// inside an add-on's `run({ sv, cwd })`: +svelteConf({ sv, cwd }, ({ ast, config, kit, js }) => { + // svelte-level option: + js.array.append( + js.object.property(config, { name: 'extensions', fallback: js.array.create() }), + '.svx' + ); + // kit-level option (nested under `kit` in svelte.config, top-level in vite.config): + js.imports.addDefault(ast, { from: '@sveltejs/adapter-node', as: 'adapter' }); + js.object.overrideProperties(kit, { + adapter: js.functions.createCall({ name: 'adapter', args: [], useIdentifiers: true }) + }); +}); +``` + +It writes through `sv.file`, so the edit is tracked like any other. It throws if the project has no config in either location. + +### `findSvelteConfig` / `getSvelteConfigObjects` + +Lower-level building blocks if you need them outside an edit: + +- **`findSvelteConfig(read)`** - returns `{ path, kind: 'svelte' | 'vite' }` (or `null`), where `read(path)` returns a file's contents or `null`. Detection is static (it never executes the config), and `svelte.config` wins when both are present. +- **`getSvelteConfigObjects(ast, kind)`** - given a parsed program and the `kind` from `findSvelteConfig`, returns the `{ config, kit }` object expressions. + ## Package manager helpers ### `pnpm.allowBuilds` diff --git a/packages/sv-utils/api-surface.md b/packages/sv-utils/api-surface.md index 317b5c1cf..3abf862f5 100644 --- a/packages/sv-utils/api-surface.md +++ b/packages/sv-utils/api-surface.md @@ -287,7 +287,7 @@ declare namespace object_d_exports { } type ObjectPrimitiveValues = string | number | boolean | undefined | null; type ObjectValues = ObjectPrimitiveValues | Record | ObjectValues[]; -type ObjectMap = Record; +type ObjectMap$1 = Record; declare function property( node: estree.ObjectExpression, options: { @@ -302,10 +302,10 @@ declare function propertyNode( fallback: T; } ): estree.Property; -declare function create(properties: ObjectMap): estree.ObjectExpression; +declare function create(properties: ObjectMap$1): estree.ObjectExpression; declare function overrideProperties( objectExpression: estree.ObjectExpression, - properties: ObjectMap + properties: ObjectMap$1 ): void; declare namespace common_d_exports { export { @@ -411,7 +411,7 @@ declare function getArgument( } ): T; declare namespace imports_d_exports { - export { addDefault, addEmpty, addNamed, addNamespace$1 as addNamespace, find, remove }; + export { addDefault, addEmpty, addNamed, addNamespace$1 as addNamespace, find$1 as find, remove }; } declare function addEmpty( node: estree.Program, @@ -441,7 +441,7 @@ declare function addNamed( isType?: boolean; } ): void; -declare function find( +declare function find$1( ast: estree.Program, options: { name: string; @@ -772,6 +772,66 @@ declare function loadPackageJson(cwd: string): { source: string; data: Package; }; + +type SvelteConfigKind = 'svelte' | 'vite'; +type SvelteConfigLocation = { + path: string; + kind: SvelteConfigKind; +}; + +type SvelteConfigObjects = { + location: SvelteConfigLocation; + config: estree.ObjectExpression; + kit: estree.ObjectExpression; +}; + +type ConfigFileReader = (path: string) => string | null; +type ObjectMap = Parameters[1]; + +declare function find(read: ConfigFileReader): SvelteConfigLocation | null; + +declare function read(readFile: ConfigFileReader): SvelteConfigObjects | null; +type SvelteConfEdit = (file: { + ast: estree.Program; + comments: Comments; + js: typeof index_d_exports$3; + location: SvelteConfigLocation; + + property: ( + name: string, + opts: { + fallback: T; + } + ) => T; + + override: ( + props: ObjectMap, + opts?: { + dropLeadingComments?: string[]; + } + ) => void; +}) => void | false; + +type SvFileApi = { + file: (path: string, edit: (content: string) => string | false) => void; +}; + +declare function edit( + { + sv, + cwd + }: { + sv: SvFileApi; + cwd: string; + }, + editFn: SvelteConfEdit +): void; + +declare const svelteConfig: { + edit: typeof edit; + find: typeof find; + read: typeof read; +}; type ColorInput = string | string[]; declare const color: { addon: (str: ColorInput) => string; @@ -803,8 +863,12 @@ export { type estree as AstTypes, COMMANDS, type Comments, + type ConfigFileReader, type Package, type SvelteAst, + type SvelteConfigKind, + type SvelteConfigLocation, + type SvelteConfigObjects, type TransformFn, index_d_exports as Walker, type YamlDocument, @@ -832,6 +896,7 @@ export { saveFile, splitVersion, index_d_exports$4 as svelte, + svelteConfig, text_d_exports as text, transforms }; diff --git a/packages/sv-utils/src/index.ts b/packages/sv-utils/src/index.ts index be4b18b60..b43817a8a 100644 --- a/packages/sv-utils/src/index.ts +++ b/packages/sv-utils/src/index.ts @@ -75,6 +75,15 @@ export { downloadJson } from './downloadJson.ts'; // File system helpers (sync, workspace-relative paths) export { fileExists, loadFile, loadPackageJson, saveFile, type Package } from './files.ts'; +// Svelte/kit config (abstracts over `svelte.config.{js,ts}` vs `sveltekit()` in `vite.config.{js,ts}`) +export { + svelteConfig, + type ConfigFileReader, + type SvelteConfigKind, + type SvelteConfigLocation, + type SvelteConfigObjects +} from './svelte-config.ts'; + // Terminal styling export { color } from './color.ts'; diff --git a/packages/sv-utils/src/svelte-config.ts b/packages/sv-utils/src/svelte-config.ts new file mode 100644 index 000000000..fe06e1bc0 --- /dev/null +++ b/packages/sv-utils/src/svelte-config.ts @@ -0,0 +1,228 @@ +import { fileExists, loadFile } from './files.ts'; +import type { AstTypes, Comments } from './tooling/index.ts'; +import * as jsNs from './tooling/js/index.ts'; +import { + findSveltekitCall, + getConfigRoot, + getKitObject, + hasDefaultExport, + type SvelteConfigKind +} from './tooling/js/svelte-config.ts'; +import { parseScript } from './tooling/parsers.ts'; +import { transforms } from './tooling/transforms.ts'; + +export type { SvelteConfigKind } from './tooling/js/svelte-config.ts'; + +export type SvelteConfigLocation = { + /** path relative to the workspace root, e.g. `vite.config.ts` or `svelte.config.js` */ + path: string; + kind: SvelteConfigKind; +}; + +/** The located config plus its resolved object expressions (returned by `read`). */ +export type SvelteConfigObjects = { + location: SvelteConfigLocation; + /** svelte-level config object (`preprocess`, `extensions`, `compilerOptions`, `vitePlugin`). */ + config: AstTypes.ObjectExpression; + /** kit-level config object (`adapter`, `alias`, `files`, `typescript`, ...). */ + kit: AstTypes.ObjectExpression; +}; + +/** Reads a workspace file. Returns `null` when the file doesn't exist. (the injected environment) */ +export type ConfigFileReader = (path: string) => string | null; + +type ObjectMap = Parameters[1]; + +/** + * The top-level options that live on the svelte config object itself. Everything else (`adapter`, + * `alias`, `files`, `typescript`, ...) is a kit-level option, which sits under `kit` in a + * `svelte.config` but flattened onto the `sveltekit()` argument in a `vite.config`. Callers address + * options by name and `edit` routes them to the right place, so they never have to know about `kit`. + */ +const SVELTE_LEVEL_OPTIONS = new Set([ + 'compilerOptions', + 'preprocess', + 'extensions', + 'vitePlugin', + 'onwarn' +]); + +// `svelte.config` is checked first so legacy projects (where the real config lives there) win +// even if a `vite.config` with a bare `sveltekit()` call is also present. +const SVELTE_CANDIDATES = ['svelte.config.js', 'svelte.config.ts'] as const; +const VITE_CANDIDATES = ['vite.config.ts', 'vite.config.js'] as const; + +function tryParse(read: ConfigFileReader, path: string): AstTypes.Program | undefined { + const source = read(path); + if (source === null) return undefined; + try { + return parseScript(source).ast; + } catch { + return undefined; + } +} + +/** Detects the config location AND keeps the parsed AST, so callers don't have to parse twice. */ +function locate( + read: ConfigFileReader +): { location: SvelteConfigLocation; ast: AstTypes.Program } | null { + for (const path of SVELTE_CANDIDATES) { + const ast = tryParse(read, path); + if (ast && hasDefaultExport(ast)) return { location: { path, kind: 'svelte' }, ast }; + } + for (const path of VITE_CANDIDATES) { + const ast = tryParse(read, path); + if (ast && findSveltekitCall(ast)) return { location: { path, kind: 'vite' }, ast }; + } + return null; +} + +/** + * Detects where the svelte/kit config lives, reading candidate files through the injected `read`. + * + * Returns `null` when no config could be found (e.g. not a SvelteKit project, or the config + * file is unparsable). Detection is static - the config is never executed. + */ +function find(read: ConfigFileReader): SvelteConfigLocation | null { + return locate(read)?.location ?? null; +} + +/** + * Locates the config and returns its `{ location, config, kit }` object expressions in a single + * parse. Returns `null` when no config is found. Throws if the located config has an unexpected + * shape (e.g. a non-object default export). + */ +function read(readFile: ConfigFileReader): SvelteConfigObjects | null { + const found = locate(readFile); + if (!found) return null; + const config = getConfigRoot(found.ast, found.location.kind); + const kit = getKitObject(config, found.location.kind); + return { location: found.location, config, kit }; +} + +export type SvelteConfEdit = (file: { + ast: AstTypes.Program; + comments: Comments; + js: typeof jsNs; + location: SvelteConfigLocation; + /** + * Get-or-create a top-level config option's value, placed in the correct location for its name + * (kit-level options end up under `kit` in a `svelte.config`, flattened in a `vite.config`). + */ + property: ( + name: string, + opts: { fallback: T } + ) => T; + /** + * Set/override top-level config options, each routed to the correct location by its name. + * Pass `dropLeadingComments` with option names whose now-stale leading comments should be removed + * (e.g. the adapter-auto note when switching adapters). + */ + override: (props: ObjectMap, opts?: { dropLeadingComments?: string[] }) => void; +}) => void | false; + +/** Minimal shape of the `sv` api needed to write the config file. */ +type SvFileApi = { + file: (path: string, edit: (content: string) => string | false) => void; +}; + +/** Removes comments sitting between `name`'s property and its previous sibling (its leading note). */ +function dropLeadingComments( + container: AstTypes.ObjectExpression, + name: string, + comments: Comments +): void { + const prop = container.properties.find( + (p): p is AstTypes.Property => + p.type === 'Property' && p.key.type === 'Identifier' && p.key.name === name + ); + const start = prop?.loc?.start.line; + if (start === undefined) return; + + let lowerBound = container.loc?.start.line ?? 0; + for (const p of container.properties) { + if (p === prop) continue; + const end = p.loc?.end.line; + if (end !== undefined && end < start && end > lowerBound) lowerBound = end; + } + comments.remove((c) => !!c.loc && c.loc.start.line > lowerBound && c.loc.end.line < start); +} + +/** The environment-free core of `edit` - parse `content`, apply `editFn`, return the new source. */ +function editContent( + content: string, + location: SvelteConfigLocation, + editFn: SvelteConfEdit +): string { + return transforms.script(({ ast, comments, js }) => { + const config = getConfigRoot(ast, location.kind); + // the `kit` object is only materialized when a kit-level option is actually edited, so a + // svelte-only edit (e.g. mdsvex) doesn't leave a spurious empty `kit: {}` behind + let kit: AstTypes.ObjectExpression | undefined; + const kitObject = () => (kit ??= getKitObject(config, location.kind)); + const containerFor = (name: string) => (SVELTE_LEVEL_OPTIONS.has(name) ? config : kitObject()); + + const property: Parameters[0]['property'] = (name, opts) => + js.object.property(containerFor(name), { name, ...opts }); + + const override: Parameters[0]['override'] = (props, opts) => { + const svelteProps: ObjectMap = {}; + const kitProps: ObjectMap = {}; + for (const [key, value] of Object.entries(props)) { + (SVELTE_LEVEL_OPTIONS.has(key) ? svelteProps : kitProps)[key] = value; + } + if (Object.keys(svelteProps).length) js.object.overrideProperties(config, svelteProps); + if (Object.keys(kitProps).length) js.object.overrideProperties(kitObject(), kitProps); + for (const name of opts?.dropLeadingComments ?? []) { + dropLeadingComments(containerFor(name), name, comments); + } + }; + + return editFn({ ast, comments, js, location, property, override }); + })(content); +} + +/** + * Edits the svelte/kit config wherever it lives, abstracting over the two possible locations + * (a `svelte.config.{js,ts}` default export, or the object passed to `sveltekit()` in a + * `vite.config.{js,ts}`). When the project has neither, a new `svelte.config.js` is created. + * + * Options are addressed by name and routed to the right place internally, so add-ons never have to + * know whether something lives under `kit`: + * + * @example + * ```ts + * svelteConfig.edit({ sv, cwd }, ({ override, js }) => { + * js.imports.addDefault(ast, { from: '@sveltejs/adapter-node', as: 'adapter' }); + * override({ adapter: js.functions.createCall({ name: 'adapter', args: [], useIdentifiers: true }) }); + * }); + * ``` + */ +function edit({ sv, cwd }: { sv: SvFileApi; cwd: string }, editFn: SvelteConfEdit): void { + const location = find((p) => (fileExists(cwd, p) ? loadFile(cwd, p) : null)) ?? { + path: 'svelte.config.js', + kind: 'svelte' + }; + + sv.file(location.path, (content) => editContent(content, location, editFn)); +} + +/** + * Helpers for the svelte/kit config, which can live either in a `svelte.config.{js,ts}` default + * export or in the object passed to `sveltekit()` in a `vite.config.{js,ts}`. + */ +export const svelteConfig: { + /** Edit the config wherever it lives (creating `svelte.config.js` if there is none). */ + edit: typeof edit; + /** Locate the config file, returning `{ path, kind }` or `null`. Detection is static (no execution). */ + find: typeof find; + /** Locate + parse the config in one pass, returning `{ location, config, kit }` or `null`. */ + read: typeof read; +} = { + edit, + find, + read +}; + +/** @internal exported for tests - the environment-free core of `svelteConfig.edit`. */ +export { editContent as _editConfigContent }; diff --git a/packages/sv-utils/src/tests/svelte-config.ts b/packages/sv-utils/src/tests/svelte-config.ts new file mode 100644 index 000000000..0957f2146 --- /dev/null +++ b/packages/sv-utils/src/tests/svelte-config.ts @@ -0,0 +1,217 @@ +import { describe, expect, test } from 'vitest'; +import { + _editConfigContent, + svelteConfig, + type SvelteConfEdit, + type SvelteConfigKind +} from '../svelte-config.ts'; + +/** An in-memory reader over a plain file map - no filesystem involved. */ +const reader = (files: Record) => (path: string) => files[path] ?? null; + +/** Runs an edit against `content` for a given location kind, returning the serialized result. */ +const applyEdit = (content: string, kind: SvelteConfigKind, edit: SvelteConfEdit) => + _editConfigContent(content, { path: '', kind }, edit); + +const addAlias: SvelteConfEdit = ({ override, js }) => + override({ alias: js.object.create({ $lib: js.common.createLiteral('./src/lib') }) }); +const addExtension: SvelteConfEdit = ({ property, js }) => + js.array.append(property('extensions', { fallback: js.array.create() }), '.svx'); + +const VITE_CONFIG = `import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +}); +`; + +const VITE_CONFIG_TS = `import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit({ adapter: adapter() })] +}) satisfies UserConfig; +`; + +const VITE_CONFIG_ALIASED = `import { sveltekit as youhou } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig(({ mode }) => ({ + plugins: [youhou({ /** stuff */ })] +})); +`; + +// a stray sveltekit() in dead code BEFORE the real one in the export's plugins array +const VITE_CONFIG_TWO_CALLS = `import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +const unused = { plugins: [sveltekit()] }; + +export default defineConfig({ + plugins: [sveltekit({ adapter: adapter() })] +}); +`; + +const SVELTE_CONFIG = `import adapter from '@sveltejs/adapter-auto'; + +const config = { + kit: { + adapter: adapter() + } +}; + +export default config; +`; + +const SVELTE_CONFIG_TS = `export default { + kit: {} +}; +`; + +const SVELTE_CONFIG_SATISFIES = `import type { Config } from '@sveltejs/kit'; + +export default { + kit: {} +} satisfies Config; +`; + +const SVELTE_CONFIG_AS = `export default { kit: {} } as Config;`; + +const SVELTE_CONFIG_BAD = `export default function () {};`; + +describe('svelteConfig.find', () => { + test('detects config in vite.config.js via sveltekit() call', () => { + expect(svelteConfig.find(reader({ 'vite.config.js': VITE_CONFIG }))).toEqual({ + path: 'vite.config.js', + kind: 'vite' + }); + }); + + test('detects config in svelte.config.js via default export', () => { + expect(svelteConfig.find(reader({ 'svelte.config.js': SVELTE_CONFIG }))).toEqual({ + path: 'svelte.config.js', + kind: 'svelte' + }); + }); + + test('detects the .ts variants', () => { + expect(svelteConfig.find(reader({ 'svelte.config.ts': SVELTE_CONFIG_TS }))).toEqual({ + path: 'svelte.config.ts', + kind: 'svelte' + }); + expect(svelteConfig.find(reader({ 'vite.config.ts': VITE_CONFIG_TS }))).toEqual({ + path: 'vite.config.ts', + kind: 'vite' + }); + }); + + test('prefers svelte.config even when a bare sveltekit() vite config is present', () => { + const read = reader({ 'svelte.config.js': SVELTE_CONFIG, 'vite.config.js': VITE_CONFIG }); + expect(svelteConfig.find(read)?.kind).toBe('svelte'); + }); + + test('detects an aliased sveltekit() inside a defineConfig arrow function', () => { + expect(svelteConfig.find(reader({ 'vite.config.js': VITE_CONFIG_ALIASED }))).toEqual({ + path: 'vite.config.js', + kind: 'vite' + }); + }); + + test('returns null when no config is present', () => { + expect(svelteConfig.find(reader({ 'package.json': '{}' }))).toBe(null); + }); +}); + +describe('svelteConfig.read', () => { + test('returns the flattened object for a vite config (config === kit)', () => { + const result = svelteConfig.read(reader({ 'vite.config.js': VITE_CONFIG })); + expect(result?.location).toEqual({ path: 'vite.config.js', kind: 'vite' }); + expect(result?.config).toBe(result?.kit); + }); + + test('returns distinct config/kit for a svelte config', () => { + const result = svelteConfig.read(reader({ 'svelte.config.js': SVELTE_CONFIG })); + expect(result?.location.kind).toBe('svelte'); + expect(result?.config).not.toBe(result?.kit); + }); + + test('returns null when no config is present', () => { + expect(svelteConfig.read(reader({ 'package.json': '{}' }))).toBe(null); + }); +}); + +describe('svelteConfig.edit routing', () => { + test('routes svelte-level options to the config root (vite location)', () => { + const result = applyEdit(VITE_CONFIG, 'vite', addExtension); + expect(result).toContain('sveltekit({'); + expect(result).toContain("extensions: ['.svx']"); + }); + + test('flattens kit-level options onto the sveltekit() argument (vite location)', () => { + const result = applyEdit(VITE_CONFIG, 'vite', addAlias); + expect(result).toContain('alias:'); + expect(result).not.toContain('kit:'); + }); + + test('nests kit-level options under kit: (svelte.config location)', () => { + const result = applyEdit(SVELTE_CONFIG, 'svelte', addAlias); + expect(result).toContain('kit:'); + expect(result).toContain('alias:'); + }); + + test('routes mixed svelte-level + kit-level keys in one override() call', () => { + const result = applyEdit(SVELTE_CONFIG, 'svelte', ({ override, js }) => { + override({ + extensions: js.array.create(), + alias: js.object.create({ $lib: js.common.createLiteral('./src/lib') }) + }); + }); + // `extensions` is svelte-level (root), `alias` is kit-level (under kit) + expect(result).toMatch(/extensions:/); + expect(result).toMatch(/kit:[\s\S]*alias:/); + // alias must NOT be a root-level sibling of kit + expect(result.indexOf('alias:')).toBeGreaterThan(result.indexOf('kit:')); + }); + + test('edits an aliased sveltekit() in an arrow defineConfig', () => { + const result = applyEdit(VITE_CONFIG_ALIASED, 'vite', ({ override, js }) => { + override({ + adapter: js.functions.createCall({ name: 'adapter', args: [], useIdentifiers: true }) + }); + }); + expect(result).toContain('youhou({'); + expect(result).toContain('adapter: adapter()'); + }); + + test('edits the sveltekit() in the exported plugins, not a stray call', () => { + const result = applyEdit(VITE_CONFIG_TWO_CALLS, 'vite', addAlias); + // the alias must land in the exported config, after `const unused`, not in the dead const + expect(result.indexOf('alias:')).toBeGreaterThan(result.indexOf('export default')); + // the unused const stays a bare sveltekit() + expect(result).toMatch(/const unused = \{ plugins: \[sveltekit\(\)\] \}/); + }); + + test('creates the default export when editing empty content', () => { + const result = applyEdit('', 'svelte', addExtension); + expect(result).toContain('export default'); + expect(result).toContain("extensions: ['.svx']"); + }); +}); + +describe('svelteConfig.edit shapes', () => { + test('handles `satisfies` config', () => { + const result = applyEdit(SVELTE_CONFIG_SATISFIES, 'svelte', addAlias); + expect(result).toContain('alias:'); + expect(result).toContain('satisfies Config'); + }); + + test('handles `as` config', () => { + const result = applyEdit(SVELTE_CONFIG_AS, 'svelte', addAlias); + expect(result).toContain('alias:'); + }); + + test('throws a clear error for a non-object default export', () => { + expect(() => applyEdit(SVELTE_CONFIG_BAD, 'svelte', addAlias)).toThrow('object literal'); + }); +}); diff --git a/packages/sv-utils/src/tooling/js/svelte-config.ts b/packages/sv-utils/src/tooling/js/svelte-config.ts new file mode 100644 index 000000000..f279b9fcb --- /dev/null +++ b/packages/sv-utils/src/tooling/js/svelte-config.ts @@ -0,0 +1,138 @@ +import * as Walker from 'zimmerframe'; +import type { AstTypes } from '../index.ts'; +import * as array from './array.ts'; +import * as exports from './exports.ts'; +import * as object from './object.ts'; +import * as vite from './vite.ts'; + +/** + * Where the svelte/kit config lives: + * - `svelte`: a `svelte.config.{js,ts}` with `export default { ...svelteOptions, kit: { ...kitOptions } }` + * - `vite`: a `vite.config.{js,ts}` with the config passed to `sveltekit({ ...svelteOptions, ...kitOptions })` + * + * In the `vite` shape kit options are flattened onto the same object as the svelte options + * (it accepts `KitConfig & SvelteConfig`), which is why `config` and `kit` point at the same + * object expression there. + */ +export type SvelteConfigKind = 'svelte' | 'vite'; + +/** Does the program have a default export? (used to detect a `svelte.config` config). */ +export function hasDefaultExport(ast: AstTypes.Program): boolean { + return ast.body.some((node) => node.type === 'ExportDefaultDeclaration'); +} + +/** + * Resolves the local name `sveltekit` is imported as from `@sveltejs/kit/vite`, honouring aliases + * (e.g. `import { sveltekit as youhou } from '@sveltejs/kit/vite'` -> `youhou`). + * Returns `'sveltekit'` when no such import is found. + */ +function sveltekitLocalName(ast: AstTypes.Program): string { + for (const node of ast.body) { + if (node.type !== 'ImportDeclaration' || node.source.value !== '@sveltejs/kit/vite') continue; + for (const spec of node.specifiers ?? []) { + if ( + spec.type === 'ImportSpecifier' && + spec.imported.type === 'Identifier' && + spec.imported.name === 'sveltekit' + ) { + return spec.local.name; + } + } + } + return 'sveltekit'; +} + +/** Finds the `sveltekit(...)` plugin call anywhere in the program (used to detect a `vite.config` config). */ +export function findSveltekitCall(ast: AstTypes.Program): AstTypes.CallExpression | undefined { + const name = sveltekitLocalName(ast); + let call: AstTypes.CallExpression | undefined; + Walker.walk(ast as AstTypes.Node, null, { + CallExpression(node, { next }) { + if (node.callee.type === 'Identifier' && node.callee.name === name) { + call ??= node; + } + next(); + } + }); + return call; +} + +/** Unwraps `... satisfies T` / `... as T` and asserts the result is an object literal. */ +function asObjectExpression(value: AstTypes.Expression): AstTypes.ObjectExpression { + let node: AstTypes.Expression = value; + while (node.type === 'TSSatisfiesExpression' || node.type === 'TSAsExpression') { + node = node.expression; + } + if (node.type !== 'ObjectExpression') { + throw new Error( + 'Expected the svelte config default export to be an object literal (e.g. `export default { ... }`)' + ); + } + return node; +} + +/** + * Returns (creating if needed) the object argument passed to `sveltekit(...)`. + * + * Reuses `vite.getConfig` so `defineConfig(...)` wrappers, arrow-function configs and + * `satisfies`/`as` are handled, and scopes the lookup to the config's `plugins` array so the + * real plugin call is edited rather than a stray `sveltekit()` elsewhere in the file. + */ +function sveltekitArg(ast: AstTypes.Program): AstTypes.ObjectExpression { + const name = sveltekitLocalName(ast); + + const viteConfig = vite.getConfig(ast); + const plugins = vite.configProperty(ast, viteConfig, { + name: 'plugins', + fallback: array.create() + }); + + let call: AstTypes.CallExpression | undefined; + if (plugins.type === 'ArrayExpression') { + call = plugins.elements.find( + (el): el is AstTypes.CallExpression => + el?.type === 'CallExpression' && el.callee.type === 'Identifier' && el.callee.name === name + ); + } + // fall back to a broad search (e.g. plugins assembled via a variable or spread) + call ??= findSveltekitCall(ast); + if (!call) { + throw new Error('Unable to find a `sveltekit()` plugin call in the vite config'); + } + + let arg = call.arguments[0] as AstTypes.Expression | undefined; + if (!arg || arg.type !== 'ObjectExpression') { + arg = object.create({}); + call.arguments[0] = arg; + } + return arg as AstTypes.ObjectExpression; +} + +/** + * Resolves the svelte-level config object (the "root"): + * - `svelte`: the default-exported object (created on demand if missing). + * - `vite`: the object passed to `sveltekit(...)`. + */ +export function getConfigRoot( + ast: AstTypes.Program, + kind: SvelteConfigKind +): AstTypes.ObjectExpression { + if (kind === 'svelte') { + const { value } = exports.createDefault(ast, { fallback: object.create({}) }); + return asObjectExpression(value); + } + return sveltekitArg(ast); +} + +/** + * Resolves the kit-level config object from the root (created on demand if missing): + * - `svelte`: the nested `kit` object. + * - `vite`: the root itself (kit options are flattened onto the `sveltekit()` argument). + */ +export function getKitObject( + root: AstTypes.ObjectExpression, + kind: SvelteConfigKind +): AstTypes.ObjectExpression { + if (kind === 'vite') return root; + return object.property(root, { name: 'kit', fallback: object.create({}) }); +} From 05ff92b19b461f28f3c823a73dbc8665eb7d2caf Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 6 Jun 2026 11:20:00 +0200 Subject: [PATCH 04/10] feat(sv): read kit config from svelte.config or vite.config parseKitOptions now locates the config in either place and falls back to defaults instead of throwing. Deprecates file.svelteConfig in favour of the svelteConfig helper. --- packages/sv/src/core/workspace.ts | 132 +++++++++++++++--------------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/packages/sv/src/core/workspace.ts b/packages/sv/src/core/workspace.ts index 30ec2a422..ee64af85e 100644 --- a/packages/sv/src/core/workspace.ts +++ b/packages/sv/src/core/workspace.ts @@ -1,11 +1,11 @@ import { type AgentName, - type AstTypes, js, - parse, loadFile, + fileExists, loadPackageJson, - minVersion + minVersion, + svelteConfig } from '@sveltejs/sv-utils'; import * as find from 'empathic/find'; import fs from 'node:fs'; @@ -32,6 +32,11 @@ export type Workspace = { language: 'ts' | 'js'; file: { viteConfig: 'vite.config.js' | 'vite.config.ts'; + /** + * @deprecated the config no longer necessarily lives in `svelte.config.{js,ts}` (it can be + * passed to `sveltekit()` in `vite.config.{js,ts}`). Use `svelteConfig` from + * `@sveltejs/sv-utils` to edit it wherever it lives. + */ svelteConfig: 'svelte.config.js' | 'svelte.config.ts'; typeConfig: 'jsconfig.json' | 'tsconfig.json' | undefined; /** `${directory.routes}/layout.css` or `src/app.css` */ @@ -93,7 +98,8 @@ const deprecatedFiles = { * Once we remove these deprecatedFiles, we can get rid of addDeprecatedFileProperties */ function addDeprecatedFileProperties( - file: Omit + file: Omit, + svelteConfig: Workspace['file']['svelteConfig'] ): Workspace['file'] { for (const [key, value] of Object.entries(deprecatedFiles)) { Object.defineProperty(file, key, { @@ -104,6 +110,17 @@ function addDeprecatedFileProperties( enumerable: false }); } + // `svelteConfig` is dynamic (`.ts` vs `.js`) and points to a file that may not exist anymore, + // so it gets its own getter pointing at `svelteConfig` rather than a static "use the string" hint. + Object.defineProperty(file, 'svelteConfig', { + get() { + svDeprecated( + 'use `svelteConfig` from `@sveltejs/sv-utils` instead of `file.svelteConfig` (the config may live in `vite.config.{js,ts}`)' + ); + return svelteConfig; + }, + enumerable: false + }); return file as Workspace['file']; } @@ -166,7 +183,7 @@ export async function createWorkspace({ const directory = override?.directory ? override.directory : isKit - ? { src: 'src', ...parseKitOptions(resolvedCwd, svelteConfig) } + ? { src: 'src', ...parseKitOptions(resolvedCwd) } : { src: 'src', lib: 'src/lib', kitRoutes: 'src/routes' }; const stylesheet: `${string}/layout.css` | 'src/app.css' = isKit @@ -177,32 +194,34 @@ export async function createWorkspace({ cwd: resolvedCwd, packageManager: packageManager ?? (await detectPackageManager(cwd)), language: typescript ? 'ts' : 'js', - file: addDeprecatedFileProperties({ - viteConfig, - svelteConfig, - typeConfig, - stylesheet, - package: 'package.json', - gitignore: '.gitignore', - getRelative({ from, to }) { - from = from ?? ''; - let relativePath = path.posix.relative(path.posix.dirname(from), to); - // Ensure relative paths start with ./ for proper relative path syntax - if (!relativePath.startsWith('.') && !relativePath.startsWith('/')) { - relativePath = `./${relativePath}`; + file: addDeprecatedFileProperties( + { + viteConfig, + typeConfig, + stylesheet, + package: 'package.json', + gitignore: '.gitignore', + getRelative({ from, to }) { + from = from ?? ''; + let relativePath = path.posix.relative(path.posix.dirname(from), to); + // Ensure relative paths start with ./ for proper relative path syntax + if (!relativePath.startsWith('.') && !relativePath.startsWith('/')) { + relativePath = `./${relativePath}`; + } + return relativePath; + }, + findUp(filename) { + const found = find.up(filename, { cwd: resolvedCwd }); + if (!found) return filename; + // don't escape .test-output during tests + if (resolvedCwd.includes('.test-output') && !found.includes('.test-output')) { + return filename; + } + return path.relative(resolvedCwd, found); } - return relativePath; }, - findUp(filename) { - const found = find.up(filename, { cwd: resolvedCwd }); - if (!found) return filename; - // don't escape .test-output during tests - if (resolvedCwd.includes('.test-output') && !found.includes('.test-output')) { - return filename; - } - return path.relative(resolvedCwd, found); - } - }), + svelteConfig + ), isKit, directory, dependencyVersion: (pkg) => dependencies[pkg] @@ -234,44 +253,25 @@ function findWorkspaceRoot(cwd: string): string { return cwd; } -function parseKitOptions(cwd: string, svelteConfigPath: string) { - const configSource = loadFile(cwd, svelteConfigPath); - const { ast } = parse.script(configSource); - - const defaultExport = ast.body.find((s) => s.type === 'ExportDefaultDeclaration'); - if (!defaultExport) throw Error(`Missing default export in \`${svelteConfigPath}\``); - - let objectExpression: AstTypes.ObjectExpression | undefined; - if (defaultExport.declaration.type === 'Identifier') { - // e.g. `export default config;` - const identifier = defaultExport.declaration; - for (const declaration of ast.body) { - if (declaration.type !== 'VariableDeclaration') continue; - - const declarator = declaration.declarations.find( - (d): d is AstTypes.VariableDeclarator => - d.type === 'VariableDeclarator' && - d.id.type === 'Identifier' && - d.id.name === identifier.name - ); - - if (declarator?.init?.type !== 'ObjectExpression') continue; - - objectExpression = declarator.init; - } +/** + * Reads `kit.files.lib` / `kit.files.routes` from wherever the config lives - a + * `svelte.config.{js,ts}` default export, or the object passed to `sveltekit()` in a + * `vite.config.{js,ts}`. Falls back to the SvelteKit defaults when no config is found + * (e.g. a freshly created project that keeps its config in `vite.config.js`). + */ +function parseKitOptions(cwd: string): { lib: string; kitRoutes: string } { + const fallback = { lib: 'src/lib', kitRoutes: 'src/routes' }; - if (!objectExpression) - throw Error(`Unable to find svelte config object expression from \`${svelteConfigPath}\``); - } else if (defaultExport.declaration.type === 'ObjectExpression') { - // e.g. `export default { ... };` - objectExpression = defaultExport.declaration; + let kit; + try { + // `read` locates + parses in one pass (no double parse); tolerate a malformed/odd config + const config = svelteConfig.read((p) => (fileExists(cwd, p) ? loadFile(cwd, p) : null)); + if (!config) return fallback; + kit = config.kit; + } catch { + return fallback; } - // We'll error out since we can't safely determine the config object - if (!objectExpression) - throw new Error(`Unexpected svelte config shape from \`${svelteConfigPath}\``); - - const kit = js.object.property(objectExpression, { name: 'kit', fallback: js.object.create({}) }); const files = js.object.property(kit, { name: 'files', fallback: js.object.create({}) }); const routes = js.object.property(files, { name: 'routes', @@ -280,7 +280,7 @@ function parseKitOptions(cwd: string, svelteConfigPath: string) { const lib = js.object.property(files, { name: 'lib', fallback: js.common.createLiteral('') }); return { - lib: (lib.value as string) || 'src/lib', - kitRoutes: (routes.value as string) || 'src/routes' + lib: (lib.value as string) || fallback.lib, + kitRoutes: (routes.value as string) || fallback.kitRoutes }; } From 4007d7f6e04bbac58859c65d12a8a2296f5fbc53 Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 6 Jun 2026 12:30:00 +0200 Subject: [PATCH 05/10] feat(sv): migrate add-ons to the svelteConfig helper mdsvex, drizzle, sveltekit-adapter and eslint now edit the config wherever it lives; eslint only wires svelteConfig when a standalone svelte.config file exists. --- packages/sv/src/addons/drizzle.ts | 29 +++---- packages/sv/src/addons/eslint.ts | 22 +++-- packages/sv/src/addons/mdsvex.ts | 59 +++++-------- packages/sv/src/addons/sveltekit-adapter.ts | 82 ++++++++----------- .../addons/tests/sveltekit-adapter/test.ts | 8 +- packages/sv/src/addons/tests/vitest/test.ts | 5 +- 6 files changed, 89 insertions(+), 116 deletions(-) diff --git a/packages/sv/src/addons/drizzle.ts b/packages/sv/src/addons/drizzle.ts index 40f202a60..fbaf5030d 100644 --- a/packages/sv/src/addons/drizzle.ts +++ b/packages/sv/src/addons/drizzle.ts @@ -6,7 +6,8 @@ import { pnpm, resolveCommandArray, fileExists, - createPrinter + createPrinter, + svelteConfig } from '@sveltejs/sv-utils'; import crypto from 'node:crypto'; import fs from 'node:fs'; @@ -297,23 +298,15 @@ export default defineAddon({ }) ); - sv.file( - file.svelteConfig, - transforms.script(({ ast, js }) => { - const { value: config } = js.exports.createDefault(ast, { - fallback: js.object.create({}) - }); - js.object.overrideProperties(config, { - kit: { - typescript: { - config: js.common.parseExpression( - `(config) => ({ ...config, include: [...config.include, '../drizzle.config.${language}'] })` - ) - } - } - }); - }) - ); + svelteConfig.edit({ sv, cwd }, ({ override, js }) => { + override({ + typescript: { + config: js.common.parseExpression( + `(config) => ({ ...config, include: [...config.include, '../drizzle.config.${language}'] })` + ) + } + }); + }); sv.file( paths['database schema'], diff --git a/packages/sv/src/addons/eslint.ts b/packages/sv/src/addons/eslint.ts index b3c6ae789..f959c4f81 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, transforms } from '@sveltejs/sv-utils'; +import { type AstTypes, fileExists, loadFile, svelteConfig, transforms } from '@sveltejs/sv-utils'; import { defineAddon } from '../core/config.ts'; import { addEslintConfigPrettier, ESLINT_VERSION, getNodeTypesVersion } from './common.ts'; @@ -8,9 +8,16 @@ export default defineAddon({ shortDescription: 'linter', homepage: 'https://eslint.org', options: {}, - run: ({ sv, language, dependencyVersion, file }) => { + run: ({ sv, language, dependencyVersion, file, cwd }) => { const typescript = language === 'ts'; const prettierInstalled = Boolean(dependencyVersion('prettier')); + // Only wire up `svelteConfig` when there's a separate `svelte.config.{js,ts}` file to import. + // When the config lives in `vite.config.js` instead, there's nothing importable here: + // `svelte-eslint-parser` falls back to its defaults when `svelteConfig` is omitted, and the + // docs warn against feeding it the vite-extracted config (non-serializable props like the + // `runes` function break eslint's `--cache`). + const configLocation = svelteConfig.find((p) => (fileExists(cwd, p) ? loadFile(cwd, p) : null)); + const svelteConfigFile = configLocation?.kind === 'svelte' ? configLocation.path : undefined; sv.devDependency('eslint', ESLINT_VERSION); sv.devDependency('eslint-plugin-svelte', '^3.19.0'); @@ -33,7 +40,9 @@ export default defineAddon({ 'eslint.config.js', transforms.script(({ ast, comments, js }) => { const eslintConfigs: Array = []; - js.imports.addDefault(ast, { from: './svelte.config.js', as: 'svelteConfig' }); + if (svelteConfigFile) { + js.imports.addDefault(ast, { from: `./${svelteConfigFile}`, as: 'svelteConfig' }); + } const gitIgnorePathStatement = js.common.parseStatement( "\nconst gitignorePath = path.resolve(import.meta.dirname, '.gitignore');" ); @@ -82,6 +91,9 @@ export default defineAddon({ eslintConfigs.push(globalsConfig); + const svelteConfigProp = svelteConfigFile + ? { svelteConfig: js.variables.createIdentifier('svelteConfig') } + : {}; if (typescript) { const svelteTSParserConfig = js.object.create({ files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], @@ -90,7 +102,7 @@ export default defineAddon({ projectService: true, extraFileExtensions: ['.svelte'], parser: js.variables.createIdentifier('ts.parser'), - svelteConfig: js.variables.createIdentifier('svelteConfig') + ...svelteConfigProp } } }); @@ -100,7 +112,7 @@ export default defineAddon({ files: ['**/*.svelte', '**/*.svelte.js'], languageOptions: { parserOptions: { - svelteConfig: js.variables.createIdentifier('svelteConfig') + ...svelteConfigProp } } }); diff --git a/packages/sv/src/addons/mdsvex.ts b/packages/sv/src/addons/mdsvex.ts index 4f66572aa..bcddd99df 100644 --- a/packages/sv/src/addons/mdsvex.ts +++ b/packages/sv/src/addons/mdsvex.ts @@ -1,4 +1,4 @@ -import { transforms } from '@sveltejs/sv-utils'; +import { svelteConfig } from '@sveltejs/sv-utils'; import { defineAddon } from '../core/config.ts'; export default defineAddon({ @@ -6,47 +6,32 @@ export default defineAddon({ shortDescription: 'svelte + markdown', homepage: 'https://mdsvex.pngwn.io', options: {}, - run: ({ sv, file }) => { + run: ({ sv, cwd }) => { sv.devDependency('mdsvex', '^0.12.7'); - sv.file( - file.svelteConfig, - transforms.script(({ ast, js }) => { - js.imports.addNamed(ast, { from: 'mdsvex', imports: ['mdsvex'] }); + svelteConfig.edit({ sv, cwd }, ({ ast, property, override, js }) => { + js.imports.addNamed(ast, { from: 'mdsvex', imports: ['mdsvex'] }); - const { value: exportDefault } = js.exports.createDefault(ast, { - fallback: js.object.create({}) - }); + // preprocess + let preprocessorArray = property('preprocess', { fallback: js.array.create() }); + const isArray = preprocessorArray.type === 'ArrayExpression'; - // 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); + override({ preprocess: preprocessorArray }); + } - 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: [] }); + mdsvexCall.arguments.push(js.object.create({ extensions: ['.svx', '.md'] })); + js.array.append(preprocessorArray, mdsvexCall); - const mdsvexCall = js.functions.createCall({ name: 'mdsvex', args: [] }); - mdsvexCall.arguments.push(js.object.create({ extensions: ['.svx', '.md'] })); - 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'); - js.array.append(extensionsArray, '.md'); - }) - ); + // extensions + const extensionsArray = property('extensions', { fallback: js.array.create() }); + js.array.append(extensionsArray, '.svelte'); + js.array.append(extensionsArray, '.svx'); + js.array.append(extensionsArray, '.md'); + }); } }); diff --git a/packages/sv/src/addons/sveltekit-adapter.ts b/packages/sv/src/addons/sveltekit-adapter.ts index 88eeb1772..4dc1cad94 100644 --- a/packages/sv/src/addons/sveltekit-adapter.ts +++ b/packages/sv/src/addons/sveltekit-adapter.ts @@ -5,7 +5,8 @@ import { fileExists, loadPackageJson, sanitizeName, - pnpm + pnpm, + svelteConfig } from '@sveltejs/sv-utils'; import { defineAddon, defineAddonOptions } from '../core/config.ts'; @@ -74,58 +75,39 @@ export default defineAddon({ sv.devDependency(adapter.package, adapter.version); - sv.file( - file.svelteConfig, - transforms.script(({ ast, comments, js }) => { - // finds any existing adapter's import declaration - const imports = ast.body.filter((n) => n.type === 'ImportDeclaration'); - const adapterImports = imports.find( - (importDecl) => - typeof importDecl.source.value === 'string' && - importDecl.source.value.startsWith('@sveltejs/adapter-') && - importDecl.importKind === 'value' - ); + svelteConfig.edit({ sv, cwd }, ({ ast, override, js }) => { + // finds any existing adapter's import declaration + const imports = ast.body.filter((n) => n.type === 'ImportDeclaration'); + const adapterImports = imports.find( + (importDecl) => + typeof importDecl.source.value === 'string' && + importDecl.source.value.startsWith('@sveltejs/adapter-') && + importDecl.importKind === 'value' + ); - let adapterName = 'adapter'; - if (adapterImports) { - // replaces the import's source with the new adapter - adapterImports.source.value = adapter.package; - // reset raw value, so that the string is re-generated - adapterImports.source.raw = undefined; - - const defaultSpecifier = adapterImports.specifiers?.find( - (s) => s.type === 'ImportDefaultSpecifier' - ); - adapterName = defaultSpecifier!.local.name; - } else { - js.imports.addDefault(ast, { from: adapter.package, as: adapterName }); - } + let adapterName = 'adapter'; + if (adapterImports) { + // replaces the import's source with the new adapter + adapterImports.source.value = adapter.package; + // reset raw value, so that the string is re-generated + adapterImports.source.raw = undefined; - const { value: config } = js.exports.createDefault(ast, { fallback: js.object.create({}) }); + const defaultSpecifier = adapterImports.specifiers?.find( + (s) => s.type === 'ImportDefaultSpecifier' + ); + adapterName = defaultSpecifier!.local.name; + } else { + js.imports.addDefault(ast, { from: adapter.package, as: adapterName }); + } - // 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 - ); - } - }) - ); + // for non-auto adapters, also drop the now-stale adapter-auto explanatory comment + override( + { adapter: js.functions.createCall({ name: adapterName, args: [], useIdentifiers: true }) }, + adapter.package === '@sveltejs/adapter-auto' + ? undefined + : { dropLeadingComments: ['adapter'] } + ); + }); if (adapter.package === '@sveltejs/adapter-cloudflare') { sv.devDependency('wrangler', '^4.81.0'); diff --git a/packages/sv/src/addons/tests/sveltekit-adapter/test.ts b/packages/sv/src/addons/tests/sveltekit-adapter/test.ts index 6be7880b4..c4e507515 100644 --- a/packages/sv/src/addons/tests/sveltekit-adapter/test.ts +++ b/packages/sv/src/addons/tests/sveltekit-adapter/test.ts @@ -29,13 +29,13 @@ test.concurrent.for(testCases)('adapter $kind.type $variant', (testCase, { ...ct const cwd = ctx.cwd(testCase); if (testCase.kind.type === 'node') { - expect(readFileSync(join(cwd, 'svelte.config.js'), 'utf8')).not.toMatch('adapter-auto'); - expect(readFileSync(join(cwd, 'svelte.config.js'), 'utf8')).not.toMatch( + expect(readFileSync(join(cwd, 'vite.config.js'), 'utf8')).not.toMatch('adapter-auto'); + expect(readFileSync(join(cwd, 'vite.config.js'), 'utf8')).not.toMatch( 'adapter-auto only supports some environments' ); } else if (testCase.kind.type === 'auto') { - expect(readFileSync(join(cwd, 'svelte.config.js'), 'utf8')).toMatch('adapter-auto'); - expect(readFileSync(join(cwd, 'svelte.config.js'), 'utf8')).toMatch( + expect(readFileSync(join(cwd, 'vite.config.js'), 'utf8')).toMatch('adapter-auto'); + expect(readFileSync(join(cwd, 'vite.config.js'), 'utf8')).toMatch( 'adapter-auto only supports some environments' ); } else if (testCase.kind.type === 'cloudflare-workers') { diff --git a/packages/sv/src/addons/tests/vitest/test.ts b/packages/sv/src/addons/tests/vitest/test.ts index 193d726d8..b00440972 100644 --- a/packages/sv/src/addons/tests/vitest/test.ts +++ b/packages/sv/src/addons/tests/vitest/test.ts @@ -25,8 +25,9 @@ test.concurrent.for(testCases)('vitest $variant', (testCase, { expect, ...ctx }) spawnSync('pnpm test', { cwd, stdio: 'pipe', shell: true, timeout: 2 * 60_000 }).status ).toBe(0); - const language = testCase.variant.includes('ts') ? 'ts' : 'js'; - const viteFile = path.resolve(cwd, `vite.config.${language}`); + const viteFile = ['vite.config.ts', 'vite.config.js'] + .map((name) => path.resolve(cwd, name)) + .find((file) => fs.existsSync(file))!; const viteContent = fs.readFileSync(viteFile, 'utf8'); expect(viteContent).toContain(`vitest/config`); From 5b4b6fae985f8a5e242e1ea77ea8902dc0e1e502 Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 6 Jun 2026 13:40:00 +0200 Subject: [PATCH 06/10] feat(create): migrate playground to svelteConfig and require @sveltejs/kit ^2.62.0 The inlined vite.config config needs kit >= 2.62.0; snapshots regenerated. --- .../tests/snapshots/@my-org/sv/vite.config.js | 20 ++++++++++++++ .../tests/snapshots/create-only/package.json | 2 +- .../snapshots/create-only/svelte.config.js | 17 ------------ .../snapshots/create-only/vite.config.js | 20 ++++++++++++++ .../snapshots/create-only/vite.config.ts | 6 ----- .../create-with-all-addons/e2e/demo.test.ts | 6 ----- .../create-with-all-addons/eslint.config.js | 4 +-- .../create-with-all-addons/package.json | 2 +- .../create-with-all-addons/svelte.config.js | 23 ---------------- .../{vite.config.ts => vite.config.js} | 22 +++++++++++++--- packages/sv/src/create/playground.ts | 26 ++++++++++++++----- .../templates/demo/package.template.json | 2 +- .../templates/library/package.template.json | 2 +- .../templates/minimal/package.template.json | 2 +- packages/sv/src/create/tests/playground.ts | 12 ++++----- 15 files changed, 91 insertions(+), 75 deletions(-) create mode 100644 packages/sv/src/cli/tests/snapshots/@my-org/sv/vite.config.js delete mode 100644 packages/sv/src/cli/tests/snapshots/create-only/svelte.config.js create mode 100644 packages/sv/src/cli/tests/snapshots/create-only/vite.config.js delete mode 100644 packages/sv/src/cli/tests/snapshots/create-only/vite.config.ts delete mode 100644 packages/sv/src/cli/tests/snapshots/create-with-all-addons/e2e/demo.test.ts delete mode 100644 packages/sv/src/cli/tests/snapshots/create-with-all-addons/svelte.config.js rename packages/sv/src/cli/tests/snapshots/create-with-all-addons/{vite.config.ts => vite.config.js} (59%) diff --git a/packages/sv/src/cli/tests/snapshots/@my-org/sv/vite.config.js b/packages/sv/src/cli/tests/snapshots/@my-org/sv/vite.config.js new file mode 100644 index 000000000..cb76b810c --- /dev/null +++ b/packages/sv/src/cli/tests/snapshots/@my-org/sv/vite.config.js @@ -0,0 +1,20 @@ +import adapter from '@sveltejs/adapter-auto'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + compilerOptions: { + // Force runes mode for the project, except for libraries. Can be removed in svelte 6. + runes: ({ filename }) => + filename.split(/[/\\]/).includes('node_modules') ? undefined : true + }, + + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter() + }) + ] +}); diff --git a/packages/sv/src/cli/tests/snapshots/create-only/package.json b/packages/sv/src/cli/tests/snapshots/create-only/package.json index 06621428f..5fae083ee 100644 --- a/packages/sv/src/cli/tests/snapshots/create-only/package.json +++ b/packages/sv/src/cli/tests/snapshots/create-only/package.json @@ -13,7 +13,7 @@ }, "devDependencies": { "@sveltejs/adapter-auto": "^7.0.1", - "@sveltejs/kit": "^2.57.0", + "@sveltejs/kit": "^2.62.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", "svelte": "^5.56.1", "svelte-check": "^4.4.6", diff --git a/packages/sv/src/cli/tests/snapshots/create-only/svelte.config.js b/packages/sv/src/cli/tests/snapshots/create-only/svelte.config.js deleted file mode 100644 index 0c3412e9f..000000000 --- a/packages/sv/src/cli/tests/snapshots/create-only/svelte.config.js +++ /dev/null @@ -1,17 +0,0 @@ -import adapter from '@sveltejs/adapter-auto'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - compilerOptions: { - // Force runes mode for the project, except for libraries. Can be removed in svelte 6. - runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true) - }, - kit: { - // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://svelte.dev/docs/kit/adapters for more information about adapters. - adapter: adapter() - } -}; - -export default config; diff --git a/packages/sv/src/cli/tests/snapshots/create-only/vite.config.js b/packages/sv/src/cli/tests/snapshots/create-only/vite.config.js new file mode 100644 index 000000000..cb76b810c --- /dev/null +++ b/packages/sv/src/cli/tests/snapshots/create-only/vite.config.js @@ -0,0 +1,20 @@ +import adapter from '@sveltejs/adapter-auto'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + compilerOptions: { + // Force runes mode for the project, except for libraries. Can be removed in svelte 6. + runes: ({ filename }) => + filename.split(/[/\\]/).includes('node_modules') ? undefined : true + }, + + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter() + }) + ] +}); diff --git a/packages/sv/src/cli/tests/snapshots/create-only/vite.config.ts b/packages/sv/src/cli/tests/snapshots/create-only/vite.config.ts deleted file mode 100644 index bbf8c7da4..000000000 --- a/packages/sv/src/cli/tests/snapshots/create-only/vite.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - plugins: [sveltekit()] -}); diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/e2e/demo.test.ts b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/e2e/demo.test.ts deleted file mode 100644 index 9985ce113..000000000 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/e2e/demo.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { expect, test } from '@playwright/test'; - -test('home page has expected h1', async ({ page }) => { - await page.goto('/'); - await expect(page.locator('h1')).toBeVisible(); -}); diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/eslint.config.js b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/eslint.config.js index 3449331cb..e46c5b535 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/eslint.config.js +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/eslint.config.js @@ -5,7 +5,6 @@ import svelte from 'eslint-plugin-svelte'; import { defineConfig, includeIgnoreFile } from 'eslint/config'; import globals from 'globals'; import ts from 'typescript-eslint'; -import svelteConfig from './svelte.config.js'; const gitignorePath = path.resolve(import.meta.dirname, '.gitignore'); @@ -30,8 +29,7 @@ export default defineConfig( parserOptions: { projectService: true, extraFileExtensions: ['.svelte'], - parser: ts.parser, - svelteConfig + parser: ts.parser } } }, diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/package.json b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/package.json index bfe34a066..42ff8f2ae 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/package.json +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/package.json @@ -28,7 +28,7 @@ "@libsql/client": "^0.17.2", "@playwright/test": "^1.60.0", "@sveltejs/adapter-node": "^5.5.4", - "@sveltejs/kit": "^2.57.0", + "@sveltejs/kit": "^2.62.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", "@tailwindcss/forms": "^0.5.11", "@tailwindcss/typography": "^0.5.19", diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/svelte.config.js b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/svelte.config.js deleted file mode 100644 index b3fc28efa..000000000 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/svelte.config.js +++ /dev/null @@ -1,23 +0,0 @@ -import { mdsvex } from 'mdsvex'; -import adapter from '@sveltejs/adapter-node'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - compilerOptions: { - // Force runes mode for the project, except for libraries. Can be removed in svelte 6. - runes: ({ filename }) => filename.split(/[/\\]/).includes('node_modules') ? undefined : true - }, - kit: { - adapter: adapter(), - typescript: { - config: (config) => ({ - ...config, - include: [...config.include, '../drizzle.config.ts'] - }) - } - }, - preprocess: [mdsvex({ extensions: ['.svx', '.md'] })], - extensions: ['.svelte', '.svx', '.md'] -}; - -export default config; diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/vite.config.ts b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/vite.config.js similarity index 59% rename from packages/sv/src/cli/tests/snapshots/create-with-all-addons/vite.config.ts rename to packages/sv/src/cli/tests/snapshots/create-with-all-addons/vite.config.js index 9c72d3491..0131f469d 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/vite.config.ts +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/vite.config.js @@ -1,20 +1,36 @@ import { paraglideVitePlugin } from '@inlang/paraglide-js'; +import { mdsvex } from 'mdsvex'; import tailwindcss from '@tailwindcss/vite'; import { defineConfig } from 'vitest/config'; import { playwright } from '@vitest/browser-playwright'; +import adapter from '@sveltejs/adapter-node'; import { sveltekit } from '@sveltejs/kit/vite'; export default defineConfig({ plugins: [ tailwindcss(), - sveltekit(), + sveltekit({ + compilerOptions: { + // Force runes mode for the project, except for libraries. Can be removed in svelte 6. + runes: ({ filename }) => filename.split(/[/\\]/).includes('node_modules') ? undefined : true + }, + adapter: adapter(), + preprocess: [mdsvex({ extensions: ['.svx', '.md'] })], + extensions: ['.svelte', '.svx', '.md'], + typescript: { + config: (config) => ({ + ...config, + include: [...config.include, '../drizzle.config.ts'] + }) + } + }), paraglideVitePlugin({ project: './project.inlang', outdir: './src/lib/paraglide' }) ], test: { expect: { requireAssertions: true }, projects: [ { - extends: './vite.config.ts', + extends: './vite.config.js', test: { name: 'client', browser: { @@ -28,7 +44,7 @@ export default defineConfig({ }, { - extends: './vite.config.ts', + extends: './vite.config.js', test: { name: 'server', environment: 'node', diff --git a/packages/sv/src/create/playground.ts b/packages/sv/src/create/playground.ts index b93a393bb..94dc3d771 100644 --- a/packages/sv/src/create/playground.ts +++ b/packages/sv/src/create/playground.ts @@ -4,6 +4,7 @@ import { js, parse, svelte, + svelteConfig, downloadJson, Walker } from '@sveltejs/sv-utils'; @@ -255,12 +256,25 @@ export function setupPlaygroundProject( let experimentalAsyncNeeded = true; const addExperimentalAsync = () => { - const svelteConfigPath = path.join(cwd, filePaths.svelteConfig); - const svelteConfig = fs.readFileSync(svelteConfigPath, 'utf-8'); - const { ast, generateCode } = parse.script(svelteConfig); - const { value: config } = js.exports.createDefault(ast, { fallback: js.object.create({}) }); - js.object.overrideProperties(config, { compilerOptions: { experimental: { async: true } } }); - fs.writeFileSync(svelteConfigPath, generateCode(), 'utf-8'); + // `compilerOptions` is a svelte-level option, so it goes on `config` regardless of whether + // the config lives in `svelte.config.js` or inside `sveltekit()` in `vite.config.js`. + svelteConfig.edit( + { + cwd, + sv: { + file: (p, edit) => { + const full = path.join(cwd, p); + const content = fs.existsSync(full) ? fs.readFileSync(full, 'utf-8') : ''; + const result = edit(content); + if (result === false || result === '') return; + fs.writeFileSync(full, result, 'utf-8'); + } + } + }, + ({ override }) => { + override({ compilerOptions: { experimental: { async: true } } }); + } + ); }; // we want to change the svelte version, even if the user decided diff --git a/packages/sv/src/create/templates/demo/package.template.json b/packages/sv/src/create/templates/demo/package.template.json index b015c2727..66546ef36 100644 --- a/packages/sv/src/create/templates/demo/package.template.json +++ b/packages/sv/src/create/templates/demo/package.template.json @@ -13,7 +13,7 @@ "@fontsource/fira-mono": "^5.2.7", "@neoconfetti/svelte": "^2.2.2", "@sveltejs/adapter-auto": "^7.0.1", - "@sveltejs/kit": "^2.57.0", + "@sveltejs/kit": "^2.62.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", "svelte": "^5.56.1", "vite": "^8.0.7" diff --git a/packages/sv/src/create/templates/library/package.template.json b/packages/sv/src/create/templates/library/package.template.json index 95c1cd4aa..2ba26d38b 100644 --- a/packages/sv/src/create/templates/library/package.template.json +++ b/packages/sv/src/create/templates/library/package.template.json @@ -24,7 +24,7 @@ }, "devDependencies": { "@sveltejs/adapter-auto": "^7.0.1", - "@sveltejs/kit": "^2.57.0", + "@sveltejs/kit": "^2.62.0", "@sveltejs/package": "^2.5.7", "@sveltejs/vite-plugin-svelte": "^7.0.0", "publint": "^0.3.18", diff --git a/packages/sv/src/create/templates/minimal/package.template.json b/packages/sv/src/create/templates/minimal/package.template.json index 690eb69a4..34dad538d 100644 --- a/packages/sv/src/create/templates/minimal/package.template.json +++ b/packages/sv/src/create/templates/minimal/package.template.json @@ -11,7 +11,7 @@ }, "devDependencies": { "@sveltejs/adapter-auto": "^7.0.1", - "@sveltejs/kit": "^2.57.0", + "@sveltejs/kit": "^2.62.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", "svelte": "^5.56.1", "vite": "^8.0.7" diff --git a/packages/sv/src/create/tests/playground.ts b/packages/sv/src/create/tests/playground.ts index 5d7778de0..d7b69ed4e 100644 --- a/packages/sv/src/create/tests/playground.ts +++ b/packages/sv/src/create/tests/playground.ts @@ -194,9 +194,9 @@ test('real world download and convert playground async', async () => { expect(packageJsonContent).toContain('"change-case": "latest"'); expect(packageJsonContent).toContain('"svelte": "5.38.7"'); - const svelteConfigPath = path.join(directory, 'svelte.config.js'); - const svelteConfigContent = fs.readFileSync(svelteConfigPath, 'utf-8'); - expect(svelteConfigContent).toContain('experimental: { async: true }'); + const viteConfigPath = path.join(directory, 'vite.config.js'); + const viteConfigContent = fs.readFileSync(viteConfigPath, 'utf-8'); + expect(viteConfigContent).toContain('experimental: { async: true }'); }); test('real world download and convert playground without async', async () => { @@ -229,7 +229,7 @@ test('real world download and convert playground without async', async () => { const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf-8'); expect(packageJsonContent).toContain('"svelte": "5.0.5"'); - const svelteConfigPath = path.join(directory, 'svelte.config.js'); - const svelteConfigContent = fs.readFileSync(svelteConfigPath, 'utf-8'); - expect(svelteConfigContent).not.toContain('experimental: { async: true }'); + const viteConfigPath = path.join(directory, 'vite.config.js'); + const viteConfigContent = fs.readFileSync(viteConfigPath, 'utf-8'); + expect(viteConfigContent).not.toContain('experimental: { async: true }'); }); From 67a909844e4b54776b584978f35ef8368842c2cf Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 6 Jun 2026 13:58:48 +0200 Subject: [PATCH 07/10] update changset --- .changeset/sv-config-in-vite.md | 5 +++++ .changeset/sv-utils-svelte-config.md | 5 +++++ .changeset/svelte-config-in-vite.md | 6 ------ 3 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 .changeset/sv-config-in-vite.md create mode 100644 .changeset/sv-utils-svelte-config.md delete mode 100644 .changeset/svelte-config-in-vite.md diff --git a/.changeset/sv-config-in-vite.md b/.changeset/sv-config-in-vite.md new file mode 100644 index 000000000..e00dd792a --- /dev/null +++ b/.changeset/sv-config-in-vite.md @@ -0,0 +1,5 @@ +--- +'sv': minor +--- + +chore: bump templates to `@sveltejs/kit` `^2.62.0` and move svelte config to vite plugin (info: https://github.com/sveltejs/kit/pull/15944) diff --git a/.changeset/sv-utils-svelte-config.md b/.changeset/sv-utils-svelte-config.md new file mode 100644 index 000000000..4c230e8d7 --- /dev/null +++ b/.changeset/sv-utils-svelte-config.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/sv-utils': minor +--- + +Add `svelteConfig` helper (`find`, `read`, `edit`) to locate and edit the svelte config whether it lives in `svelte.config.{js,ts}` or the `sveltekit()` call in `vite.config.{js,ts}` diff --git a/.changeset/svelte-config-in-vite.md b/.changeset/svelte-config-in-vite.md deleted file mode 100644 index 56ba713b7..000000000 --- a/.changeset/svelte-config-in-vite.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@sveltejs/sv-utils': minor -'sv': minor ---- - -Add the `svelteConfig` helper and read/edit the svelte config wherever it lives - a `svelte.config.{js,ts}` default export or the object passed to `sveltekit()` in `vite.config.{js,ts}` From 6526e51f03b7c6ffdfd6330d17c2ca5d4429a514 Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 6 Jun 2026 14:33:49 +0200 Subject: [PATCH 08/10] shared --- packages/sv/src/create/shared/{vite.config.js => vite.config.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/sv/src/create/shared/{vite.config.js => vite.config.ts} (100%) diff --git a/packages/sv/src/create/shared/vite.config.js b/packages/sv/src/create/shared/vite.config.ts similarity index 100% rename from packages/sv/src/create/shared/vite.config.js rename to packages/sv/src/create/shared/vite.config.ts From fc3e7a3a244d9d404ae2ce27d35db41d9282be32 Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 6 Jun 2026 14:34:46 +0200 Subject: [PATCH 09/10] need vite.config.ts for type ts (even if no ts inside the file) --- documentation/docs/50-api/20-sv-utils.md | 40 +++++++++---------- packages/sv-utils/api-surface.md | 35 +++++++--------- packages/sv-utils/src/svelte-config.ts | 11 +++-- packages/sv-utils/src/tests/svelte-config.ts | 6 ++- .../addons/tests/sveltekit-adapter/test.ts | 15 ++++--- .../{vite.config.js => vite.config.ts} | 0 .../{vite.config.js => vite.config.ts} | 4 +- packages/sv/src/create/tests/playground.ts | 4 +- 8 files changed, 59 insertions(+), 56 deletions(-) rename packages/sv/src/cli/tests/snapshots/create-only/{vite.config.js => vite.config.ts} (100%) rename packages/sv/src/cli/tests/snapshots/create-with-all-addons/{vite.config.js => vite.config.ts} (95%) diff --git a/documentation/docs/50-api/20-sv-utils.md b/documentation/docs/50-api/20-sv-utils.md index 39e7e7a84..70da9de46 100644 --- a/documentation/docs/50-api/20-sv-utils.md +++ b/documentation/docs/50-api/20-sv-utils.md @@ -232,44 +232,42 @@ Namespaced helpers for AST manipulation: ## Svelte config -A SvelteKit project's config can live in two places: the default export of a `svelte.config.{js,ts}`, or the object passed to `sveltekit()` in a `vite.config.{js,ts}`. `svelteConf` edits it wherever it is, so add-ons don't have to care which. +As of SvelteKit 2.62, the svelte/kit config can be passed straight to the `sveltekit()` plugin in `vite.config.{js,ts}`, and a separate `svelte.config.{js,ts}` is no longer required. **This is now the default for projects created by `sv`** - generated projects keep their config inside `vite.config.js` and ship no `svelte.config.js`. -### `svelteConf` +`svelteConfig` lets add-ons read and edit that config wherever it lives - the `sveltekit()` argument in `vite.config.{js,ts}` (the new default), or a `svelte.config.{js,ts}` default export (still supported) - without having to know which. -Locates the config and hands your callback two object expressions to edit: +### `svelteConfig.edit` -- **`config`** - the svelte-level config (`preprocess`, `extensions`, `compilerOptions`, `vitePlugin`). -- **`kit`** - the kit-level config (`adapter`, `alias`, `files`, `typescript`, …). - -In a `svelte.config.js`, `kit` is the nested `kit: { … }` object. In a `vite.config.js`, `sveltekit()` takes a flattened `KitConfig & SvelteConfig`, so `config` and `kit` point at the same object. You write the same code either way: +You address options by name and the helper writes each one to the right place, so you never deal with the `kit` nesting yourself. Svelte-level options (`compilerOptions`, `preprocess`, `extensions`, `vitePlugin`) sit on the config object; everything else (`adapter`, `alias`, `files`, `typescript`, …) is a kit option, which means flattened onto the `sveltekit()` argument in a vite config, or nested under `kit` in a `svelte.config`. ```js // @noErrors -import { svelteConf } from '@sveltejs/sv-utils'; +import { svelteConfig } from '@sveltejs/sv-utils'; // inside an add-on's `run({ sv, cwd })`: -svelteConf({ sv, cwd }, ({ ast, config, kit, js }) => { - // svelte-level option: - js.array.append( - js.object.property(config, { name: 'extensions', fallback: js.array.create() }), - '.svx' - ); - // kit-level option (nested under `kit` in svelte.config, top-level in vite.config): +svelteConfig.edit({ sv, cwd }, ({ ast, property, override, js }) => { + // svelte-level option - get-or-create its value, then mutate in place: + js.array.append(property('extensions', { fallback: js.array.create() }), '.svx'); + + // kit option - routed automatically, no `kit` nesting to think about: js.imports.addDefault(ast, { from: '@sveltejs/adapter-node', as: 'adapter' }); - js.object.overrideProperties(kit, { + override({ adapter: js.functions.createCall({ name: 'adapter', args: [], useIdentifiers: true }) }); }); ``` -It writes through `sv.file`, so the edit is tracked like any other. It throws if the project has no config in either location. +- **`property(name, { fallback })`** - get-or-create an option's value to mutate in place (arrays, nested objects). +- **`override(props, { dropLeadingComments })`** - set/replace options; `dropLeadingComments` clears a now-stale leading comment (e.g. the adapter-auto note when switching adapters). + +It writes through `sv.file`, so the edit is tracked like any other. If the project has neither config file, a `svelte.config.js` is created. -### `findSvelteConfig` / `getSvelteConfigObjects` +### `svelteConfig.find` / `svelteConfig.read` -Lower-level building blocks if you need them outside an edit: +Lower-level building blocks, both reading candidate files through an injected `read(path)` (returns the file contents or `null`) so detection stays static - the config is never executed: -- **`findSvelteConfig(read)`** - returns `{ path, kind: 'svelte' | 'vite' }` (or `null`), where `read(path)` returns a file's contents or `null`. Detection is static (it never executes the config), and `svelte.config` wins when both are present. -- **`getSvelteConfigObjects(ast, kind)`** - given a parsed program and the `kind` from `findSvelteConfig`, returns the `{ config, kit }` object expressions. +- **`svelteConfig.find(read)`** - returns `{ path, kind }` or `null` (`kind` is `'vite'` or `'svelte'`; `svelte.config` wins when both are present). +- **`svelteConfig.read(read)`** - locates and parses in one pass, returning `{ location, config, kit }` (the object expressions) or `null`. ## Package manager helpers diff --git a/packages/sv-utils/api-surface.md b/packages/sv-utils/api-surface.md index 3abf862f5..640f83db6 100644 --- a/packages/sv-utils/api-surface.md +++ b/packages/sv-utils/api-surface.md @@ -411,7 +411,7 @@ declare function getArgument( } ): T; declare namespace imports_d_exports { - export { addDefault, addEmpty, addNamed, addNamespace$1 as addNamespace, find$1 as find, remove }; + export { addDefault, addEmpty, addNamed, addNamespace$1 as addNamespace, find, remove }; } declare function addEmpty( node: estree.Program, @@ -441,7 +441,7 @@ declare function addNamed( isType?: boolean; } ): void; -declare function find$1( +declare function find( ast: estree.Program, options: { name: string; @@ -774,8 +774,10 @@ declare function loadPackageJson(cwd: string): { }; type SvelteConfigKind = 'svelte' | 'vite'; + +type SvelteConfigPath = `${'svelte.config' | 'vite.config'}.${'js' | 'ts'}`; type SvelteConfigLocation = { - path: string; + path: SvelteConfigPath; kind: SvelteConfigKind; }; @@ -787,10 +789,6 @@ type SvelteConfigObjects = { type ConfigFileReader = (path: string) => string | null; type ObjectMap = Parameters[1]; - -declare function find(read: ConfigFileReader): SvelteConfigLocation | null; - -declare function read(readFile: ConfigFileReader): SvelteConfigObjects | null; type SvelteConfEdit = (file: { ast: estree.Program; comments: Comments; @@ -816,21 +814,16 @@ type SvFileApi = { file: (path: string, edit: (content: string) => string | false) => void; }; -declare function edit( - { - sv, - cwd - }: { - sv: SvFileApi; - cwd: string; - }, - editFn: SvelteConfEdit -): void; - declare const svelteConfig: { - edit: typeof edit; - find: typeof find; - read: typeof read; + edit: ( + target: { + sv: SvFileApi; + cwd: string; + }, + editFn: SvelteConfEdit + ) => void; + find: (read: ConfigFileReader) => SvelteConfigLocation | null; + read: (read: ConfigFileReader) => SvelteConfigObjects | null; }; type ColorInput = string | string[]; declare const color: { diff --git a/packages/sv-utils/src/svelte-config.ts b/packages/sv-utils/src/svelte-config.ts index fe06e1bc0..6a9842d01 100644 --- a/packages/sv-utils/src/svelte-config.ts +++ b/packages/sv-utils/src/svelte-config.ts @@ -13,9 +13,12 @@ import { transforms } from './tooling/transforms.ts'; export type { SvelteConfigKind } from './tooling/js/svelte-config.ts'; +/** The four config file paths the helper understands. */ +export type SvelteConfigPath = `${'svelte.config' | 'vite.config'}.${'js' | 'ts'}`; + export type SvelteConfigLocation = { /** path relative to the workspace root, e.g. `vite.config.ts` or `svelte.config.js` */ - path: string; + path: SvelteConfigPath; kind: SvelteConfigKind; }; @@ -213,11 +216,11 @@ function edit({ sv, cwd }: { sv: SvFileApi; cwd: string }, editFn: SvelteConfEdi */ export const svelteConfig: { /** Edit the config wherever it lives (creating `svelte.config.js` if there is none). */ - edit: typeof edit; + edit: (target: { sv: SvFileApi; cwd: string }, editFn: SvelteConfEdit) => void; /** Locate the config file, returning `{ path, kind }` or `null`. Detection is static (no execution). */ - find: typeof find; + find: (read: ConfigFileReader) => SvelteConfigLocation | null; /** Locate + parse the config in one pass, returning `{ location, config, kit }` or `null`. */ - read: typeof read; + read: (read: ConfigFileReader) => SvelteConfigObjects | null; } = { edit, find, diff --git a/packages/sv-utils/src/tests/svelte-config.ts b/packages/sv-utils/src/tests/svelte-config.ts index 0957f2146..51b4da727 100644 --- a/packages/sv-utils/src/tests/svelte-config.ts +++ b/packages/sv-utils/src/tests/svelte-config.ts @@ -11,7 +11,11 @@ const reader = (files: Record) => (path: string) => files[path] /** Runs an edit against `content` for a given location kind, returning the serialized result. */ const applyEdit = (content: string, kind: SvelteConfigKind, edit: SvelteConfEdit) => - _editConfigContent(content, { path: '', kind }, edit); + _editConfigContent( + content, + { path: kind === 'vite' ? 'vite.config.js' : 'svelte.config.js', kind }, + edit + ); const addAlias: SvelteConfEdit = ({ override, js }) => override({ alias: js.object.create({ $lib: js.common.createLiteral('./src/lib') }) }); diff --git a/packages/sv/src/addons/tests/sveltekit-adapter/test.ts b/packages/sv/src/addons/tests/sveltekit-adapter/test.ts index c4e507515..d77875233 100644 --- a/packages/sv/src/addons/tests/sveltekit-adapter/test.ts +++ b/packages/sv/src/addons/tests/sveltekit-adapter/test.ts @@ -1,4 +1,4 @@ -import { readFileSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { expect } from 'vitest'; import sveltekitAdapter from '../../sveltekit-adapter.ts'; @@ -28,14 +28,19 @@ const { test, testCases } = setupTest( test.concurrent.for(testCases)('adapter $kind.type $variant', (testCase, { ...ctx }) => { const cwd = ctx.cwd(testCase); + // config lives in vite.config.ts (ts) or vite.config.js (js) + const viteConfig = ['vite.config.ts', 'vite.config.js'] + .map((name) => join(cwd, name)) + .find((file) => existsSync(file))!; + if (testCase.kind.type === 'node') { - expect(readFileSync(join(cwd, 'vite.config.js'), 'utf8')).not.toMatch('adapter-auto'); - expect(readFileSync(join(cwd, 'vite.config.js'), 'utf8')).not.toMatch( + expect(readFileSync(viteConfig, 'utf8')).not.toMatch('adapter-auto'); + expect(readFileSync(viteConfig, 'utf8')).not.toMatch( 'adapter-auto only supports some environments' ); } else if (testCase.kind.type === 'auto') { - expect(readFileSync(join(cwd, 'vite.config.js'), 'utf8')).toMatch('adapter-auto'); - expect(readFileSync(join(cwd, 'vite.config.js'), 'utf8')).toMatch( + expect(readFileSync(viteConfig, 'utf8')).toMatch('adapter-auto'); + expect(readFileSync(viteConfig, 'utf8')).toMatch( 'adapter-auto only supports some environments' ); } else if (testCase.kind.type === 'cloudflare-workers') { diff --git a/packages/sv/src/cli/tests/snapshots/create-only/vite.config.js b/packages/sv/src/cli/tests/snapshots/create-only/vite.config.ts similarity index 100% rename from packages/sv/src/cli/tests/snapshots/create-only/vite.config.js rename to packages/sv/src/cli/tests/snapshots/create-only/vite.config.ts diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/vite.config.js b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/vite.config.ts similarity index 95% rename from packages/sv/src/cli/tests/snapshots/create-with-all-addons/vite.config.js rename to packages/sv/src/cli/tests/snapshots/create-with-all-addons/vite.config.ts index 0131f469d..649d6b1d9 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/vite.config.js +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/vite.config.ts @@ -30,7 +30,7 @@ export default defineConfig({ expect: { requireAssertions: true }, projects: [ { - extends: './vite.config.js', + extends: './vite.config.ts', test: { name: 'client', browser: { @@ -44,7 +44,7 @@ export default defineConfig({ }, { - extends: './vite.config.js', + extends: './vite.config.ts', test: { name: 'server', environment: 'node', diff --git a/packages/sv/src/create/tests/playground.ts b/packages/sv/src/create/tests/playground.ts index d7b69ed4e..82bbab63a 100644 --- a/packages/sv/src/create/tests/playground.ts +++ b/packages/sv/src/create/tests/playground.ts @@ -194,7 +194,7 @@ test('real world download and convert playground async', async () => { expect(packageJsonContent).toContain('"change-case": "latest"'); expect(packageJsonContent).toContain('"svelte": "5.38.7"'); - const viteConfigPath = path.join(directory, 'vite.config.js'); + const viteConfigPath = path.join(directory, 'vite.config.ts'); const viteConfigContent = fs.readFileSync(viteConfigPath, 'utf-8'); expect(viteConfigContent).toContain('experimental: { async: true }'); }); @@ -229,7 +229,7 @@ test('real world download and convert playground without async', async () => { const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf-8'); expect(packageJsonContent).toContain('"svelte": "5.0.5"'); - const viteConfigPath = path.join(directory, 'vite.config.js'); + const viteConfigPath = path.join(directory, 'vite.config.ts'); const viteConfigContent = fs.readFileSync(viteConfigPath, 'utf-8'); expect(viteConfigContent).not.toContain('experimental: { async: true }'); }); From ef6075d0329a5b324a80a76a716e36d5a231aa3c Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 6 Jun 2026 14:53:29 +0200 Subject: [PATCH 10/10] not needed --- .../tests/snapshots/@my-org/sv/vite.config.js | 20 ------------------- packages/sv/src/create/index.ts | 10 ++++++++-- 2 files changed, 8 insertions(+), 22 deletions(-) delete mode 100644 packages/sv/src/cli/tests/snapshots/@my-org/sv/vite.config.js diff --git a/packages/sv/src/cli/tests/snapshots/@my-org/sv/vite.config.js b/packages/sv/src/cli/tests/snapshots/@my-org/sv/vite.config.js deleted file mode 100644 index cb76b810c..000000000 --- a/packages/sv/src/cli/tests/snapshots/@my-org/sv/vite.config.js +++ /dev/null @@ -1,20 +0,0 @@ -import adapter from '@sveltejs/adapter-auto'; -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - plugins: [ - sveltekit({ - compilerOptions: { - // Force runes mode for the project, except for libraries. Can be removed in svelte 6. - runes: ({ filename }) => - filename.split(/[/\\]/).includes('node_modules') ? undefined : true - }, - - // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://svelte.dev/docs/kit/adapters for more information about adapters. - adapter: adapter() - }) - ] -}); diff --git a/packages/sv/src/create/index.ts b/packages/sv/src/create/index.ts index 8b487a481..9a1cb3877 100644 --- a/packages/sv/src/create/index.ts +++ b/packages/sv/src/create/index.ts @@ -46,8 +46,14 @@ export function create({ cwd, ...options }: Options): void { // Files that are not relevant for 'addon' template if (options.template === 'addon') { - fs.rmSync(path.join(cwd, 'svelte.config.js')); - fs.rmSync(path.join(cwd, 'vite.config.js')); + for (const name of [ + 'svelte.config.js', + 'svelte.config.ts', + 'vite.config.js', + 'vite.config.ts' + ]) { + fs.rmSync(path.join(cwd, name), { force: true }); + } } }