From feb1b56f2e9d569f3299b435f7b2d0e36d63c2b8 Mon Sep 17 00:00:00 2001 From: dominikg Date: Sat, 14 Dec 2024 21:59:04 +0100 Subject: [PATCH 1/6] feat: compilerOptions as function in root of svelte.config.js --- packages/vite-plugin-svelte/src/index.js | 5 +- packages/vite-plugin-svelte/src/public.d.ts | 3 +- .../vite-plugin-svelte/src/types/compile.d.ts | 2 +- .../vite-plugin-svelte/src/types/options.d.ts | 10 +- .../vite-plugin-svelte/src/utils/compile.js | 91 +++++++++++-------- .../vite-plugin-svelte/src/utils/esbuild.js | 19 ++-- .../vite-plugin-svelte/src/utils/options.js | 61 ++++--------- .../src/utils/preprocess.js | 27 ------ packages/vite-plugin-svelte/types/index.d.ts | 9 +- .../vite-plugin-svelte/types/index.d.ts.map | 6 +- 10 files changed, 112 insertions(+), 121 deletions(-) diff --git a/packages/vite-plugin-svelte/src/index.js b/packages/vite-plugin-svelte/src/index.js index 9ec43e9ec..778dcf59c 100644 --- a/packages/vite-plugin-svelte/src/index.js +++ b/packages/vite-plugin-svelte/src/index.js @@ -190,7 +190,10 @@ export function svelte(inlineOptions) { }, handleHotUpdate(ctx) { - if (!options.compilerOptions.hmr || !options.emitCss) { + if ( + (typeof options.compilerOptions === 'object' && !options.compilerOptions.hmr) || + !options.emitCss + ) { return; } const svelteRequest = requestParser(ctx.file, false, ctx.timestamp); 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