diff --git a/.changeset/fresh-hotels-behave.md b/.changeset/fresh-hotels-behave.md
new file mode 100644
index 000000000..7ab3c81f5
--- /dev/null
+++ b/.changeset/fresh-hotels-behave.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/vite-plugin-svelte': patch
+---
+
+allow compilerOptions in svelte.config.js to be a function ({filename,code})=>CompileOptions
diff --git a/.prettierrc.js b/.prettierrc.js
index 044917ed6..4377a9276 100644
--- a/.prettierrc.js
+++ b/.prettierrc.js
@@ -24,6 +24,7 @@ export default {
'.changeset/pre.json',
'**/vite.config.js.timestamp-*.mjs',
'packages/e2e-tests/dynamic-compile-options/src/components/A.svelte',
+ 'packages/e2e-tests/dynamic-compile-options/src/components/C.svelte',
'packages/playground/big/src/pages/**', // lots of generated files
'packages/e2e-tests/scan-deps/src/Svelte*.svelte' // various syntax tests that require no format
],
diff --git a/packages/e2e-tests/dynamic-compile-options/__tests__/dynamic-compile-options.spec.ts b/packages/e2e-tests/dynamic-compile-options/__tests__/dynamic-compile-options.spec.ts
index 210eec30f..2a1d9b01e 100644
--- a/packages/e2e-tests/dynamic-compile-options/__tests__/dynamic-compile-options.spec.ts
+++ b/packages/e2e-tests/dynamic-compile-options/__tests__/dynamic-compile-options.spec.ts
@@ -1,6 +1,7 @@
import { getText } from '~utils';
-test('should respect dynamic compile option preserveWhitespace: true for A', async () => {
+test('should respect dynamic compile option preserveWhitespace: true', async () => {
expect(await getText('#A')).toBe(' preserved leading whitespace');
expect(await getText('#B')).toBe('removed leading whitespace');
+ expect(await getText('#C')).toBe(' preserved leading whitespace');
});
diff --git a/packages/e2e-tests/dynamic-compile-options/src/App.svelte b/packages/e2e-tests/dynamic-compile-options/src/App.svelte
index 0fade0148..5551f2855 100644
--- a/packages/e2e-tests/dynamic-compile-options/src/App.svelte
+++ b/packages/e2e-tests/dynamic-compile-options/src/App.svelte
@@ -1,7 +1,9 @@
+
diff --git a/packages/e2e-tests/dynamic-compile-options/src/components/C.svelte b/packages/e2e-tests/dynamic-compile-options/src/components/C.svelte
new file mode 100644
index 000000000..1befea4f7
--- /dev/null
+++ b/packages/e2e-tests/dynamic-compile-options/src/components/C.svelte
@@ -0,0 +1 @@
+
preserved leading whitespace
diff --git a/packages/e2e-tests/dynamic-compile-options/vite.config.js b/packages/e2e-tests/dynamic-compile-options/vite.config.js
index b3ed4b6f0..30817b057 100644
--- a/packages/e2e-tests/dynamic-compile-options/vite.config.js
+++ b/packages/e2e-tests/dynamic-compile-options/vite.config.js
@@ -11,6 +11,18 @@ export default defineConfig(() => {
preserveWhitespace: true
};
}
+ },
+ compilerOptions({ filename }) {
+ if (filename.endsWith('A.svelte')) {
+ return {
+ preserveWhitespace: false // should be overridden by dynamicCompileOptions above
+ };
+ }
+ if (filename.endsWith('C.svelte')) {
+ return {
+ preserveWhitespace: true
+ };
+ }
}
})
],
diff --git a/packages/vite-plugin-svelte/src/index.js b/packages/vite-plugin-svelte/src/index.js
index 9ec43e9ec..461b48ebe 100644
--- a/packages/vite-plugin-svelte/src/index.js
+++ b/packages/vite-plugin-svelte/src/index.js
@@ -190,9 +190,6 @@ export function svelte(inlineOptions) {
},
handleHotUpdate(ctx) {
- if (!options.compilerOptions.hmr || !options.emitCss) {
- return;
- }
const svelteRequest = requestParser(ctx.file, false, ctx.timestamp);
if (svelteRequest) {
return handleHotUpdate(compileSvelte, ctx, svelteRequest, cache, options);
diff --git a/packages/vite-plugin-svelte/src/public.d.ts b/packages/vite-plugin-svelte/src/public.d.ts
index 6335e1aa7..5adc6ae83 100644
--- a/packages/vite-plugin-svelte/src/public.d.ts
+++ b/packages/vite-plugin-svelte/src/public.d.ts
@@ -1,6 +1,7 @@
import type { InlineConfig, ResolvedConfig } from 'vite';
import type { CompileOptions, Warning, PreprocessorGroup } from 'svelte/compiler';
import type { Options as InspectorOptions } from '@sveltejs/vite-plugin-svelte-inspector';
+import type { DynamicRestrictedSvelteCompileOptions } from './types/options.d.ts';
export type Options = Omit & PluginOptionsInline;
@@ -131,7 +132,7 @@ export interface SvelteConfig {
*
* @see https://svelte.dev/docs#svelte_compile
*/
- compilerOptions?: Omit;
+ compilerOptions?: DynamicRestrictedSvelteCompileOptions;
/**
* Handles warning emitted from the Svelte compiler
diff --git a/packages/vite-plugin-svelte/src/types/compile.d.ts b/packages/vite-plugin-svelte/src/types/compile.d.ts
index d6ba48e0f..0a9bf4c71 100644
--- a/packages/vite-plugin-svelte/src/types/compile.d.ts
+++ b/packages/vite-plugin-svelte/src/types/compile.d.ts
@@ -5,7 +5,7 @@ import type { ResolvedOptions } from './options.d.ts';
export type CompileSvelte = (
svelteRequest: SvelteRequest,
code: string,
- options: Partial
+ options: ResolvedOptions
) => Promise;
export interface Code {
diff --git a/packages/vite-plugin-svelte/src/types/options.d.ts b/packages/vite-plugin-svelte/src/types/options.d.ts
index 5b4ca4274..b22a50b3a 100644
--- a/packages/vite-plugin-svelte/src/types/options.d.ts
+++ b/packages/vite-plugin-svelte/src/types/options.d.ts
@@ -4,9 +4,17 @@ import type { ViteDevServer } from 'vite';
import { VitePluginSvelteStats } from '../utils/vite-plugin-svelte-stats.js';
import type { Options } from '../public.d.ts';
+export type RestrictedSvelteCompileOptions = Omit<
+ CompileOptions,
+ 'filename' | 'format' | 'generate'
+>;
+export type DynamicRestrictedSvelteCompileOptions =
+ | RestrictedSvelteCompileOptions
+ | ((args: { filename: string; code: string }) => RestrictedSvelteCompileOptions);
+
export interface PreResolvedOptions extends Options {
// these options are non-nullable after resolve
- compilerOptions: CompileOptions;
+ compilerOptions: DynamicRestrictedSvelteCompileOptions;
// extra options
root: string;
isBuild: boolean;
diff --git a/packages/vite-plugin-svelte/src/utils/compile.js b/packages/vite-plugin-svelte/src/utils/compile.js
index 4c0e524c9..5b48411d4 100644
--- a/packages/vite-plugin-svelte/src/utils/compile.js
+++ b/packages/vite-plugin-svelte/src/utils/compile.js
@@ -3,12 +3,10 @@ import * as svelte from 'svelte/compiler';
import { safeBase64Hash } from './hash.js';
import { log } from './log.js';
-import {
- checkPreprocessDependencies,
- createInjectScopeEverythingRulePreprocessorGroup
-} from './preprocess.js';
+import { checkPreprocessDependencies } from './preprocess.js';
import { mapToRelative } from './sourcemaps.js';
import { enhanceCompileError } from './error.js';
+import { enforceCompilerOptions } from './options.js';
// TODO this is a patched version of https://github.com/sveltejs/vite-plugin-svelte/pull/796/files#diff-3bce0b33034aad4b35ca094893671f7e7ddf4d27254ae7b9b0f912027a001b15R10
// which is closer to the other regexes in at least not falling into commented script
@@ -22,11 +20,10 @@ const scriptLangRE =
export function createCompileSvelte() {
/** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */
let stats;
- const devStylePreprocessor = createInjectScopeEverythingRulePreprocessorGroup();
/** @type {import('../types/compile.d.ts').CompileSvelte} */
return async function compileSvelte(svelteRequest, code, options) {
const { filename, normalizedFilename, cssId, ssr, raw } = svelteRequest;
- const { emitCss = true } = options;
+
/** @type {string[]} */
const dependencies = [];
/** @type {import('svelte/compiler').Warning[]} */
@@ -56,30 +53,10 @@ export function createCompileSvelte() {
// also they for hmr updates too
}
}
- /** @type {import('svelte/compiler').CompileOptions} */
- const compileOptions = {
- ...options.compilerOptions,
- filename,
- generate: ssr ? 'server' : 'client'
- };
-
- if (compileOptions.hmr && options.emitCss) {
- const hash = `s-${safeBase64Hash(normalizedFilename)}`;
- compileOptions.cssHash = () => hash;
- }
let preprocessed;
- let preprocessors = options.preprocess;
- if (!options.isBuild && options.emitCss && compileOptions.hmr) {
- // inject preprocessor that ensures css hmr works better
- if (!Array.isArray(preprocessors)) {
- preprocessors = preprocessors
- ? [preprocessors, devStylePreprocessor]
- : [devStylePreprocessor];
- } else {
- preprocessors = preprocessors.concat(devStylePreprocessor);
- }
- }
+ const preprocessors = options.preprocess;
+
if (preprocessors) {
try {
preprocessed = await svelte.preprocess(code, preprocessors, { filename }); // full filename here so postcss works
@@ -97,8 +74,6 @@ export function createCompileSvelte() {
dependencies.push(...checked.dependencies);
}
}
-
- if (preprocessed.map) compileOptions.sourcemap = preprocessed.map;
}
if (typeof preprocessed?.map === 'object') {
mapToRelative(preprocessed?.map, filename);
@@ -109,7 +84,32 @@ export function createCompileSvelte() {
preprocessed: preprocessed ?? { code }
};
}
- const finalCode = preprocessed ? preprocessed.code : code;
+ let finalCode = preprocessed ? preprocessed.code : code;
+ /**@type import('svelte/compiler').CompileOptions */
+ const compileOptions = {
+ css: options.emitCss ? 'external' : 'injected',
+ dev: !options.isProduction,
+ hmr:
+ !options.isProduction &&
+ !options.isBuild &&
+ options.server &&
+ options.server.config.server.hmr !== false,
+ ...(typeof options.compilerOptions === 'function'
+ ? options.compilerOptions({ filename, code: finalCode })
+ : options.compilerOptions)
+ };
+
+ enforceCompilerOptions(compileOptions, options);
+ compileOptions.filename = filename;
+ compileOptions.generate = ssr ? 'server' : 'client';
+ if (preprocessed?.map) {
+ compileOptions.sourcemap = preprocessed.map;
+ }
+ if (compileOptions.hmr && options.emitCss) {
+ const hash = `s-${safeBase64Hash(normalizedFilename)}`;
+ compileOptions.cssHash = () => hash;
+ }
+
const dynamicCompileOptions = await options?.dynamicCompileOptions?.({
filename,
code: finalCode,
@@ -122,12 +122,27 @@ export function createCompileSvelte() {
'compile'
);
}
- const finalCompileOptions = dynamicCompileOptions
- ? {
- ...compileOptions,
- ...dynamicCompileOptions
- }
- : compileOptions;
+ const finalCompileOptions = {
+ ...compileOptions,
+ ...dynamicCompileOptions
+ };
+
+ if (!options.isBuild && options.emitCss && finalCompileOptions.hmr) {
+ // use css preprocessor to inject rule that ensures css-only hmr works better
+ const processed = await svelte.preprocess(
+ finalCode,
+ [
+ {
+ name: 'inject-scope-everything-rule',
+ // no sourcemap, we just append 4 chars at the last line so no shifts
+ style: ({ content }) => ({ code: `${content ?? ''} *{}` })
+ }
+ ],
+ { filename }
+ );
+ finalCode = processed.code;
+ }
+
const endStat = stats?.start(filename);
/** @type {import('svelte/compiler').CompileResult} */
let compiled;
@@ -164,7 +179,7 @@ export function createCompileSvelte() {
// wire css import and code for hmr
const hasCss = compiled.css?.code?.trim()?.length ?? 0 > 0;
// compiler might not emit css with mode none or it may be empty
- if (emitCss && hasCss) {
+ if (options.emitCss && hasCss) {
// TODO properly update sourcemap?
compiled.js.code += `\nimport ${JSON.stringify(cssId)};\n`;
}
diff --git a/packages/vite-plugin-svelte/src/utils/esbuild.js b/packages/vite-plugin-svelte/src/utils/esbuild.js
index 5a1ca89b9..2936c99d1 100644
--- a/packages/vite-plugin-svelte/src/utils/esbuild.js
+++ b/packages/vite-plugin-svelte/src/utils/esbuild.js
@@ -56,16 +56,13 @@ export function esbuildSveltePlugin(options) {
* @returns {Promise}
*/
async function compileSvelte(options, { filename, code }, statsCollection) {
- let css = options.compilerOptions.css;
- if (css !== 'injected') {
- // TODO ideally we'd be able to externalize prebundled styles too, but for now always put them in the js
- css = 'injected';
- }
/** @type {import('svelte/compiler').CompileOptions} */
const compileOptions = {
dev: true, // default to dev: true because prebundling is only used in dev
- ...options.compilerOptions,
- css,
+ ...(typeof options.compilerOptions === 'function'
+ ? options.compilerOptions({ filename, code })
+ : options.compilerOptions),
+ css: 'injected', // TODO ideally we'd be able to externalize prebundled styles too, but for now always put them in the js
filename,
generate: 'client'
};
@@ -163,8 +160,14 @@ export function esbuildSvelteModulePlugin(options) {
*/
async function compileSvelteModule(options, { filename, code }, statsCollection) {
const endStat = statsCollection?.start(filename);
+ // default to dev: true because prebundling is only used in dev
+ const dev =
+ (typeof options.compilerOptions === 'function'
+ ? options.compilerOptions({ filename, code })
+ : options.compilerOptions
+ )?.dev ?? true;
const compiled = svelte.compileModule(code, {
- dev: options.compilerOptions?.dev ?? true, // default to dev: true because prebundling is only used in dev
+ dev,
filename,
generate: 'client'
});
diff --git a/packages/vite-plugin-svelte/src/utils/options.js b/packages/vite-plugin-svelte/src/utils/options.js
index 6582e6e7e..cd9e01cd6 100644
--- a/packages/vite-plugin-svelte/src/utils/options.js
+++ b/packages/vite-plugin-svelte/src/utils/options.js
@@ -198,20 +198,10 @@ function mergeConfigs(...configs) {
* @returns {import('../types/options.d.ts').ResolvedOptions}
*/
export function resolveOptions(preResolveOptions, viteConfig, cache) {
- const css = preResolveOptions.emitCss ? 'external' : 'injected';
/** @type {Partial} */
const defaultOptions = {
- compilerOptions: {
- css,
- dev: !viteConfig.isProduction,
- hmr:
- !viteConfig.isProduction &&
- !preResolveOptions.isBuild &&
- viteConfig.server &&
- viteConfig.server.hmr !== false
- }
+ emitCss: true
};
-
/** @type {Partial} */
const extraOptions = {
root: viteConfig.root,
@@ -221,11 +211,9 @@ export function resolveOptions(preResolveOptions, viteConfig, cache) {
mergeConfigs(defaultOptions, preResolveOptions, extraOptions)
);
- removeIgnoredOptions(merged);
handleDeprecatedOptions(merged);
addExtraPreprocessors(merged, viteConfig);
- enforceOptionsForHmr(merged, viteConfig);
- enforceOptionsForProduction(merged);
+
// mergeConfigs would mangle functions on the stats class, so do this afterwards
if (log.debug.enabled && isDebugNamespaceEnabled('stats')) {
merged.stats = new VitePluginSvelteStats(cache);
@@ -234,64 +222,53 @@ export function resolveOptions(preResolveOptions, viteConfig, cache) {
}
/**
+ * @param {import('svelte/compiler').CompileOptions} compilerOptions
* @param {import('../types/options.d.ts').ResolvedOptions} options
- * @param {import('vite').ResolvedConfig} viteConfig
*/
-function enforceOptionsForHmr(options, viteConfig) {
+export function enforceCompilerOptions(compilerOptions, options) {
if (options.hot) {
- log.warn(
+ log.warn.once(
'svelte 5 has hmr integrated in core. Please remove the vitePlugin.hot option and use compilerOptions.hmr instead'
);
- delete options.hot;
- options.compilerOptions.hmr = true;
+ compilerOptions.hmr = true;
}
- if (options.compilerOptions.hmr && viteConfig.server?.hmr === false) {
- log.warn(
+ if (compilerOptions.hmr && options.server?.config.server.hmr === false) {
+ log.warn.once(
'vite config server.hmr is false but compilerOptions.hmr is true. Forcing compilerOptions.hmr to false as it would not work.'
);
- options.compilerOptions.hmr = false;
+ compilerOptions.hmr = false;
}
-}
-/**
- * @param {import('../types/options.d.ts').ResolvedOptions} options
- */
-function enforceOptionsForProduction(options) {
if (options.isProduction) {
- if (options.compilerOptions.hmr) {
- log.warn(
+ if (compilerOptions.hmr) {
+ log.warn.once(
'you are building for production but compilerOptions.hmr is true, forcing it to false'
);
- options.compilerOptions.hmr = false;
+ compilerOptions.hmr = false;
}
- if (options.compilerOptions.dev) {
- log.warn(
+ if (compilerOptions.dev) {
+ log.warn.once(
'you are building for production but compilerOptions.dev is true, forcing it to false'
);
- options.compilerOptions.dev = false;
+ compilerOptions.dev = false;
}
}
-}
-/**
- * @param {import('../types/options.d.ts').ResolvedOptions} options
- */
-function removeIgnoredOptions(options) {
const ignoredCompilerOptions = ['generate', 'format', 'filename'];
- if (options.compilerOptions.hmr && options.emitCss) {
+ if (compilerOptions.hmr && options.emitCss) {
ignoredCompilerOptions.push('cssHash');
}
- const passedCompilerOptions = Object.keys(options.compilerOptions || {});
+ const passedCompilerOptions = Object.keys(compilerOptions || {});
const passedIgnored = passedCompilerOptions.filter((o) => ignoredCompilerOptions.includes(o));
if (passedIgnored.length) {
- log.warn(
+ log.warn.once(
`The following Svelte compilerOptions are controlled by vite-plugin-svelte and essential to its functionality. User-specified values are ignored. Please remove them from your configuration: ${passedIgnored.join(
', '
)}`
);
passedIgnored.forEach((ignored) => {
// @ts-expect-error string access
- delete options.compilerOptions[ignored];
+ delete compilerOptions[ignored];
});
}
}
diff --git a/packages/vite-plugin-svelte/src/utils/preprocess.js b/packages/vite-plugin-svelte/src/utils/preprocess.js
index 602c649b5..81db187c9 100644
--- a/packages/vite-plugin-svelte/src/utils/preprocess.js
+++ b/packages/vite-plugin-svelte/src/utils/preprocess.js
@@ -1,33 +1,6 @@
-import MagicString from 'magic-string';
import { log } from './log.js';
-import path from 'node:path';
import { normalizePath } from 'vite';
-/**
- * this appends a *{} rule to component styles to force the svelte compiler to add style classes to all nodes
- * That means adding/removing class rules from