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/documentation/docs/50-api/20-sv-utils.md b/documentation/docs/50-api/20-sv-utils.md index 062863068..70da9de46 100644 --- a/documentation/docs/50-api/20-sv-utils.md +++ b/documentation/docs/50-api/20-sv-utils.md @@ -230,6 +230,45 @@ Namespaced helpers for AST manipulation: - **`html.*`** - attribute manipulation - **`text.*`** - upsert lines in flat files (.env, .gitignore) +## Svelte config + +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`. + +`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. + +### `svelteConfig.edit` + +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 { svelteConfig } from '@sveltejs/sv-utils'; + +// inside an add-on's `run({ sv, cwd })`: +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' }); + override({ + adapter: js.functions.createCall({ name: 'adapter', args: [], useIdentifiers: true }) + }); +}); +``` + +- **`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. + +### `svelteConfig.find` / `svelteConfig.read` + +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: + +- **`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 ### `pnpm.allowBuilds` diff --git a/packages/sv-utils/api-surface.md b/packages/sv-utils/api-surface.md index 317b5c1cf..640f83db6 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 { @@ -772,6 +772,59 @@ declare function loadPackageJson(cwd: string): { source: string; data: Package; }; + +type SvelteConfigKind = 'svelte' | 'vite'; + +type SvelteConfigPath = `${'svelte.config' | 'vite.config'}.${'js' | 'ts'}`; +type SvelteConfigLocation = { + path: SvelteConfigPath; + kind: SvelteConfigKind; +}; + +type SvelteConfigObjects = { + location: SvelteConfigLocation; + config: estree.ObjectExpression; + kit: estree.ObjectExpression; +}; + +type ConfigFileReader = (path: string) => string | null; +type ObjectMap = Parameters[1]; +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 const svelteConfig: { + 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: { addon: (str: ColorInput) => string; @@ -803,8 +856,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 +889,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..6a9842d01 --- /dev/null +++ b/packages/sv-utils/src/svelte-config.ts @@ -0,0 +1,231 @@ +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'; + +/** 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: SvelteConfigPath; + 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: (target: { sv: SvFileApi; cwd: string }, editFn: SvelteConfEdit) => void; + /** Locate the config file, returning `{ path, kind }` or `null`. Detection is static (no execution). */ + find: (read: ConfigFileReader) => SvelteConfigLocation | null; + /** Locate + parse the config in one pass, returning `{ location, config, kit }` or `null`. */ + read: (read: ConfigFileReader) => SvelteConfigObjects | null; +} = { + 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..51b4da727 --- /dev/null +++ b/packages/sv-utils/src/tests/svelte-config.ts @@ -0,0 +1,221 @@ +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 === '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') }) }); +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({}) }); +} 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..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, 'svelte.config.js'), 'utf8')).not.toMatch('adapter-auto'); - expect(readFileSync(join(cwd, 'svelte.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, 'svelte.config.js'), 'utf8')).toMatch('adapter-auto'); - expect(readFileSync(join(cwd, 'svelte.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/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`); 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.ts b/packages/sv/src/cli/tests/snapshots/create-only/vite.config.ts index bbf8c7da4..cb76b810c 100644 --- a/packages/sv/src/cli/tests/snapshots/create-only/vite.config.ts +++ b/packages/sv/src/cli/tests/snapshots/create-only/vite.config.ts @@ -1,6 +1,20 @@ +import adapter from '@sveltejs/adapter-auto'; import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [sveltekit()] + 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-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.ts index 9c72d3491..649d6b1d9 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.ts @@ -1,13 +1,29 @@ 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: { 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 }; } 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 }); + } } } 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/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.ts b/packages/sv/src/create/shared/vite.config.ts index bbf8c7da4..cb76b810c 100644 --- a/packages/sv/src/create/shared/vite.config.ts +++ b/packages/sv/src/create/shared/vite.config.ts @@ -1,6 +1,20 @@ +import adapter from '@sveltejs/adapter-auto'; import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [sveltekit()] + 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/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/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/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/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/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/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() + }) + ] }); diff --git a/packages/sv/src/create/tests/playground.ts b/packages/sv/src/create/tests/playground.ts index 5d7778de0..82bbab63a 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.ts'); + 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.ts'); + const viteConfigContent = fs.readFileSync(viteConfigPath, 'utf-8'); + expect(viteConfigContent).not.toContain('experimental: { async: true }'); }); 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