From 7cdc1df125bd4303f74e55cab42f5e2a680ba4ea Mon Sep 17 00:00:00 2001 From: dominikg Date: Wed, 18 Jun 2025 10:56:20 +0200 Subject: [PATCH 01/18] wip: first steps --- .../vite-plugin-svelte/src/index-modular.js | 30 ++++++ .../src/plugins/compile-module.js | 8 ++ .../vite-plugin-svelte/src/plugins/compile.js | 8 ++ .../vite-plugin-svelte/src/plugins/config.js | 92 +++++++++++++++++++ .../src/plugins/external-css.js | 8 ++ .../src/plugins/optimize-module.js | 8 ++ .../src/plugins/optimize.js | 8 ++ .../src/plugins/preprocess.js | 8 ++ .../src/types/plugin-api.d.ts | 13 ++- .../vite-plugin-svelte/src/utils/options.js | 8 +- .../vite-plugin-svelte/src/utils/watch.js | 16 +--- 11 files changed, 184 insertions(+), 23 deletions(-) create mode 100644 packages/vite-plugin-svelte/src/index-modular.js create mode 100644 packages/vite-plugin-svelte/src/plugins/compile-module.js create mode 100644 packages/vite-plugin-svelte/src/plugins/compile.js create mode 100644 packages/vite-plugin-svelte/src/plugins/config.js create mode 100644 packages/vite-plugin-svelte/src/plugins/external-css.js create mode 100644 packages/vite-plugin-svelte/src/plugins/optimize-module.js create mode 100644 packages/vite-plugin-svelte/src/plugins/optimize.js create mode 100644 packages/vite-plugin-svelte/src/plugins/preprocess.js diff --git a/packages/vite-plugin-svelte/src/index-modular.js b/packages/vite-plugin-svelte/src/index-modular.js new file mode 100644 index 000000000..4a171cfc3 --- /dev/null +++ b/packages/vite-plugin-svelte/src/index-modular.js @@ -0,0 +1,30 @@ +import { config } from './plugins/config.js'; +import { preprocess } from './plugins/preprocess.js'; +import { compile } from './plugins/compile.js'; +import { externalCss } from './plugins/external-css.js'; +import { optimize } from './plugins/optimize.js'; +import { optimizeModule } from './plugins/optimize-module.js'; +import { compileModule } from './plugins/compile-module.js'; +import { svelteInspector } from '@sveltejs/vite-plugin-svelte-inspector'; +/** + * returns a list of plugins to handle svelte files + * plugins are named `vite-plugin-svelte:` + * + * @param {Partial} [inlineOptions] + * @returns {import('vite').Plugin[]} + */ +export function svelte(inlineOptions) { + return [ + config(inlineOptions), // parse config and put it on api.__internal for the other plugins to use + optimize(), // create optimize plugin + preprocess(), // preprocess .svelte files + compile(), // compile .svelte files + externalCss(), // return vitrual css modules created by compile + optimizeModule(), // create optimize module plugin + compileModule(),// compile module + svelteInspector() + ]; +} + +export { vitePreprocess } from './preprocess.js'; +export { loadSvelteConfig } from './utils/load-svelte-config.js'; diff --git a/packages/vite-plugin-svelte/src/plugins/compile-module.js b/packages/vite-plugin-svelte/src/plugins/compile-module.js new file mode 100644 index 000000000..99c815302 --- /dev/null +++ b/packages/vite-plugin-svelte/src/plugins/compile-module.js @@ -0,0 +1,8 @@ +/** + * @returns {import('vite').Plugin} + */ +export function compileModule() { + return { + name: 'vite-plugin-svelte:compile-module' + }; +} diff --git a/packages/vite-plugin-svelte/src/plugins/compile.js b/packages/vite-plugin-svelte/src/plugins/compile.js new file mode 100644 index 000000000..55fa3b20b --- /dev/null +++ b/packages/vite-plugin-svelte/src/plugins/compile.js @@ -0,0 +1,8 @@ +/** + * @returns {import('vite').Plugin} + */ +export function compile() { + return { + name: 'vite-plugin-svelte:compile' + }; +} diff --git a/packages/vite-plugin-svelte/src/plugins/config.js b/packages/vite-plugin-svelte/src/plugins/config.js new file mode 100644 index 000000000..7063782f3 --- /dev/null +++ b/packages/vite-plugin-svelte/src/plugins/config.js @@ -0,0 +1,92 @@ +import process from 'node:process'; +import { log } from '../utils/log.js'; + +import { + buildExtraViteConfig, + validateInlineOptions, + resolveOptions, + preResolveOptions, + ensureConfigEnvironmentMainFields, + ensureConfigEnvironmentConditions +} from '../utils/options.js'; +import { setupWatchers } from '../utils/watch.js'; + +import * as vite from 'vite'; +// @ts-expect-error rolldownVersion +const { version: viteVersion, rolldownVersion } = vite; + +/** @typedef {import('../types/plugin-api.d.ts').PluginAPI} PluginAPI */ + +/** + * @param {Partial} [inlineOptions] + * @returns {import('vite').Plugin} + */ +export function config(inlineOptions) { + if (process.env.DEBUG != null) { + log.setLevel('debug'); + } + if (rolldownVersion) { + log.warn.once( + `!!! Support for rolldown-vite in vite-plugin-svelte is experimental (rolldown: ${rolldownVersion}, vite: ${viteVersion}) !!!` + ); + } + + validateInlineOptions(inlineOptions); + + /** @type {PluginAPI} */ + const api = { + __internal: { + // @ts-expect-error set in config hook + options: {} + } + }; + + /** @type {import('vite').Plugin} */ + return { + name: 'vite-plugin-svelte:config', + // make sure it runs first + enforce: 'pre', + api, + async config(config, configEnv) { + // setup logger + if (process.env.DEBUG) { + log.setLevel('debug'); + } else if (config.logLevel) { + log.setLevel(config.logLevel); + } + + const options = await preResolveOptions(inlineOptions, config, configEnv); + // @ts-expect-error temporarily lend the options variable until fixed in configResolved + api.__internal.options = options; + // extra vite config + const extraViteConfig = await buildExtraViteConfig(options, config); + log.debug('additional vite config', extraViteConfig, 'config'); + return extraViteConfig; + }, + + configResolved: { + order: 'pre', // we assign internal api here, make sure it really is first before our other plugins + handler(config) { + const options = resolveOptions(api.__internal.options, config); + api.__internal.options = options; + log.debug('resolved options', options, 'config'); + } + }, + + configEnvironment(name, config, opts) { + ensureConfigEnvironmentMainFields(name, config, opts); + // @ts-expect-error the function above should make `resolve.mainFields` non-nullable + config.resolve.mainFields.unshift('svelte'); + + ensureConfigEnvironmentConditions(name, config, opts); + // @ts-expect-error the function above should make `resolve.conditions` non-nullable + config.resolve.conditions.push('svelte'); + }, + + configureServer(server) { + const { options, cache } = api.__internal; + options.server = server; + setupWatchers(options, cache, requestParser); + } + }; +} diff --git a/packages/vite-plugin-svelte/src/plugins/external-css.js b/packages/vite-plugin-svelte/src/plugins/external-css.js new file mode 100644 index 000000000..8e0fdcd38 --- /dev/null +++ b/packages/vite-plugin-svelte/src/plugins/external-css.js @@ -0,0 +1,8 @@ +/** + * @returns {import('vite').Plugin} + */ +export function externalCss() { + return { + name: 'vite-plugin-svelte:externalCss' + }; +} diff --git a/packages/vite-plugin-svelte/src/plugins/optimize-module.js b/packages/vite-plugin-svelte/src/plugins/optimize-module.js new file mode 100644 index 000000000..7baea6cdd --- /dev/null +++ b/packages/vite-plugin-svelte/src/plugins/optimize-module.js @@ -0,0 +1,8 @@ +/** + * @returns {import('vite').Plugin} + */ +export function optimizeModule() { + return { + name: 'vite-plugin-svelte:optimize-module' + }; +} diff --git a/packages/vite-plugin-svelte/src/plugins/optimize.js b/packages/vite-plugin-svelte/src/plugins/optimize.js new file mode 100644 index 000000000..3cb7b8975 --- /dev/null +++ b/packages/vite-plugin-svelte/src/plugins/optimize.js @@ -0,0 +1,8 @@ +/** + * @returns {import('vite').Plugin} + */ +export function optimize() { + return { + name: 'vite-plugin-svelte:optimize' + }; +} diff --git a/packages/vite-plugin-svelte/src/plugins/preprocess.js b/packages/vite-plugin-svelte/src/plugins/preprocess.js new file mode 100644 index 000000000..5584c5832 --- /dev/null +++ b/packages/vite-plugin-svelte/src/plugins/preprocess.js @@ -0,0 +1,8 @@ +/** + * @returns {import('vite').Plugin} + */ +export function preprocess() { + return { + name: 'vite-plugin-svelte:preprocess' + }; +} diff --git a/packages/vite-plugin-svelte/src/types/plugin-api.d.ts b/packages/vite-plugin-svelte/src/types/plugin-api.d.ts index 36c42b6ae..b56563adc 100644 --- a/packages/vite-plugin-svelte/src/types/plugin-api.d.ts +++ b/packages/vite-plugin-svelte/src/types/plugin-api.d.ts @@ -1,11 +1,16 @@ import type { ResolvedOptions } from './options.d.ts'; +import { VitePluginSvelteCache } from '../utils/vite-plugin-svelte-cache.js'; +import { VitePluginSvelteStats } from '../utils/vite-plugin-svelte-stats.js'; export interface PluginAPI { /** - * must not be modified, should not be used outside of vite-plugin-svelte repo + * must not be used by plugins outside of the vite-plugin-svelte monorepo + * this is not part of our public semver contract, breaking changes to it can and will happen in patch releases * @internal - * @experimental */ - options?: ResolvedOptions; - // TODO expose compile cache here so other utility plugins can use it + __internal: { + options: ResolvedOptions; + cache?: VitePluginSvelteCache; + stats?: VitePluginSvelteStats; + }; } diff --git a/packages/vite-plugin-svelte/src/utils/options.js b/packages/vite-plugin-svelte/src/utils/options.js index 9c83a0edc..0507dedfd 100644 --- a/packages/vite-plugin-svelte/src/utils/options.js +++ b/packages/vite-plugin-svelte/src/utils/options.js @@ -198,10 +198,9 @@ function mergeConfigs(...configs) { * * @param {import('../types/options.d.ts').PreResolvedOptions} preResolveOptions * @param {import('vite').ResolvedConfig} viteConfig - * @param {import('./vite-plugin-svelte-cache.js').VitePluginSvelteCache} cache * @returns {import('../types/options.d.ts').ResolvedOptions} */ -export function resolveOptions(preResolveOptions, viteConfig, cache) { +export function resolveOptions(preResolveOptions, viteConfig) { const css = preResolveOptions.emitCss ? 'external' : 'injected'; /** @type {Partial} */ const defaultOptions = { @@ -230,10 +229,7 @@ export function resolveOptions(preResolveOptions, viteConfig, cache) { 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); - } + return merged; } diff --git a/packages/vite-plugin-svelte/src/utils/watch.js b/packages/vite-plugin-svelte/src/utils/watch.js index de711cf16..5ed90bc29 100644 --- a/packages/vite-plugin-svelte/src/utils/watch.js +++ b/packages/vite-plugin-svelte/src/utils/watch.js @@ -6,10 +6,9 @@ import path from 'node:path'; /** * @param {import('../types/options.d.ts').ResolvedOptions} options * @param {import('./vite-plugin-svelte-cache.js').VitePluginSvelteCache} cache - * @param {import('../types/id.d.ts').IdParser} requestParser * @returns {void} */ -export function setupWatchers(options, cache, requestParser) { +export function setupWatchers(options, cache) { const { server, configFile: svelteConfigFile } = options; if (!server) { return; @@ -30,16 +29,7 @@ export function setupWatchers(options, cache, requestParser) { } }); }; - /** @type {(filename: string) => void} */ - const removeUnlinkedFromCache = (filename) => { - const svelteRequest = requestParser(filename, false); - if (svelteRequest) { - const removedFromCache = cache.remove(svelteRequest); - if (removedFromCache) { - log.debug(`cleared VitePluginSvelteCache for deleted file ${filename}`, undefined, 'hmr'); - } - } - }; + /** @type {(filename: string) => void} */ const triggerViteRestart = (filename) => { if (serverConfig.middlewareMode) { @@ -63,7 +53,7 @@ export function setupWatchers(options, cache, requestParser) { const listenerCollection = { add: [], change: [emitChangeEventOnDependants], - unlink: [removeUnlinkedFromCache, emitChangeEventOnDependants] + unlink: [emitChangeEventOnDependants] }; if (svelteConfigFile !== false) { From f02a1743a683975366d4e1e2bf02afec4502b57f Mon Sep 17 00:00:00 2001 From: dominikg Date: Wed, 18 Jun 2025 22:24:49 +0200 Subject: [PATCH 02/18] wip: second day, getting a hang for it --- .../configfile-custom/svelte.config.ts | 2 + .../src/handle-hot-update.js | 2 +- .../vite-plugin-svelte/src/index-modular.js | 32 +- .../vite-plugin-svelte/src/plugins/config.js | 92 ----- .../src/plugins/configure.js | 172 +++++++++ .../src/plugins/external-css.js | 8 - .../src/plugins/load-compiled-css.js | 39 ++ .../src/plugins/load-custom.js | 171 +++++++++ .../src/plugins/optimize-module.js | 8 - .../src/plugins/optimize.js | 8 - .../src/plugins/setup-optimizer.js | 339 ++++++++++++++++++ .../vite-plugin-svelte/src/types/compile.d.ts | 1 + .../src/types/plugin-api.d.ts | 22 +- .../vite-plugin-svelte/src/utils/compile.js | 1 + .../vite-plugin-svelte/src/utils/optimizer.js | 53 --- .../src/utils/vite-plugin-svelte-cache.js | 9 +- 16 files changed, 765 insertions(+), 194 deletions(-) create mode 100644 packages/e2e-tests/configfile-custom/svelte.config.ts delete mode 100644 packages/vite-plugin-svelte/src/plugins/config.js create mode 100644 packages/vite-plugin-svelte/src/plugins/configure.js delete mode 100644 packages/vite-plugin-svelte/src/plugins/external-css.js create mode 100644 packages/vite-plugin-svelte/src/plugins/load-compiled-css.js create mode 100644 packages/vite-plugin-svelte/src/plugins/load-custom.js delete mode 100644 packages/vite-plugin-svelte/src/plugins/optimize-module.js delete mode 100644 packages/vite-plugin-svelte/src/plugins/optimize.js create mode 100644 packages/vite-plugin-svelte/src/plugins/setup-optimizer.js delete mode 100644 packages/vite-plugin-svelte/src/utils/optimizer.js diff --git a/packages/e2e-tests/configfile-custom/svelte.config.ts b/packages/e2e-tests/configfile-custom/svelte.config.ts new file mode 100644 index 000000000..e92845e0b --- /dev/null +++ b/packages/e2e-tests/configfile-custom/svelte.config.ts @@ -0,0 +1,2 @@ +console.log('default svelte config loaded'); +export default {}; diff --git a/packages/vite-plugin-svelte/src/handle-hot-update.js b/packages/vite-plugin-svelte/src/handle-hot-update.js index 82758e55a..5c880f65d 100644 --- a/packages/vite-plugin-svelte/src/handle-hot-update.js +++ b/packages/vite-plugin-svelte/src/handle-hot-update.js @@ -24,7 +24,7 @@ export async function handleHotUpdate(compileSvelte, ctx, svelteRequest, cache, const { read, server, modules } = ctx; const cachedJS = cache.getJS(svelteRequest); - const cachedCss = cache.getCSS(svelteRequest); + const cachedCss = cache.getCSS(svelteRequest.cssId); const content = await read(); /** @type {import('./types/compile.d.ts').CompileData} */ diff --git a/packages/vite-plugin-svelte/src/index-modular.js b/packages/vite-plugin-svelte/src/index-modular.js index 4a171cfc3..47b30c28e 100644 --- a/packages/vite-plugin-svelte/src/index-modular.js +++ b/packages/vite-plugin-svelte/src/index-modular.js @@ -1,11 +1,12 @@ -import { config } from './plugins/config.js'; +import { configure } from './plugins/configure.js'; import { preprocess } from './plugins/preprocess.js'; import { compile } from './plugins/compile.js'; -import { externalCss } from './plugins/external-css.js'; -import { optimize } from './plugins/optimize.js'; +import { loadCompiledCss } from './plugins/load-compiled-css.js'; +import { setupOptimizer } from './plugins/setup-optimizer.js'; import { optimizeModule } from './plugins/optimize-module.js'; import { compileModule } from './plugins/compile-module.js'; import { svelteInspector } from '@sveltejs/vite-plugin-svelte-inspector'; +import {loadCustom} from "./plugins/load-custom.js"; /** * returns a list of plugins to handle svelte files * plugins are named `vite-plugin-svelte:` @@ -14,14 +15,25 @@ import { svelteInspector } from '@sveltejs/vite-plugin-svelte-inspector'; * @returns {import('vite').Plugin[]} */ export function svelte(inlineOptions) { + /** @type {import('./types/plugin-api.js').PluginAPI} */ + const api = { + // @ts-expect-error protection against early use + get options(){ + throw new Error('must not use configResolved') + }, + // @ts-expect-error protection against early use + get getEnvironmentState() { + throw new Error('must not use before configResolved') + } + }; return [ - config(inlineOptions), // parse config and put it on api.__internal for the other plugins to use - optimize(), // create optimize plugin - preprocess(), // preprocess .svelte files - compile(), // compile .svelte files - externalCss(), // return vitrual css modules created by compile - optimizeModule(), // create optimize module plugin - compileModule(),// compile module + configure(api,inlineOptions), // parse config and put it on api.__internal for the other plugins to use + setupOptimizer(api), // add optimizer plugins for pre-bundling in development + preprocess(api), // preprocess .svelte files + compile(api), // compile .svelte files + loadCompiledCss(api), // return virtual css modules created by compile + loadCustom(api), // return custom output d + compileModule(api),// compile module svelteInspector() ]; } diff --git a/packages/vite-plugin-svelte/src/plugins/config.js b/packages/vite-plugin-svelte/src/plugins/config.js deleted file mode 100644 index 7063782f3..000000000 --- a/packages/vite-plugin-svelte/src/plugins/config.js +++ /dev/null @@ -1,92 +0,0 @@ -import process from 'node:process'; -import { log } from '../utils/log.js'; - -import { - buildExtraViteConfig, - validateInlineOptions, - resolveOptions, - preResolveOptions, - ensureConfigEnvironmentMainFields, - ensureConfigEnvironmentConditions -} from '../utils/options.js'; -import { setupWatchers } from '../utils/watch.js'; - -import * as vite from 'vite'; -// @ts-expect-error rolldownVersion -const { version: viteVersion, rolldownVersion } = vite; - -/** @typedef {import('../types/plugin-api.d.ts').PluginAPI} PluginAPI */ - -/** - * @param {Partial} [inlineOptions] - * @returns {import('vite').Plugin} - */ -export function config(inlineOptions) { - if (process.env.DEBUG != null) { - log.setLevel('debug'); - } - if (rolldownVersion) { - log.warn.once( - `!!! Support for rolldown-vite in vite-plugin-svelte is experimental (rolldown: ${rolldownVersion}, vite: ${viteVersion}) !!!` - ); - } - - validateInlineOptions(inlineOptions); - - /** @type {PluginAPI} */ - const api = { - __internal: { - // @ts-expect-error set in config hook - options: {} - } - }; - - /** @type {import('vite').Plugin} */ - return { - name: 'vite-plugin-svelte:config', - // make sure it runs first - enforce: 'pre', - api, - async config(config, configEnv) { - // setup logger - if (process.env.DEBUG) { - log.setLevel('debug'); - } else if (config.logLevel) { - log.setLevel(config.logLevel); - } - - const options = await preResolveOptions(inlineOptions, config, configEnv); - // @ts-expect-error temporarily lend the options variable until fixed in configResolved - api.__internal.options = options; - // extra vite config - const extraViteConfig = await buildExtraViteConfig(options, config); - log.debug('additional vite config', extraViteConfig, 'config'); - return extraViteConfig; - }, - - configResolved: { - order: 'pre', // we assign internal api here, make sure it really is first before our other plugins - handler(config) { - const options = resolveOptions(api.__internal.options, config); - api.__internal.options = options; - log.debug('resolved options', options, 'config'); - } - }, - - configEnvironment(name, config, opts) { - ensureConfigEnvironmentMainFields(name, config, opts); - // @ts-expect-error the function above should make `resolve.mainFields` non-nullable - config.resolve.mainFields.unshift('svelte'); - - ensureConfigEnvironmentConditions(name, config, opts); - // @ts-expect-error the function above should make `resolve.conditions` non-nullable - config.resolve.conditions.push('svelte'); - }, - - configureServer(server) { - const { options, cache } = api.__internal; - options.server = server; - setupWatchers(options, cache, requestParser); - } - }; -} diff --git a/packages/vite-plugin-svelte/src/plugins/configure.js b/packages/vite-plugin-svelte/src/plugins/configure.js new file mode 100644 index 000000000..8284d1cf4 --- /dev/null +++ b/packages/vite-plugin-svelte/src/plugins/configure.js @@ -0,0 +1,172 @@ +import process from 'node:process'; +import path from 'node:path'; +import { log } from '../utils/log.js'; +import * as vite from 'vite'; +import { knownSvelteConfigNames } from '../utils/load-svelte-config.js'; +import { VitePluginSvelteCache } from '../utils/vite-plugin-svelte-cache.js'; +import { VitePluginSvelteStats } from '../utils/vite-plugin-svelte-stats.js'; +import { + buildExtraViteConfig, + validateInlineOptions, + resolveOptions, + preResolveOptions, + ensureConfigEnvironmentMainFields, + ensureConfigEnvironmentConditions +} from '../utils/options.js'; +import {buildIdFilter, buildIdParser} from "../utils/id.js"; +import {createCompileSvelte} from "../utils/compile.js"; + + +// @ts-expect-error rolldownVersion +const { version: viteVersion, rolldownVersion, perEnvironmentState } = vite; + +/** + * @param {Partial} [inlineOptions] + * @param {import('../types/plugin-api.d.ts').PluginAPI} api + * @returns {import('vite').Plugin} + */ +export function configure(api,inlineOptions) { + if (process.env.DEBUG != null) { + log.setLevel('debug'); + } + if (rolldownVersion) { + log.warn.once( + `!!! Support for rolldown-vite in vite-plugin-svelte is experimental (rolldown: ${rolldownVersion}, vite: ${viteVersion}) !!!` + ); + } + + validateInlineOptions(inlineOptions); + + /** + * @type {import("../types/options.d.ts").PreResolvedOptions} + */ + let preOptions; + + /** @type {import('vite').Plugin} */ + return { + name: 'vite-plugin-svelte:config', + // make sure it runs first + enforce: 'pre', + config:{ + order: 'pre', + async handler(config, configEnv) { + // setup logger + if (process.env.DEBUG) { + log.setLevel('debug'); + } else if (config.logLevel) { + log.setLevel(config.logLevel); + } + + preOptions = await preResolveOptions(inlineOptions, config, configEnv); + // extra vite config + const extraViteConfig = await buildExtraViteConfig(preOptions, config); + log.debug('additional vite config', extraViteConfig, 'config'); + return extraViteConfig; + } + }, + configResolved: { + order: 'pre', + handler(config) { + const options = resolveOptions(preOptions, config); + api.options = options; + api.getEnvironmentState = perEnvironmentState(_env => { + const cache = new VitePluginSvelteCache() + const stats = new VitePluginSvelteStats(cache) + return {cache,stats} + }) + api.idFilter = buildIdFilter(options); + + api.idParser = buildIdParser(options); + api.compileSvelte = createCompileSvelte(); + log.debug('resolved options', api.options, 'config'); + } + }, + + configEnvironment(name, config, opts) { + ensureConfigEnvironmentMainFields(name, config, opts); + // @ts-expect-error the function above should make `resolve.mainFields` non-nullable + config.resolve.mainFields.unshift('svelte'); + + ensureConfigEnvironmentConditions(name, config, opts); + // @ts-expect-error the function above should make `resolve.conditions` non-nullable + config.resolve.conditions.push('svelte'); + }, + + configureServer(server) { + const { options } = api; + options.server = server; + restartOnSvelteConfigChanges(options) + } + }; +} + +/** + * @param {import('../types/options.d.ts').ResolvedOptions} options + * @returns {void} + */ +export function restartOnSvelteConfigChanges(options) { + const { server, configFile: svelteConfigFile } = options; + if (!server) { + return; + } + const { watcher, ws } = server; + const { root, server: serverConfig } = server.config; + + + /** @type {(filename: string) => void} */ + const triggerViteRestart = (filename) => { + if (serverConfig.middlewareMode) { + // in middlewareMode we can't restart the server automatically + // show the user an overlay instead + const message = + 'Svelte config change detected, restart your dev process to apply the changes.'; + log.info(message, filename); + ws.send({ + type: 'error', + err: { message, stack: '', plugin: 'vite-plugin-svelte', id: filename } + }); + } else { + log.info(`svelte config changed: restarting vite server. - file: ${filename}`); + server.restart(); + } + }; + + // collection of watcher listeners by event + /** @type {Record} */ + const listenerCollection = { + add: [], + change: [], + unlink: [] + }; + + if (svelteConfigFile !== false) { + // configFile false means we ignore the file and external process is responsible + const possibleSvelteConfigs = knownSvelteConfigNames.map((cfg) => path.join(root, cfg)); + /** @type {(filename: string) => void} */ + const restartOnConfigAdd = (filename) => { + if (possibleSvelteConfigs.includes(filename)) { + triggerViteRestart(filename); + } + }; + + /** @type {(filename: string) => void} */ + const restartOnConfigChange = (filename) => { + if (filename === svelteConfigFile) { + triggerViteRestart(filename); + } + }; + + if (svelteConfigFile) { + listenerCollection.change.push(restartOnConfigChange); + listenerCollection.unlink.push(restartOnConfigChange); + } else { + listenerCollection.add.push(restartOnConfigAdd); + } + } + + Object.entries(listenerCollection).forEach(([evt, listeners]) => { + if (listeners.length > 0) { + watcher.on(evt, (filename) => listeners.forEach((listener) => listener(filename))); + } + }); +} diff --git a/packages/vite-plugin-svelte/src/plugins/external-css.js b/packages/vite-plugin-svelte/src/plugins/external-css.js deleted file mode 100644 index 8e0fdcd38..000000000 --- a/packages/vite-plugin-svelte/src/plugins/external-css.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @returns {import('vite').Plugin} - */ -export function externalCss() { - return { - name: 'vite-plugin-svelte:externalCss' - }; -} diff --git a/packages/vite-plugin-svelte/src/plugins/load-compiled-css.js b/packages/vite-plugin-svelte/src/plugins/load-compiled-css.js new file mode 100644 index 000000000..30fe9ca84 --- /dev/null +++ b/packages/vite-plugin-svelte/src/plugins/load-compiled-css.js @@ -0,0 +1,39 @@ +import { log } from '../utils/log.js'; +import { SVELTE_VIRTUAL_STYLE_ID_REGEX } from '../utils/constants.js'; + +const filter = { id: SVELTE_VIRTUAL_STYLE_ID_REGEX }; + +/** + * @param {import('../types/plugin-api.d.ts').PluginAPI} api + * @returns {import('vite').Plugin} + */ +export function loadCompiledCss({getEnvironmentState}) { + return { + name: 'vite-plugin-svelte:load-compiled-css', + resolveId: { + filter, // same filter in load to ensure minimal work + handler(id) { + log.debug(`resolveId resolved virtual css module ${id}`, undefined, 'resolve'); + return id; + } + }, + load: { + filter, + async handler(id) { + const {cache} = getEnvironmentState(this); + const cachedCss = cache.getCSS(id); + if (cachedCss) { + const { hasGlobal, ...css } = cachedCss; + if (hasGlobal === false) { + // hasGlobal was added in svelte 5.26.0, so make sure it is boolean false + css.meta ??= {}; + css.meta.vite ??= {}; + css.meta.vite.cssScopeTo = [id.slice(0,id.lastIndexOf('?')), 'default']; + } + css.moduleType = 'css'; + return css; + } + } + } + }; +} diff --git a/packages/vite-plugin-svelte/src/plugins/load-custom.js b/packages/vite-plugin-svelte/src/plugins/load-custom.js new file mode 100644 index 000000000..0bb81e768 --- /dev/null +++ b/packages/vite-plugin-svelte/src/plugins/load-custom.js @@ -0,0 +1,171 @@ +import fs from 'node:fs'; +import { toRollupError } from '../utils/error.js'; +import { log } from '../utils/log.js'; + + +/** + * @param {import('../types/plugin-api.d.ts').PluginAPI} api + * @returns {import('vite').Plugin} + */ +export function loadCustom({idFilter,idParser, options, compileSvelte}) { + + /** @type {import('vite').Plugin} */ + const plugin = { + name: 'vite-plugin-svelte:load-custom', + configResolved() { + //@ts-expect-error load defined below but filter not in type + plugin.load.filter = idFilter; + }, + + load: { + filter: {id:/^$/}, // set in configResolved + async handler(id) { + const config = this.environment.config; + const ssr = config.consumer === 'server'; + const svelteRequest = idParser(id, ssr); + if (svelteRequest) { + const { filename, raw } = svelteRequest; + if (raw) { + const code = await compileRaw(svelteRequest, compileSvelte, options); + // prevent vite from injecting sourcemaps in the results. + return { + code, + map: { + mappings: '' + } + }; + } else { + // prevent vite asset plugin from loading files as url that should be compiled in transform + if (config.assetsInclude(filename)) { + log.debug(`load returns raw content for ${filename}`, undefined, 'load'); + return fs.readFileSync(filename, 'utf-8'); + } + } + } + } + }, + }; + return plugin; +} + +/** + * utility function to compile ?raw and ?direct requests in load hook + * + * @param {import('../types/id.d.ts').SvelteRequest} svelteRequest + * @param {import('../types/compile.d.ts').CompileSvelte} compileSvelte + * @param {import('../types/options.d.ts').ResolvedOptions} options + * @returns {Promise} + */ +async function compileRaw(svelteRequest, compileSvelte, options) { + const { id, filename, query } = svelteRequest; + + // raw svelte subrequest, compile on the fly and return requested subpart + let compileData; + const source = fs.readFileSync(filename, 'utf-8'); + try { + //avoid compileSvelte doing extra ssr stuff unless requested + svelteRequest.ssr = query.compilerOptions?.generate === 'server'; + compileData = await compileSvelte(svelteRequest, source, { + ...options, + // don't use dynamic vite-plugin-svelte defaults here to ensure stable result between ssr,dev and build + compilerOptions: { + dev: false, + css: 'external', + hmr: false, + ...svelteRequest.query.compilerOptions + }, + emitCss: true + }); + } catch (e) { + throw toRollupError(e, options); + } + let result; + if (query.type === 'style') { + result = compileData.compiled.css ?? { code: '', map: null }; + } else if (query.type === 'script') { + result = compileData.compiled.js; + } else if (query.type === 'preprocessed') { + result = compileData.preprocessed; + } else if (query.type === 'all' && query.raw) { + return allToRawExports(compileData, source); + } else { + throw new Error( + `invalid "type=${query.type}" in ${id}. supported are script, style, preprocessed, all` + ); + } + if (query.direct) { + const supportedDirectTypes = ['script', 'style']; + if (!supportedDirectTypes.includes(query.type)) { + throw new Error( + `invalid "type=${ + query.type + }" combined with direct in ${id}. supported are: ${supportedDirectTypes.join(', ')}` + ); + } + log.debug(`load returns direct result for ${id}`, undefined, 'load'); + let directOutput = result.code; + // @ts-expect-error might not be SourceMap but toUrl check should suffice + if (query.sourcemap && result.map?.toUrl) { + // @ts-expect-error toUrl might not exist + const map = `sourceMappingURL=${result.map.toUrl()}`; + if (query.type === 'style') { + directOutput += `\n\n/*# ${map} */\n`; + } else if (query.type === 'script') { + directOutput += `\n\n//# ${map}\n`; + } + } + return directOutput; + } else if (query.raw) { + log.debug(`load returns raw result for ${id}`, undefined, 'load'); + return toRawExports(result); + } else { + throw new Error(`invalid raw mode in ${id}, supported are raw, direct`); + } +} + +/** + * turn compileData and source into a flat list of raw exports + * + * @param {import('../types/compile.d.ts').CompileData} compileData + * @param {string} source + */ +function allToRawExports(compileData, source) { + // flatten CompileData + /** @type {Partial} */ + const exports = { + ...compileData, + ...compileData.compiled, + source + }; + delete exports.compiled; + delete exports.filename; // absolute path, remove to avoid it in output + return toRawExports(exports); +} + +/** + * turn object into raw exports. + * + * every prop is returned as a const export, and if prop 'code' exists it is additionally added as default export + * + * eg {'foo':'bar','code':'baz'} results in + * + * ```js + * export const code='baz' + * export const foo='bar' + * export default code + * ``` + * @param {object} object + * @returns {string} + */ +function toRawExports(object) { + let exports = + Object.entries(object) + .filter(([_key, value]) => typeof value !== 'function') // preprocess output has a toString function that's enumerable + .sort(([a], [b]) => (a < b ? -1 : a === b ? 0 : 1)) + .map(([key, value]) => `export const ${key}=${JSON.stringify(value)}`) + .join('\n') + '\n'; + if (Object.prototype.hasOwnProperty.call(object, 'code')) { + exports += 'export default code\n'; + } + return exports; +} diff --git a/packages/vite-plugin-svelte/src/plugins/optimize-module.js b/packages/vite-plugin-svelte/src/plugins/optimize-module.js deleted file mode 100644 index 7baea6cdd..000000000 --- a/packages/vite-plugin-svelte/src/plugins/optimize-module.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @returns {import('vite').Plugin} - */ -export function optimizeModule() { - return { - name: 'vite-plugin-svelte:optimize-module' - }; -} diff --git a/packages/vite-plugin-svelte/src/plugins/optimize.js b/packages/vite-plugin-svelte/src/plugins/optimize.js deleted file mode 100644 index 3cb7b8975..000000000 --- a/packages/vite-plugin-svelte/src/plugins/optimize.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @returns {import('vite').Plugin} - */ -export function optimize() { - return { - name: 'vite-plugin-svelte:optimize' - }; -} diff --git a/packages/vite-plugin-svelte/src/plugins/setup-optimizer.js b/packages/vite-plugin-svelte/src/plugins/setup-optimizer.js new file mode 100644 index 000000000..e71f08390 --- /dev/null +++ b/packages/vite-plugin-svelte/src/plugins/setup-optimizer.js @@ -0,0 +1,339 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { readFileSync } from 'node:fs'; +import * as svelte from 'svelte/compiler'; +import { log } from '../utils/log.js'; +import { toESBuildError, toRollupError } from '../utils/error.js'; +import { safeBase64Hash } from '../utils/hash.js'; +import { normalize } from '../utils/id.js'; +import * as vite from 'vite'; +// @ts-expect-error not typed on vite +const {rolldownVersion} = vite; + +/** + * @typedef {NonNullable} EsbuildOptions + * @typedef {NonNullable[number]} EsbuildPlugin + */ +/** + * @typedef {NonNullable} RollupPlugin + */ + +const optimizeSveltePluginName = 'vite-plugin-svelte:optimize'; +const optimizeSvelteModulePluginName = 'vite-plugin-svelte:optimize-module'; + +/** + * @param {import('../types/plugin-api.d.ts').PluginAPI} api + * @returns {import('vite').Plugin} + */ +export function setupOptimizer({options}) { + /** @type {import('vite').ResolvedConfig} */ + let viteConfig; + + return { + name: 'vite-plugin-svelte:setup-optimizer', + apply: 'serve', + config(){ + /** @type {import('vite').UserConfig['optimizeDeps']} */ + const optimizeDeps = {}; + // Add optimizer plugins to prebundle Svelte files. + // Currently, a placeholder as more information is needed after Vite config is resolved, + // the added plugins are patched in configResolved below + if (rolldownVersion) { + //@ts-expect-error rolldown types not finished + optimizeDeps.rollupOptions = { + plugins: [ + placeholderRolldownOptimizerPlugin(optimizeSveltePluginName), + placeholderRolldownOptimizerPlugin(optimizeSvelteModulePluginName) + ] + }; + } else { + optimizeDeps.esbuildOptions = { + plugins: [ + { name: optimizeSveltePluginName, setup: () => {} }, + { name: optimizeSvelteModulePluginName, setup: () => {} } + ] + }; + } + return {optimizeDeps}; + }, + configResolved(c) { + viteConfig = c; + const optimizeDeps = c.optimizeDeps; + if (rolldownVersion) { + const plugins = + // @ts-expect-error not typed + optimizeDeps.rollupOptions?.plugins?.filter((p) => + [optimizeSveltePluginName, optimizeSvelteModulePluginName].includes(p.name) + ) ?? []; + for (const plugin of plugins) { + patchRolldownOptimizerPlugin(plugin, options); + } + } else { + const plugins = optimizeDeps.esbuildOptions?.plugins?.filter((p) => + [optimizeSveltePluginName, optimizeSvelteModulePluginName].includes(p.name) + ) ?? []; + for (const plugin of plugins) { + patchESBuildOptimizerPlugin(plugin, options); + } + } + }, + async buildStart() { + if (!options.prebundleSvelteLibraries) return; + const changed = await svelteMetadataChanged(viteConfig.cacheDir, options); + if (changed) { + // Force Vite to optimize again. Although we mutate the config here, it works because + // Vite's optimizer runs after `buildStart()`. + viteConfig.optimizeDeps.force = true; + } + }, + }; +} + + +/** + * @param {EsbuildPlugin} plugin + * @param {import('../types/options.d.ts').ResolvedOptions} options + */ +function patchESBuildOptimizerPlugin(plugin, options) { + const components = plugin.name === optimizeSveltePluginName; + const compileFn = components ? compileSvelte : compileSvelteModule; + const statsName = components ? 'prebundle library components' : 'prebundle library modules'; + const filter = components ? /\.svelte(?:\?.*)?$/ : /\.svelte\.[jt]s(?:\?.*)?$/; + plugin.setup = (build) => { + if (build.initialOptions.plugins?.some((v) => v.name === 'vite:dep-scan')) return; + + /** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */ + let statsCollection; + build.onStart(() => { + statsCollection = options.stats?.startCollection(statsName, { + logResult: (c) => c.stats.length > 1 + }); + }); + build.onLoad({ filter }, async ({ path: filename }) => { + const code = readFileSync(filename, 'utf8'); + try { + const result = await compileFn(options, { filename, code }, statsCollection); + const contents = result.map + ? result.code + '//# sourceMappingURL=' + result.map.toUrl() + : result.code; + return { contents }; + } catch (e) { + return { errors: [toESBuildError(e, options)] }; + } + }); + build.onEnd(() => { + statsCollection?.finish(); + }); + }; +} + +/** + * @param {RollupPlugin} plugin + * @param {import('../types/options.d.ts').ResolvedOptions} options + */ +function patchRolldownOptimizerPlugin(plugin, options) { + const components = plugin.name === optimizeSveltePluginName; + const compileFn = components ? compileSvelte : compileSvelteModule; + const statsName = components ? 'prebundle library components' : 'prebundle library modules'; + const includeRe = components ? /^[^?#]+\.svelte(?:[?#]|$)/ : /^[^?#]+\.svelte\.[jt]s(?:[?#]|$)/; + /** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */ + let statsCollection; + + plugin.options = (opts) => { + // @ts-expect-error plugins is an array here + const isScanner = opts.plugins.some( + (/** @type {{ name: string; }} */ p) => p.name === 'vite:dep-scan:resolve' + ); + if (isScanner) { + delete plugin.buildStart; + delete plugin.transform; + delete plugin.buildEnd; + } else { + plugin.transform = { + filter: { id: includeRe }, + /** + * @param {string} code + * @param {string} filename + */ + async handler(code, filename) { + try { + return await compileFn(options, { filename, code }, statsCollection); + } catch (e) { + throw toRollupError(e, options); + } + } + }; + plugin.buildStart = () => { + statsCollection = options.stats?.startCollection(statsName, { + logResult: (c) => c.stats.length > 1 + }); + }; + plugin.buildEnd = () => { + statsCollection?.finish(); + }; + } + }; +} + +/** + * @param {import('../types/options.d.ts').ResolvedOptions} options + * @param {{ filename: string, code: string }} input + * @param {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection} [statsCollection] + * @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, + filename, + generate: 'client' + }; + + if (compileOptions.hmr && options.emitCss) { + const hash = `s-${safeBase64Hash(normalize(filename, options.root))}`; + compileOptions.cssHash = () => hash; + } + + let preprocessed; + + if (options.preprocess) { + try { + preprocessed = await svelte.preprocess(code, options.preprocess, { filename }); + } catch (e) { + e.message = `Error while preprocessing ${filename}${e.message ? ` - ${e.message}` : ''}`; + throw e; + } + if (preprocessed.map) compileOptions.sourcemap = preprocessed.map; + } + + const finalCode = preprocessed ? preprocessed.code : code; + + const dynamicCompileOptions = await options?.dynamicCompileOptions?.({ + filename, + code: finalCode, + compileOptions + }); + + if (dynamicCompileOptions && log.debug.enabled) { + log.debug( + `dynamic compile options for ${filename}: ${JSON.stringify(dynamicCompileOptions)}`, + undefined, + 'compile' + ); + } + + const finalCompileOptions = dynamicCompileOptions + ? { + ...compileOptions, + ...dynamicCompileOptions + } + : compileOptions; + const endStat = statsCollection?.start(filename); + const compiled = svelte.compile(finalCode, finalCompileOptions); + if (endStat) { + endStat(); + } + return { + ...compiled.js, + moduleType: 'js' + }; +} + +/** + * @param {import('../types/options.d.ts').ResolvedOptions} options + * @param {{ filename: string; code: string }} input + * @param {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection} [statsCollection] + * @returns {Promise} + */ +async function compileSvelteModule(options, { filename, code }, statsCollection) { + const endStat = statsCollection?.start(filename); + const compiled = svelte.compileModule(code, { + dev: options.compilerOptions?.dev ?? true, // default to dev: true because prebundling is only used in dev + filename, + generate: 'client' + }); + if (endStat) { + endStat(); + } + return { + ...compiled.js, + moduleType: 'js' + }; +} + + +// List of options that changes the prebundling result +/** @type {(keyof import('../types/options.d.ts').ResolvedOptions)[]} */ +const PREBUNDLE_SENSITIVE_OPTIONS = [ + 'compilerOptions', + 'configFile', + 'experimental', + 'extensions', + 'ignorePluginPreprocessors', + 'preprocess' +]; + +/** + * stores svelte metadata in cache dir and compares if it has changed + * + * @param {string} cacheDir + * @param {import('../types/options.d.ts').ResolvedOptions} options + * @returns {Promise} Whether the Svelte metadata has changed + */ +async function svelteMetadataChanged(cacheDir, options) { + const svelteMetadata = generateSvelteMetadata(options); + const svelteMetadataPath = path.resolve(cacheDir, '_svelte_metadata.json'); + + const currentSvelteMetadata = JSON.stringify(svelteMetadata, (_, value) => { + // Handle preprocessors + return typeof value === 'function' ? value.toString() : value; + }); + + /** @type {string | undefined} */ + let existingSvelteMetadata; + try { + existingSvelteMetadata = await fs.readFile(svelteMetadataPath, 'utf8'); + } catch { + // ignore + } + + await fs.mkdir(cacheDir, { recursive: true }); + await fs.writeFile(svelteMetadataPath, currentSvelteMetadata); + return currentSvelteMetadata !== existingSvelteMetadata; +} + +/** + * + * @param {string} name + * @returns {import('vite').Rollup.Plugin} + */ +function placeholderRolldownOptimizerPlugin(name){ + return { + name, + options() {}, + buildStart() {}, + buildEnd() {}, + transform: { filter: { id: /^$/ }, handler() {} } + } +} + +/** + * @param {import('../types/options.d.ts').ResolvedOptions} options + * @returns {Partial} + */ +function generateSvelteMetadata(options) { + /** @type {Record} */ + const metadata = {}; + for (const key of PREBUNDLE_SENSITIVE_OPTIONS) { + metadata[key] = options[key]; + } + return metadata; +} + diff --git a/packages/vite-plugin-svelte/src/types/compile.d.ts b/packages/vite-plugin-svelte/src/types/compile.d.ts index 14a5a8f03..64ba60737 100644 --- a/packages/vite-plugin-svelte/src/types/compile.d.ts +++ b/packages/vite-plugin-svelte/src/types/compile.d.ts @@ -23,6 +23,7 @@ export interface Code { export interface CompileData { filename: string; normalizedFilename: string; + cssId: string; lang: string; compiled: CompileResult; ssr: boolean | undefined; diff --git a/packages/vite-plugin-svelte/src/types/plugin-api.d.ts b/packages/vite-plugin-svelte/src/types/plugin-api.d.ts index b56563adc..c6ca0cd8d 100644 --- a/packages/vite-plugin-svelte/src/types/plugin-api.d.ts +++ b/packages/vite-plugin-svelte/src/types/plugin-api.d.ts @@ -1,16 +1,18 @@ import type { ResolvedOptions } from './options.d.ts'; +import { perEnvironmentState } from 'vite'; import { VitePluginSvelteCache } from '../utils/vite-plugin-svelte-cache.js'; import { VitePluginSvelteStats } from '../utils/vite-plugin-svelte-stats.js'; +import type { IdFilter, IdParser } from './id.d.ts'; +import {CompileSvelte} from './compile.d.ts'; +interface EnvironmentState { + cache: VitePluginSvelteCache; + stats: VitePluginSvelteStats; +} export interface PluginAPI { - /** - * must not be used by plugins outside of the vite-plugin-svelte monorepo - * this is not part of our public semver contract, breaking changes to it can and will happen in patch releases - * @internal - */ - __internal: { - options: ResolvedOptions; - cache?: VitePluginSvelteCache; - stats?: VitePluginSvelteStats; - }; + options: ResolvedOptions; + getEnvironmentState: ReturnType>; + idFilter: IdFilter; + idParser: IdParser; + compileSvelte: CompileSvelte; } diff --git a/packages/vite-plugin-svelte/src/utils/compile.js b/packages/vite-plugin-svelte/src/utils/compile.js index f7999fe72..86c747d42 100644 --- a/packages/vite-plugin-svelte/src/utils/compile.js +++ b/packages/vite-plugin-svelte/src/utils/compile.js @@ -181,6 +181,7 @@ export function createCompileSvelte() { return { filename, normalizedFilename, + cssId, lang, compiled, ssr, diff --git a/packages/vite-plugin-svelte/src/utils/optimizer.js b/packages/vite-plugin-svelte/src/utils/optimizer.js deleted file mode 100644 index 71aa57162..000000000 --- a/packages/vite-plugin-svelte/src/utils/optimizer.js +++ /dev/null @@ -1,53 +0,0 @@ -import { promises as fs } from 'node:fs'; -import path from 'node:path'; - -// List of options that changes the prebundling result -/** @type {(keyof import('../types/options.d.ts').ResolvedOptions)[]} */ -const PREBUNDLE_SENSITIVE_OPTIONS = [ - 'compilerOptions', - 'configFile', - 'experimental', - 'extensions', - 'ignorePluginPreprocessors', - 'preprocess' -]; - -/** - * @param {string} cacheDir - * @param {import('../types/options.d.ts').ResolvedOptions} options - * @returns {Promise} Whether the Svelte metadata has changed - */ -export async function saveSvelteMetadata(cacheDir, options) { - const svelteMetadata = generateSvelteMetadata(options); - const svelteMetadataPath = path.resolve(cacheDir, '_svelte_metadata.json'); - - const currentSvelteMetadata = JSON.stringify(svelteMetadata, (_, value) => { - // Handle preprocessors - return typeof value === 'function' ? value.toString() : value; - }); - - /** @type {string | undefined} */ - let existingSvelteMetadata; - try { - existingSvelteMetadata = await fs.readFile(svelteMetadataPath, 'utf8'); - } catch { - // ignore - } - - await fs.mkdir(cacheDir, { recursive: true }); - await fs.writeFile(svelteMetadataPath, currentSvelteMetadata); - return currentSvelteMetadata !== existingSvelteMetadata; -} - -/** - * @param {import('../types/options.d.ts').ResolvedOptions} options - * @returns {Partial} - */ -function generateSvelteMetadata(options) { - /** @type {Record} */ - const metadata = {}; - for (const key of PREBUNDLE_SENSITIVE_OPTIONS) { - metadata[key] = options[key]; - } - return metadata; -} diff --git a/packages/vite-plugin-svelte/src/utils/vite-plugin-svelte-cache.js b/packages/vite-plugin-svelte/src/utils/vite-plugin-svelte-cache.js index 0f5538d3c..f2fcde2a5 100644 --- a/packages/vite-plugin-svelte/src/utils/vite-plugin-svelte-cache.js +++ b/packages/vite-plugin-svelte/src/utils/vite-plugin-svelte-cache.js @@ -63,7 +63,7 @@ export class VitePluginSvelteCache { * @param {import('../types/compile.d.ts').CompileData} compileData */ #updateCSS(compileData) { - this.#css.set(compileData.normalizedFilename, compileData.compiled.css); + this.#css.set(compileData.cssId, compileData.compiled.css); } /** @@ -132,13 +132,14 @@ export class VitePluginSvelteCache { } /** - * @param {import('../types/id.d.ts').SvelteRequest} svelteRequest + * @param {string} cssId * @returns {import('../types/compile.d.ts').Code | undefined | null} */ - getCSS(svelteRequest) { - return this.#css.get(svelteRequest.normalizedFilename); + getCSS(cssId) { + return this.#css.get(cssId); } + /** * @param {import('../types/id.d.ts').SvelteRequest} svelteRequest * @returns {import('../types/compile.d.ts').Code | undefined | null} From bca41f22c53ad552c18ca1cde148f0c2aa97da2a Mon Sep 17 00:00:00 2001 From: dominikg Date: Thu, 19 Jun 2025 11:28:50 +0200 Subject: [PATCH 03/18] fix: don't destructure api --- .../src/plugins/load-compiled-css.js | 10 +++-- .../src/plugins/load-custom.js | 14 +++---- .../src/plugins/setup-optimizer.js | 40 +++++++++---------- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/vite-plugin-svelte/src/plugins/load-compiled-css.js b/packages/vite-plugin-svelte/src/plugins/load-compiled-css.js index 30fe9ca84..a48b09ce0 100644 --- a/packages/vite-plugin-svelte/src/plugins/load-compiled-css.js +++ b/packages/vite-plugin-svelte/src/plugins/load-compiled-css.js @@ -7,9 +7,12 @@ const filter = { id: SVELTE_VIRTUAL_STYLE_ID_REGEX }; * @param {import('../types/plugin-api.d.ts').PluginAPI} api * @returns {import('vite').Plugin} */ -export function loadCompiledCss({getEnvironmentState}) { +export function loadCompiledCss(api) { return { name: 'vite-plugin-svelte:load-compiled-css', + applyToEnvironment(env) { + return env.config.consumer === 'client'; // ssr compile does not emit css + }, resolveId: { filter, // same filter in load to ensure minimal work handler(id) { @@ -20,7 +23,7 @@ export function loadCompiledCss({getEnvironmentState}) { load: { filter, async handler(id) { - const {cache} = getEnvironmentState(this); + const { cache } = api.getEnvironmentState(this); const cachedCss = cache.getCSS(id); if (cachedCss) { const { hasGlobal, ...css } = cachedCss; @@ -28,7 +31,8 @@ export function loadCompiledCss({getEnvironmentState}) { // hasGlobal was added in svelte 5.26.0, so make sure it is boolean false css.meta ??= {}; css.meta.vite ??= {}; - css.meta.vite.cssScopeTo = [id.slice(0,id.lastIndexOf('?')), 'default']; + // TODO is that slice the best way to get the filename without parsing the id? + css.meta.vite.cssScopeTo = [id.slice(0, id.lastIndexOf('?')), 'default']; } css.moduleType = 'css'; return css; diff --git a/packages/vite-plugin-svelte/src/plugins/load-custom.js b/packages/vite-plugin-svelte/src/plugins/load-custom.js index 0bb81e768..4cb0a52a9 100644 --- a/packages/vite-plugin-svelte/src/plugins/load-custom.js +++ b/packages/vite-plugin-svelte/src/plugins/load-custom.js @@ -2,31 +2,29 @@ import fs from 'node:fs'; import { toRollupError } from '../utils/error.js'; import { log } from '../utils/log.js'; - /** * @param {import('../types/plugin-api.d.ts').PluginAPI} api * @returns {import('vite').Plugin} */ -export function loadCustom({idFilter,idParser, options, compileSvelte}) { - +export function loadCustom(api) { /** @type {import('vite').Plugin} */ const plugin = { name: 'vite-plugin-svelte:load-custom', configResolved() { //@ts-expect-error load defined below but filter not in type - plugin.load.filter = idFilter; + plugin.load.filter = api.idFilter; }, load: { - filter: {id:/^$/}, // set in configResolved + //filter: is set in configResolved async handler(id) { const config = this.environment.config; const ssr = config.consumer === 'server'; - const svelteRequest = idParser(id, ssr); + const svelteRequest = api.idParser(id, ssr); if (svelteRequest) { const { filename, raw } = svelteRequest; if (raw) { - const code = await compileRaw(svelteRequest, compileSvelte, options); + const code = await compileRaw(svelteRequest, api.compileSvelte, api.options); // prevent vite from injecting sourcemaps in the results. return { code, @@ -43,7 +41,7 @@ export function loadCustom({idFilter,idParser, options, compileSvelte}) { } } } - }, + } }; return plugin; } diff --git a/packages/vite-plugin-svelte/src/plugins/setup-optimizer.js b/packages/vite-plugin-svelte/src/plugins/setup-optimizer.js index e71f08390..64aac611a 100644 --- a/packages/vite-plugin-svelte/src/plugins/setup-optimizer.js +++ b/packages/vite-plugin-svelte/src/plugins/setup-optimizer.js @@ -8,7 +8,7 @@ import { safeBase64Hash } from '../utils/hash.js'; import { normalize } from '../utils/id.js'; import * as vite from 'vite'; // @ts-expect-error not typed on vite -const {rolldownVersion} = vite; +const { rolldownVersion } = vite; /** * @typedef {NonNullable} EsbuildOptions @@ -25,14 +25,14 @@ const optimizeSvelteModulePluginName = 'vite-plugin-svelte:optimize-module'; * @param {import('../types/plugin-api.d.ts').PluginAPI} api * @returns {import('vite').Plugin} */ -export function setupOptimizer({options}) { +export function setupOptimizer(api) { /** @type {import('vite').ResolvedConfig} */ let viteConfig; - return { + return { name: 'vite-plugin-svelte:setup-optimizer', apply: 'serve', - config(){ + config() { /** @type {import('vite').UserConfig['optimizeDeps']} */ const optimizeDeps = {}; // Add optimizer plugins to prebundle Svelte files. @@ -54,7 +54,7 @@ export function setupOptimizer({options}) { ] }; } - return {optimizeDeps}; + return { optimizeDeps }; }, configResolved(c) { viteConfig = c; @@ -66,30 +66,30 @@ export function setupOptimizer({options}) { [optimizeSveltePluginName, optimizeSvelteModulePluginName].includes(p.name) ) ?? []; for (const plugin of plugins) { - patchRolldownOptimizerPlugin(plugin, options); + patchRolldownOptimizerPlugin(plugin, api.options); } } else { - const plugins = optimizeDeps.esbuildOptions?.plugins?.filter((p) => - [optimizeSveltePluginName, optimizeSvelteModulePluginName].includes(p.name) - ) ?? []; + const plugins = + optimizeDeps.esbuildOptions?.plugins?.filter((p) => + [optimizeSveltePluginName, optimizeSvelteModulePluginName].includes(p.name) + ) ?? []; for (const plugin of plugins) { - patchESBuildOptimizerPlugin(plugin, options); + patchESBuildOptimizerPlugin(plugin, api.options); } } }, async buildStart() { - if (!options.prebundleSvelteLibraries) return; - const changed = await svelteMetadataChanged(viteConfig.cacheDir, options); + if (!api.options.prebundleSvelteLibraries) return; + const changed = await svelteMetadataChanged(viteConfig.cacheDir, api.options); if (changed) { // Force Vite to optimize again. Although we mutate the config here, it works because // Vite's optimizer runs after `buildStart()`. viteConfig.optimizeDeps.force = true; } - }, + } }; } - /** * @param {EsbuildPlugin} plugin * @param {import('../types/options.d.ts').ResolvedOptions} options @@ -231,9 +231,9 @@ async function compileSvelte(options, { filename, code }, statsCollection) { const finalCompileOptions = dynamicCompileOptions ? { - ...compileOptions, - ...dynamicCompileOptions - } + ...compileOptions, + ...dynamicCompileOptions + } : compileOptions; const endStat = statsCollection?.start(filename); const compiled = svelte.compile(finalCode, finalCompileOptions); @@ -268,7 +268,6 @@ async function compileSvelteModule(options, { filename, code }, statsCollection) }; } - // List of options that changes the prebundling result /** @type {(keyof import('../types/options.d.ts').ResolvedOptions)[]} */ const PREBUNDLE_SENSITIVE_OPTIONS = [ @@ -314,14 +313,14 @@ async function svelteMetadataChanged(cacheDir, options) { * @param {string} name * @returns {import('vite').Rollup.Plugin} */ -function placeholderRolldownOptimizerPlugin(name){ +function placeholderRolldownOptimizerPlugin(name) { return { name, options() {}, buildStart() {}, buildEnd() {}, transform: { filter: { id: /^$/ }, handler() {} } - } + }; } /** @@ -336,4 +335,3 @@ function generateSvelteMetadata(options) { } return metadata; } - From 94ac814eca682091c95996621f545b5b10eb9b27 Mon Sep 17 00:00:00 2001 From: dominikg Date: Sun, 22 Jun 2025 17:13:26 +0200 Subject: [PATCH 04/18] mostly working, but hacky raw/direct need work --- .../vite-plugin-svelte-inspector/src/index.js | 79 +++-- .../src/handle-hot-update.js | 2 +- .../vite-plugin-svelte/src/index-modular.js | 42 --- packages/vite-plugin-svelte/src/index.js | 280 ++---------------- .../src/plugins/compile-module.js | 49 ++- .../vite-plugin-svelte/src/plugins/compile.js | 71 ++++- .../src/plugins/configure.js | 103 +------ .../src/plugins/hot-update.js | 172 +++++++++++ .../src/plugins/load-compiled-css.js | 13 +- .../src/plugins/load-custom.js | 1 + .../src/plugins/preprocess.js | 120 +++++++- .../vite-plugin-svelte/src/types/compile.d.ts | 21 +- .../src/types/plugin-api.d.ts | 16 +- .../vite-plugin-svelte/src/utils/compile.js | 38 +-- .../vite-plugin-svelte/src/utils/constants.js | 5 +- packages/vite-plugin-svelte/src/utils/id.js | 11 +- .../vite-plugin-svelte/src/utils/options.js | 3 +- .../src/utils/vite-plugin-svelte-cache.js | 70 +---- .../src/utils/vite-plugin-svelte-stats.js | 70 ++++- packages/vite-plugin-svelte/types/index.d.ts | 5 + .../vite-plugin-svelte/types/index.d.ts.map | 2 +- 21 files changed, 611 insertions(+), 562 deletions(-) delete mode 100644 packages/vite-plugin-svelte/src/index-modular.js create mode 100644 packages/vite-plugin-svelte/src/plugins/hot-update.js diff --git a/packages/vite-plugin-svelte-inspector/src/index.js b/packages/vite-plugin-svelte-inspector/src/index.js index 3b6978322..6eaf5dfa6 100644 --- a/packages/vite-plugin-svelte-inspector/src/index.js +++ b/packages/vite-plugin-svelte-inspector/src/index.js @@ -33,6 +33,10 @@ export function svelteInspector(options) { apply: 'serve', enforce: 'pre', + applyToEnvironment(env) { + return env.config.consumer === 'client'; + }, + configResolved(config) { viteConfig = config; const environmentOptions = parseEnvironmentOptions(config); @@ -43,7 +47,7 @@ export function svelteInspector(options) { } // Handle config from svelte.config.js through vite-plugin-svelte - const vps = config.plugins.find((p) => p.name === 'vite-plugin-svelte'); + const vps = config.plugins.find((p) => p.name === 'vite-plugin-svelte:config'); const configFileOptions = vps?.api?.options?.inspector; // vite-plugin-svelte can only pass options through it's `api` instead of `options`. @@ -69,42 +73,53 @@ export function svelteInspector(options) { base: config.base?.replace(/\/$/, '') || '' }; }, - - async resolveId(importee, _, options) { - if (options?.ssr || disabled) { - return; - } - if (importee.startsWith('virtual:svelte-inspector-options')) { - return importee; - } else if (importee.startsWith('virtual:svelte-inspector-path:')) { - return importee.replace('virtual:svelte-inspector-path:', inspectorPath); + resolveId: { + filter: { + id: /^virtual:svelte-inspector-/ + }, + async handler(importee, _, options) { + if (options?.ssr || disabled) { + return; + } + if (importee.startsWith('virtual:svelte-inspector-options')) { + return importee; + } else if (importee.startsWith('virtual:svelte-inspector-path:')) { + return importee.replace('virtual:svelte-inspector-path:', inspectorPath); + } } }, - - async load(id, options) { - if (options?.ssr || disabled) { - return; - } - if (id === 'virtual:svelte-inspector-options') { - return `export default ${JSON.stringify(inspectorOptions ?? {})}`; - } else if (id.startsWith(inspectorPath)) { - // read file ourselves to avoid getting shut out by vites fs.allow check - const file = cleanUrl(id); - if (fs.existsSync(id)) { - return await fs.promises.readFile(file, 'utf-8'); - } else { - viteConfig.logger.error( - `[vite-plugin-svelte-inspector] failed to find svelte-inspector: ${id}` - ); + load: { + filter: { + id: { + include: [`${inspectorPath}/**`, /^virtual:svelte-inspector-options$/], + exclude: [/style&lang\.css$/] + } + }, + async handler(id) { + if (disabled) { + return; + } + if (id === 'virtual:svelte-inspector-options') { + return `export default ${JSON.stringify(inspectorOptions ?? {})}`; + } else if (id.startsWith(inspectorPath)) { + // read file ourselves to avoid getting shut out by vites fs.allow check + const file = cleanUrl(id); + if (fs.existsSync(id)) { + return await fs.promises.readFile(file, 'utf-8'); + } else { + viteConfig.logger.error( + `[vite-plugin-svelte-inspector] failed to find svelte-inspector: ${id}` + ); + } } } }, - - transform(code, id, options) { - if (options?.ssr || disabled) { - return; - } - if (id.includes('vite/dist/client/client.mjs')) { + transform: { + filter: { id: /vite\/dist\/client\/client\.mjs(?:\?|$)/ }, + handler(code) { + if (disabled) { + return; + } return { code: `${code}\nimport('virtual:svelte-inspector-path:load-inspector.js')` }; } } diff --git a/packages/vite-plugin-svelte/src/handle-hot-update.js b/packages/vite-plugin-svelte/src/handle-hot-update.js index 5c880f65d..82758e55a 100644 --- a/packages/vite-plugin-svelte/src/handle-hot-update.js +++ b/packages/vite-plugin-svelte/src/handle-hot-update.js @@ -24,7 +24,7 @@ export async function handleHotUpdate(compileSvelte, ctx, svelteRequest, cache, const { read, server, modules } = ctx; const cachedJS = cache.getJS(svelteRequest); - const cachedCss = cache.getCSS(svelteRequest.cssId); + const cachedCss = cache.getCSS(svelteRequest); const content = await read(); /** @type {import('./types/compile.d.ts').CompileData} */ diff --git a/packages/vite-plugin-svelte/src/index-modular.js b/packages/vite-plugin-svelte/src/index-modular.js deleted file mode 100644 index 47b30c28e..000000000 --- a/packages/vite-plugin-svelte/src/index-modular.js +++ /dev/null @@ -1,42 +0,0 @@ -import { configure } from './plugins/configure.js'; -import { preprocess } from './plugins/preprocess.js'; -import { compile } from './plugins/compile.js'; -import { loadCompiledCss } from './plugins/load-compiled-css.js'; -import { setupOptimizer } from './plugins/setup-optimizer.js'; -import { optimizeModule } from './plugins/optimize-module.js'; -import { compileModule } from './plugins/compile-module.js'; -import { svelteInspector } from '@sveltejs/vite-plugin-svelte-inspector'; -import {loadCustom} from "./plugins/load-custom.js"; -/** - * returns a list of plugins to handle svelte files - * plugins are named `vite-plugin-svelte:` - * - * @param {Partial} [inlineOptions] - * @returns {import('vite').Plugin[]} - */ -export function svelte(inlineOptions) { - /** @type {import('./types/plugin-api.js').PluginAPI} */ - const api = { - // @ts-expect-error protection against early use - get options(){ - throw new Error('must not use configResolved') - }, - // @ts-expect-error protection against early use - get getEnvironmentState() { - throw new Error('must not use before configResolved') - } - }; - return [ - configure(api,inlineOptions), // parse config and put it on api.__internal for the other plugins to use - setupOptimizer(api), // add optimizer plugins for pre-bundling in development - preprocess(api), // preprocess .svelte files - compile(api), // compile .svelte files - loadCompiledCss(api), // return virtual css modules created by compile - loadCustom(api), // return custom output d - compileModule(api),// compile module - svelteInspector() - ]; -} - -export { vitePreprocess } from './preprocess.js'; -export { loadSvelteConfig } from './utils/load-svelte-config.js'; diff --git a/packages/vite-plugin-svelte/src/index.js b/packages/vite-plugin-svelte/src/index.js index 68c9a4a5d..9a5ab91b7 100644 --- a/packages/vite-plugin-svelte/src/index.js +++ b/packages/vite-plugin-svelte/src/index.js @@ -1,36 +1,19 @@ -import fs from 'node:fs'; import process from 'node:process'; +import { log } from './utils/log.js'; +import { configure } from './plugins/configure.js'; +import { preprocess } from './plugins/preprocess.js'; +import { compile } from './plugins/compile.js'; +import { loadCompiledCss } from './plugins/load-compiled-css.js'; +import { setupOptimizer } from './plugins/setup-optimizer.js'; +import { compileModule } from './plugins/compile-module.js'; import { svelteInspector } from '@sveltejs/vite-plugin-svelte-inspector'; -import { handleHotUpdate } from './handle-hot-update.js'; -import { log, logCompilerWarnings } from './utils/log.js'; -import { createCompileSvelte } from './utils/compile.js'; -import { - buildIdFilter, - buildIdParser, - buildModuleIdFilter, - buildModuleIdParser -} from './utils/id.js'; -import { - buildExtraViteConfig, - validateInlineOptions, - resolveOptions, - patchResolvedViteConfig, - preResolveOptions, - ensureConfigEnvironmentMainFields, - ensureConfigEnvironmentConditions -} from './utils/options.js'; -import { ensureWatchedFile, setupWatchers } from './utils/watch.js'; -import { toRollupError } from './utils/error.js'; -import { saveSvelteMetadata } from './utils/optimizer.js'; -import { VitePluginSvelteCache } from './utils/vite-plugin-svelte-cache.js'; -import { loadRaw } from './utils/load-raw.js'; -import * as svelteCompiler from 'svelte/compiler'; -import { SVELTE_VIRTUAL_STYLE_ID_REGEX } from './utils/constants.js'; -import * as vite from 'vite'; -// @ts-expect-error rolldownVersion -const { version: viteVersion, rolldownVersion } = vite; +import { loadCustom } from './plugins/load-custom.js'; +import { hotUpdate } from './plugins/hot-update.js'; /** + * returns a list of plugins to handle svelte files + * plugins are named `vite-plugin-svelte:` + * * @param {Partial} [inlineOptions] * @returns {import('vite').Plugin[]} */ @@ -38,231 +21,20 @@ export function svelte(inlineOptions) { if (process.env.DEBUG != null) { log.setLevel('debug'); } - if (rolldownVersion) { - log.warn.once( - `!!! Support for rolldown-vite in vite-plugin-svelte is experimental (rolldown: ${rolldownVersion}, vite: ${viteVersion}) !!!` - ); - } - - validateInlineOptions(inlineOptions); - const cache = new VitePluginSvelteCache(); - // updated in configResolved hook - /** @type {import('./types/id.d.ts').IdParser} */ - let requestParser; - /** @type {import('./types/id.d.ts').ModuleIdParser} */ - let moduleRequestParser; - /** @type {import('./types/options.d.ts').ResolvedOptions} */ - let options; - /** @type {import('vite').ResolvedConfig} */ - let viteConfig; - /** @type {import('./types/compile.d.ts').CompileSvelte} */ - let compileSvelte; - - /** @type {import('vite').Plugin} */ - const compilePlugin = { - name: 'vite-plugin-svelte', - // make sure our resolver runs before vite internal resolver to resolve svelte field correctly - enforce: 'pre', - /** @type {import('./types/plugin-api.d.ts').PluginAPI} */ - api: {}, - async config(config, configEnv) { - // setup logger - if (process.env.DEBUG) { - log.setLevel('debug'); - } else if (config.logLevel) { - log.setLevel(config.logLevel); - } - // @ts-expect-error temporarily lend the options variable until fixed in configResolved - options = await preResolveOptions(inlineOptions, config, configEnv); - // extra vite config - const extraViteConfig = await buildExtraViteConfig(options, config); - log.debug('additional vite config', extraViteConfig, 'config'); - return extraViteConfig; - }, - - configEnvironment(name, config, opts) { - ensureConfigEnvironmentMainFields(name, config, opts); - // @ts-expect-error the function above should make `resolve.mainFields` non-nullable - config.resolve.mainFields.unshift('svelte'); - - ensureConfigEnvironmentConditions(name, config, opts); - // @ts-expect-error the function above should make `resolve.conditions` non-nullable - config.resolve.conditions.push('svelte'); - }, - - async configResolved(config) { - options = resolveOptions(options, config, cache); - patchResolvedViteConfig(config, options); - const filter = buildIdFilter(options); - //@ts-expect-error transform defined below but filter not in type - compilePlugin.transform.filter = filter; - //@ts-expect-error load defined below but filter not in type - compilePlugin.load.filter = filter; - - requestParser = buildIdParser(options); - compileSvelte = createCompileSvelte(); - viteConfig = config; - // TODO deep clone to avoid mutability from outside? - compilePlugin.api.options = options; - log.debug('resolved options', options, 'config'); - log.debug('filters', filter, 'config'); - }, - - async buildStart() { - if (!options.prebundleSvelteLibraries) return; - const isSvelteMetadataChanged = await saveSvelteMetadata(viteConfig.cacheDir, options); - if (isSvelteMetadataChanged) { - // Force Vite to optimize again. Although we mutate the config here, it works because - // Vite's optimizer runs after `buildStart()`. - viteConfig.optimizeDeps.force = true; - } - }, - - configureServer(server) { - options.server = server; - setupWatchers(options, cache, requestParser); - }, - - load: { - async handler(id, opts) { - const ssr = !!opts?.ssr; - const svelteRequest = requestParser(id, !!ssr); - if (svelteRequest) { - const { filename, query, raw } = svelteRequest; - if (raw) { - const code = await loadRaw(svelteRequest, compileSvelte, options); - // prevent vite from injecting sourcemaps in the results. - return { - code, - map: { - mappings: '' - } - }; - } else { - if (query.svelte && query.type === 'style') { - const cachedCss = cache.getCSS(svelteRequest); - if (cachedCss) { - const { hasGlobal, ...css } = cachedCss; - if (hasGlobal === false) { - // hasGlobal was added in svelte 5.26.0, so make sure it is boolean false - css.meta ??= {}; - css.meta.vite ??= {}; - css.meta.vite.cssScopeTo = [svelteRequest.filename, 'default']; - } - css.moduleType = 'css'; - - return css; - } - } - // prevent vite asset plugin from loading files as url that should be compiled in transform - if (viteConfig.assetsInclude(filename)) { - log.debug(`load returns raw content for ${filename}`, undefined, 'load'); - return fs.readFileSync(filename, 'utf-8'); - } - } - } - } - }, - - resolveId: { - // we don't use our generic filter here but a reduced one that only matches our virtual css - filter: { id: SVELTE_VIRTUAL_STYLE_ID_REGEX }, - handler(id) { - // return cssId with root prefix so postcss pipeline of vite finds the directory correctly - // see https://github.com/sveltejs/vite-plugin-svelte/issues/14 - log.debug(`resolveId resolved virtual css module ${id}`, undefined, 'resolve'); - // TODO: do we have to repeat the dance for constructing the virtual id here? our transform added it that way - return id; - } - }, - - transform: { - async handler(code, id, opts) { - const ssr = !!opts?.ssr; - const svelteRequest = requestParser(id, ssr); - if (!svelteRequest || svelteRequest.query.type === 'style' || svelteRequest.raw) { - return; - } - let compileData; - try { - compileData = await compileSvelte(svelteRequest, code, options); - } catch (e) { - cache.setError(svelteRequest, e); - throw toRollupError(e, options); - } - logCompilerWarnings(svelteRequest, compileData.compiled.warnings, options); - cache.update(compileData); - if (compileData.dependencies?.length) { - if (options.server) { - for (const dep of compileData.dependencies) { - ensureWatchedFile(options.server.watcher, dep, options.root); - } - } else if (options.isBuild && viteConfig.build.watch) { - for (const dep of compileData.dependencies) { - this.addWatchFile(dep); - } - } - } - return { - ...compileData.compiled.js, - moduleType: 'js', - meta: { - vite: { - lang: compileData.lang - } - } - }; - } - }, - - 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); - } - }, - async buildEnd() { - await options.stats?.finishAll(); - } - }; - - /** @type {import('vite').Plugin} */ - const moduleCompilePlugin = { - name: 'vite-plugin-svelte-module', - enforce: 'post', - async configResolved() { - //@ts-expect-error transform defined below but filter not in type - moduleCompilePlugin.transform.filter = buildModuleIdFilter(options); - moduleRequestParser = buildModuleIdParser(options); - }, - transform: { - async handler(code, id, opts) { - const ssr = !!opts?.ssr; - const moduleRequest = moduleRequestParser(id, ssr); - if (!moduleRequest) { - return; - } - try { - const compileResult = svelteCompiler.compileModule(code, { - dev: !viteConfig.isProduction, - generate: ssr ? 'server' : 'client', - filename: moduleRequest.filename - }); - logCompilerWarnings(moduleRequest, compileResult.warnings, options); - return compileResult.js; - } catch (e) { - throw toRollupError(e, options); - } - } - } - }; - - /** @type {import('vite').Plugin[]} */ - const plugins = [compilePlugin, moduleCompilePlugin, svelteInspector()]; - return plugins; + /** @type {import('./types/plugin-api.js').PluginAPI} */ + // @ts-expect-error initialize empty to guard against early use + const api = {}; // initialized by configure plugin, used in others + return [ + configure(api, inlineOptions), + setupOptimizer(api), + preprocess(api), + compile(api), + hotUpdate(api), + loadCompiledCss(api), + loadCustom(api), + compileModule(api), + svelteInspector() + ]; } export { vitePreprocess } from './preprocess.js'; diff --git a/packages/vite-plugin-svelte/src/plugins/compile-module.js b/packages/vite-plugin-svelte/src/plugins/compile-module.js index 99c815302..b7f9544e2 100644 --- a/packages/vite-plugin-svelte/src/plugins/compile-module.js +++ b/packages/vite-plugin-svelte/src/plugins/compile-module.js @@ -1,8 +1,51 @@ +import { buildModuleIdFilter, buildModuleIdParser } from '../utils/id.js'; +import * as svelteCompiler from 'svelte/compiler'; +import { logCompilerWarnings } from '../utils/log.js'; +import { toRollupError } from '../utils/error.js'; + /** + * @param {import('../types/plugin-api.d.ts').PluginAPI} api * @returns {import('vite').Plugin} */ -export function compileModule() { - return { - name: 'vite-plugin-svelte:compile-module' +export function compileModule(api) { + /** + * @type {import("../types/options.js").ResolvedOptions} + */ + let options; + /** + * @type {import("../types/id.js").ModuleIdParser} + */ + let idParser; + /** @type {import('vite').Plugin} */ + const plugin = { + name: 'vite-plugin-svelte-module', + enforce: 'post', + async configResolved() { + options = api.options; + //@ts-expect-error transform defined below but filter not in type + plugin.transform.filter = buildModuleIdFilter(options); + idParser = buildModuleIdParser(options); + }, + transform: { + async handler(code, id) { + const ssr = this.environment.config.consumer === 'server'; + const moduleRequest = idParser(id, ssr); + if (!moduleRequest) { + return; + } + try { + const compileResult = svelteCompiler.compileModule(code, { + dev: !this.environment.config.isProduction, + generate: ssr ? 'server' : 'client', + filename: moduleRequest.filename + }); + logCompilerWarnings(moduleRequest, compileResult.warnings, options); + return compileResult.js; + } catch (e) { + throw toRollupError(e, options); + } + } + } }; + return plugin; } diff --git a/packages/vite-plugin-svelte/src/plugins/compile.js b/packages/vite-plugin-svelte/src/plugins/compile.js index 55fa3b20b..560cf901d 100644 --- a/packages/vite-plugin-svelte/src/plugins/compile.js +++ b/packages/vite-plugin-svelte/src/plugins/compile.js @@ -1,8 +1,73 @@ +import { toRollupError } from '../utils/error.js'; +import { logCompilerWarnings } from '../utils/log.js'; +import { ensureWatchedFile } from '../utils/watch.js'; + /** + * @param {import('../types/plugin-api.d.ts').PluginAPI} api * @returns {import('vite').Plugin} */ -export function compile() { - return { - name: 'vite-plugin-svelte:compile' +export function compile(api) { + /** + * @type {import("../types/options.js").ResolvedOptions} + */ + let options; + + /** + * @type {import("../types/compile.d.ts").CompileSvelte} + */ + let compileSvelte; + /** @type {import('vite').Plugin} */ + const plugin = { + name: 'vite-plugin-svelte:compile', + configResolved() { + //@ts-expect-error defined below but filter not in type + plugin.transform.filter = api.idFilter; + options = api.options; + compileSvelte = api.compileSvelte; + }, + transform: { + async handler(code, id) { + // TODO: hack work around access restriction to meta in vite dev + const svelteMeta = Object.entries(this.getModuleInfo(id)?.meta ?? {}).find( + ([key]) => key === 'svelte' + )?.[1]; + const cache = api.getEnvironmentCache(this); + const ssr = this.environment.config.consumer === 'server'; + const svelteRequest = api.idParser(id, ssr); + if (!svelteRequest || svelteRequest.query.type === 'style' || svelteRequest.raw) { + return; + } + let compileData; + try { + compileData = await compileSvelte(svelteRequest, code, options, svelteMeta?.preprocessed); + } catch (e) { + cache.setError(svelteRequest, e); + throw toRollupError(e, options); + } + logCompilerWarnings(svelteRequest, compileData.compiled.warnings, options); + cache.update(compileData); + if (compileData.dependencies?.length) { + if (options.server) { + for (const dep of compileData.dependencies) { + ensureWatchedFile(options.server.watcher, dep, options.root); + } + } else if (options.isBuild && this.environment.config.build.watch) { + for (const dep of compileData.dependencies) { + this.addWatchFile(dep); + } + } + } + return { + ...compileData.compiled.js, + moduleType: 'js', + meta: { + vite: { + lang: compileData.lang + } + } + }; + } + } }; + return plugin; } diff --git a/packages/vite-plugin-svelte/src/plugins/configure.js b/packages/vite-plugin-svelte/src/plugins/configure.js index 8284d1cf4..9bbdffc2d 100644 --- a/packages/vite-plugin-svelte/src/plugins/configure.js +++ b/packages/vite-plugin-svelte/src/plugins/configure.js @@ -1,8 +1,6 @@ import process from 'node:process'; -import path from 'node:path'; -import { log } from '../utils/log.js'; +import { isDebugNamespaceEnabled, log } from '../utils/log.js'; import * as vite from 'vite'; -import { knownSvelteConfigNames } from '../utils/load-svelte-config.js'; import { VitePluginSvelteCache } from '../utils/vite-plugin-svelte-cache.js'; import { VitePluginSvelteStats } from '../utils/vite-plugin-svelte-stats.js'; import { @@ -13,9 +11,8 @@ import { ensureConfigEnvironmentMainFields, ensureConfigEnvironmentConditions } from '../utils/options.js'; -import {buildIdFilter, buildIdParser} from "../utils/id.js"; -import {createCompileSvelte} from "../utils/compile.js"; - +import { buildIdFilter, buildIdParser } from '../utils/id.js'; +import { createCompileSvelte } from '../utils/compile.js'; // @ts-expect-error rolldownVersion const { version: viteVersion, rolldownVersion, perEnvironmentState } = vite; @@ -25,10 +22,7 @@ const { version: viteVersion, rolldownVersion, perEnvironmentState } = vite; * @param {import('../types/plugin-api.d.ts').PluginAPI} api * @returns {import('vite').Plugin} */ -export function configure(api,inlineOptions) { - if (process.env.DEBUG != null) { - log.setLevel('debug'); - } +export function configure(api, inlineOptions) { if (rolldownVersion) { log.warn.once( `!!! Support for rolldown-vite in vite-plugin-svelte is experimental (rolldown: ${rolldownVersion}, vite: ${viteVersion}) !!!` @@ -38,16 +32,17 @@ export function configure(api,inlineOptions) { validateInlineOptions(inlineOptions); /** - * @type {import("../types/options.d.ts").PreResolvedOptions} - */ + * @type {import("../types/options.d.ts").PreResolvedOptions} + */ let preOptions; /** @type {import('vite').Plugin} */ return { name: 'vite-plugin-svelte:config', + api, // make sure it runs first enforce: 'pre', - config:{ + config: { order: 'pre', async handler(config, configEnv) { // setup logger @@ -69,11 +64,11 @@ export function configure(api,inlineOptions) { handler(config) { const options = resolveOptions(preOptions, config); api.options = options; - api.getEnvironmentState = perEnvironmentState(_env => { - const cache = new VitePluginSvelteCache() - const stats = new VitePluginSvelteStats(cache) - return {cache,stats} - }) + if (isDebugNamespaceEnabled('stats')) { + api.options.stats = new VitePluginSvelteStats(); + } + //@ts-expect-error perEnvironmentState uses a wider type for PluginContext + api.getEnvironmentCache = perEnvironmentState((_env) => new VitePluginSvelteCache()); api.idFilter = buildIdFilter(options); api.idParser = buildIdParser(options); @@ -95,78 +90,6 @@ export function configure(api,inlineOptions) { configureServer(server) { const { options } = api; options.server = server; - restartOnSvelteConfigChanges(options) - } - }; -} - -/** - * @param {import('../types/options.d.ts').ResolvedOptions} options - * @returns {void} - */ -export function restartOnSvelteConfigChanges(options) { - const { server, configFile: svelteConfigFile } = options; - if (!server) { - return; - } - const { watcher, ws } = server; - const { root, server: serverConfig } = server.config; - - - /** @type {(filename: string) => void} */ - const triggerViteRestart = (filename) => { - if (serverConfig.middlewareMode) { - // in middlewareMode we can't restart the server automatically - // show the user an overlay instead - const message = - 'Svelte config change detected, restart your dev process to apply the changes.'; - log.info(message, filename); - ws.send({ - type: 'error', - err: { message, stack: '', plugin: 'vite-plugin-svelte', id: filename } - }); - } else { - log.info(`svelte config changed: restarting vite server. - file: ${filename}`); - server.restart(); } }; - - // collection of watcher listeners by event - /** @type {Record} */ - const listenerCollection = { - add: [], - change: [], - unlink: [] - }; - - if (svelteConfigFile !== false) { - // configFile false means we ignore the file and external process is responsible - const possibleSvelteConfigs = knownSvelteConfigNames.map((cfg) => path.join(root, cfg)); - /** @type {(filename: string) => void} */ - const restartOnConfigAdd = (filename) => { - if (possibleSvelteConfigs.includes(filename)) { - triggerViteRestart(filename); - } - }; - - /** @type {(filename: string) => void} */ - const restartOnConfigChange = (filename) => { - if (filename === svelteConfigFile) { - triggerViteRestart(filename); - } - }; - - if (svelteConfigFile) { - listenerCollection.change.push(restartOnConfigChange); - listenerCollection.unlink.push(restartOnConfigChange); - } else { - listenerCollection.add.push(restartOnConfigAdd); - } - } - - Object.entries(listenerCollection).forEach(([evt, listeners]) => { - if (listeners.length > 0) { - watcher.on(evt, (filename) => listeners.forEach((listener) => listener(filename))); - } - }); } diff --git a/packages/vite-plugin-svelte/src/plugins/hot-update.js b/packages/vite-plugin-svelte/src/plugins/hot-update.js new file mode 100644 index 000000000..e9fbf90e5 --- /dev/null +++ b/packages/vite-plugin-svelte/src/plugins/hot-update.js @@ -0,0 +1,172 @@ +import { log } from '../utils/log.js'; +import { setupWatchers } from '../utils/watch.js'; +import { SVELTE_VIRTUAL_STYLE_ID_REGEX } from '../utils/constants.js'; + +/** + * @param {import('../types/plugin-api.d.ts').PluginAPI} api + * @returns {import('vite').Plugin} + */ +export function hotUpdate(api) { + /** + * @type {import("../types/options.js").ResolvedOptions} + */ + let options; + /** + * @type {import('../types/id.d.ts').IdParser} + */ + let idParser; + + /** + * + * @type {Map} + */ + const transformResultCache = new Map(); + + /** @type {import('vite').Plugin} */ + const plugin = { + name: 'vite-plugin-svelte:hot-update', + enforce: 'post', + configResolved() { + options = api.options; + idParser = api.idParser; + + // @ts-expect-error + plugin.transform.filter = { + id: { + // reinclude virtual styles to get their output + include: [...api.idFilter.id.include, SVELTE_VIRTUAL_STYLE_ID_REGEX], + exclude: [ + // ignore files in node_modules, we don't hot update them + /\/node_modules\//, + // remove style exclusion + ...api.idFilter.id.exclude.filter((filter) => filter !== SVELTE_VIRTUAL_STYLE_ID_REGEX) + ] + } + }; + }, + + applyToEnvironment(env) { + // we only handle updates for client components + // ssr frameworks have to handle updating/reloading themselves as v-p-s can't know what they prefer + const hmrEnabled = options.compilerOptions.hmr && options.emitCss; + return hmrEnabled && env.config.consumer === 'client'; + }, + + configureServer(server) { + const clientEnvironment = Object.values(server.environments).find( + (e) => e.config.consumer === 'client' + ); + if (clientEnvironment) { + setupWatchers(options, api.getEnvironmentCache({ environment: clientEnvironment })); + } else { + log.warn( + 'No client environment found, not adding watchers for svelte config and preprocessor dependencies' + ); + } + }, + + buildStart() { + transformResultCache.clear(); + }, + + transform: { + order: 'post', + handler(code, id) { + transformResultCache.set(id, code); + } + }, + + async hotUpdate(ctx) { + const svelteRequest = idParser(ctx.file, false, ctx.timestamp); + if (svelteRequest) { + const { modules } = ctx; + const svelteModules = modules.filter((m) => transformResultCache.has(m.id)); + if (svelteModules.length === 0) { + return; // nothing to do for us, unlikely to happen + } + const affectedModules = []; + const prevResults = svelteModules.map((m) => transformResultCache.get(m.id)); + for (let i = 0; i < svelteModules.length; i++) { + const mod = svelteModules[i]; + const prev = prevResults[i]; + await this.environment.transformRequest(mod.url); + const next = transformResultCache.get(mod.id); + if (!hasCodeChanged(prev, next, mod.id)) { + log.debug( + `skipping hot update for ${mod.id} because result is unchanged`, + undefined, + 'hmr' + ); + continue; + } + affectedModules.push(mod); + } + log.debug( + `hotUpdate for ${svelteRequest.id} result: [${affectedModules.map((m) => m.id).join(', ')}]`, + undefined, + 'hmr' + ); + return affectedModules; + } + } + }; + + return plugin; +} + +/** + * @param {string | undefined | null} prev + * @param {string | undefined | null} next + * @param {string | null} id + * @returns {boolean} + */ +function hasCodeChanged(prev, next, id) { + const isStrictEqual = nullSafeEqual(prev, next); + if (isStrictEqual) { + //console.log('strict equal ',{id,prev,next}) + return false; + } + ////console.log({normalizedNext,normalizedPrev}) + const isLooseEqual = nullSafeEqual(normalize(prev), normalize(next)); + if (!isStrictEqual && isLooseEqual) { + ////console.log('loose equal ',{filename,prev,next}) + log.debug( + `ignoring compiler output change for ${id} as it is equal to previous output after normalization`, + undefined, + 'hmr' + ); + } + return !isLooseEqual; +} + +/** + * @param {string | null | undefined} prev + * @param {string | null | undefined} next + * @returns {boolean} + */ +function nullSafeEqual(prev, next) { + return (prev == null && next == null) || (prev != null && next != null && prev === next); +} + +/** + * remove code that only changes metadata and does not require a js update for the component to keep working + * + * 1) add_location() calls. These add location metadata to elements, only used by some dev tools + * 2) vite query timestamps t=1235345. + * + * @param {string | null | undefined } code + * @returns {string | null | undefined} + */ +function normalize(code) { + if (code == null) { + return code; + } + + return ( + code + // svelte5 add_location line numbers argument + .replace(/(\$\.add_locations\(.*), \[(\[[.[\], \d]+])]/g, '$1, []') + // vite import analysis timestamp queries + .replace(/[?&]t=\d+/g, '') + ); +} diff --git a/packages/vite-plugin-svelte/src/plugins/load-compiled-css.js b/packages/vite-plugin-svelte/src/plugins/load-compiled-css.js index a48b09ce0..fc7ec9d61 100644 --- a/packages/vite-plugin-svelte/src/plugins/load-compiled-css.js +++ b/packages/vite-plugin-svelte/src/plugins/load-compiled-css.js @@ -10,9 +10,7 @@ const filter = { id: SVELTE_VIRTUAL_STYLE_ID_REGEX }; export function loadCompiledCss(api) { return { name: 'vite-plugin-svelte:load-compiled-css', - applyToEnvironment(env) { - return env.config.consumer === 'client'; // ssr compile does not emit css - }, + resolveId: { filter, // same filter in load to ensure minimal work handler(id) { @@ -23,8 +21,13 @@ export function loadCompiledCss(api) { load: { filter, async handler(id) { - const { cache } = api.getEnvironmentState(this); - const cachedCss = cache.getCSS(id); + const ssr = this.environment.config.consumer === 'server'; + const svelteRequest = api.idParser(id, ssr); + if (!svelteRequest) { + return; + } + const cache = api.getEnvironmentCache(this); + const cachedCss = cache.getCSS(svelteRequest); if (cachedCss) { const { hasGlobal, ...css } = cachedCss; if (hasGlobal === false) { diff --git a/packages/vite-plugin-svelte/src/plugins/load-custom.js b/packages/vite-plugin-svelte/src/plugins/load-custom.js index 4cb0a52a9..fe5908ad8 100644 --- a/packages/vite-plugin-svelte/src/plugins/load-custom.js +++ b/packages/vite-plugin-svelte/src/plugins/load-custom.js @@ -10,6 +10,7 @@ export function loadCustom(api) { /** @type {import('vite').Plugin} */ const plugin = { name: 'vite-plugin-svelte:load-custom', + enforce: 'pre', // must come before vites own asset handling or custom extensions like .svg won't work configResolved() { //@ts-expect-error load defined below but filter not in type plugin.load.filter = api.idFilter; diff --git a/packages/vite-plugin-svelte/src/plugins/preprocess.js b/packages/vite-plugin-svelte/src/plugins/preprocess.js index 5584c5832..5250b425d 100644 --- a/packages/vite-plugin-svelte/src/plugins/preprocess.js +++ b/packages/vite-plugin-svelte/src/plugins/preprocess.js @@ -1,8 +1,122 @@ +import { toRollupError } from '../utils/error.js'; +import { mapToRelative } from '../utils/sourcemaps.js'; +import { createInjectScopeEverythingRulePreprocessorGroup } from '../utils/preprocess.js'; +import * as svelte from 'svelte/compiler'; + /** + * @param {import('../types/plugin-api.d.ts').PluginAPI} api * @returns {import('vite').Plugin} */ -export function preprocess() { - return { - name: 'vite-plugin-svelte:preprocess' +export function preprocess(api) { + /** + * @type {import("../types/options.js").ResolvedOptions} + */ + let options; + + /** + * @type {import("../types/compile.d.ts").PreprocessSvelte} + */ + let preprocessSvelte; + /** @type {import('vite').Plugin} */ + const plugin = { + name: 'vite-plugin-svelte:preprocess', + enforce: 'pre', + configResolved() { + //@ts-expect-error defined below but filter not in type + plugin.transform.filter = api.idFilter; + options = api.options; + preprocessSvelte = createPreprocessSvelte(); + }, + + transform: { + async handler(code, id) { + const cache = api.getEnvironmentCache(this); + const ssr = this.environment.config.consumer === 'server'; + const svelteRequest = api.idParser(id, ssr); + if (!svelteRequest || svelteRequest.query.type === 'style' || svelteRequest.raw) { + return; + } + try { + return await preprocessSvelte(svelteRequest, code, options); + } catch (e) { + cache.setError(svelteRequest, e); + throw toRollupError(e, options); + } + } + } + }; + return plugin; +} /** + * @returns {import('../types/compile.d.ts').PreprocessSvelte} + */ +function createPreprocessSvelte() { + /** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */ + let stats; + const devStylePreprocessor = createInjectScopeEverythingRulePreprocessorGroup(); + /** @type {import('../types/compile.d.ts').PreprocessSvelte} */ + return async function preprocessSvelte(svelteRequest, code, options) { + const { filename, ssr } = svelteRequest; + + if (options.stats) { + if (options.isBuild) { + if (!stats) { + // build is either completely ssr or csr, create stats collector on first compile + // it is then finished in the buildEnd hook. + stats = options.stats.startCollection(`${ssr ? 'ssr' : 'dom'} preprocess`, { + logInProgress: () => false + }); + } + } else { + // dev time ssr, it's a ssr request and there are no stats, assume new page load and start collecting + if (ssr && !stats) { + stats = options.stats.startCollection('ssr compile'); + } + // stats are being collected but this isn't an ssr request, assume page loaded and stop collecting + if (!ssr && stats) { + stats.finish(); + stats = undefined; + } + // TODO find a way to trace dom compile during dev + // problem: we need to call finish at some point but have no way to tell if page load finished + // also they for hmr updates too + } + } + + let preprocessed; + let preprocessors = options.preprocess; + if (!options.isBuild && options.emitCss && options.compilerOptions?.hmr) { + // inject preprocessor that ensures css hmr works better + if (!Array.isArray(preprocessors)) { + preprocessors = preprocessors + ? [preprocessors, devStylePreprocessor] + : [devStylePreprocessor]; + } else { + preprocessors = preprocessors.concat(devStylePreprocessor); + } + } + if (preprocessors) { + try { + const endStat = stats?.start(filename); + preprocessed = await svelte.preprocess(code, preprocessors, { filename }); // full filename here so postcss works + endStat?.(); + } catch (e) { + e.message = `Error while preprocessing ${filename}${e.message ? ` - ${e.message}` : ''}`; + throw e; + } + + if (typeof preprocessed?.map === 'object') { + mapToRelative(preprocessed?.map, filename); + } + return /** @type {import('../types/compile.d.ts').PreprocessTransformOutput} */ { + code: preprocessed.code, + // @ts-expect-error + map: preprocessed.map, + meta: { + svelte: { + preprocessed + } + } + }; + } }; } diff --git a/packages/vite-plugin-svelte/src/types/compile.d.ts b/packages/vite-plugin-svelte/src/types/compile.d.ts index 64ba60737..603446376 100644 --- a/packages/vite-plugin-svelte/src/types/compile.d.ts +++ b/packages/vite-plugin-svelte/src/types/compile.d.ts @@ -1,14 +1,31 @@ import type { Processed, CompileResult } from 'svelte/compiler'; import type { SvelteRequest } from './id.d.ts'; import type { ResolvedOptions } from './options.d.ts'; -import type { CustomPluginOptionsVite } from 'vite'; +import type { CustomPluginOptionsVite, Rollup } from 'vite'; export type CompileSvelte = ( svelteRequest: SvelteRequest, code: string, - options: Partial + options: Partial, + preprocessed?: Processed ) => Promise; +export type PreprocessSvelte = ( + svelteRequest: SvelteRequest, + code: string, + options: Partial +) => Promise; + +export interface PreprocessTransformOutput { + code: string; + map: Rollup.SourceMapInput; + meta: { + svelte: { + preprocessed: Processed; + }; + }; +} + export interface Code { code: string; map?: any; diff --git a/packages/vite-plugin-svelte/src/types/plugin-api.d.ts b/packages/vite-plugin-svelte/src/types/plugin-api.d.ts index c6ca0cd8d..bb89bc942 100644 --- a/packages/vite-plugin-svelte/src/types/plugin-api.d.ts +++ b/packages/vite-plugin-svelte/src/types/plugin-api.d.ts @@ -1,17 +1,17 @@ import type { ResolvedOptions } from './options.d.ts'; -import { perEnvironmentState } from 'vite'; -import { VitePluginSvelteCache } from '../utils/vite-plugin-svelte-cache.js'; -import { VitePluginSvelteStats } from '../utils/vite-plugin-svelte-stats.js'; import type { IdFilter, IdParser } from './id.d.ts'; -import {CompileSvelte} from './compile.d.ts'; +import type { CompileSvelte } from './compile.d.ts'; +import type { Environment } from 'vite'; +// eslint-disable-next-line n/no-missing-import +import { VitePluginSvelteCache } from '../utils/vite-plugin-svelte-cache.js'; -interface EnvironmentState { - cache: VitePluginSvelteCache; - stats: VitePluginSvelteStats; +interface EnvContext { + environment: Environment; } + export interface PluginAPI { options: ResolvedOptions; - getEnvironmentState: ReturnType>; + getEnvironmentCache: (arg: EnvContext) => VitePluginSvelteCache; idFilter: IdFilter; idParser: IdParser; compileSvelte: CompileSvelte; diff --git a/packages/vite-plugin-svelte/src/utils/compile.js b/packages/vite-plugin-svelte/src/utils/compile.js index 86c747d42..550898bab 100644 --- a/packages/vite-plugin-svelte/src/utils/compile.js +++ b/packages/vite-plugin-svelte/src/utils/compile.js @@ -2,10 +2,7 @@ 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'; @@ -21,9 +18,8 @@ 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) { + return async function compileSvelte(svelteRequest, code, options, preprocessed) { const { filename, normalizedFilename, cssId, ssr, raw } = svelteRequest; const { emitCss = true } = options; /** @type {string[]} */ @@ -66,27 +62,7 @@ export function createCompileSvelte() { 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); - } - } - if (preprocessors) { - try { - preprocessed = await svelte.preprocess(code, preprocessors, { filename }); // full filename here so postcss works - } catch (e) { - e.message = `Error while preprocessing ${filename}${e.message ? ` - ${e.message}` : ''}`; - throw e; - } - + if (preprocessed) { if (preprocessed.dependencies?.length) { const checked = checkPreprocessDependencies(filename, preprocessed.dependencies); if (checked.warnings.length) { @@ -99,16 +75,14 @@ export function createCompileSvelte() { if (preprocessed.map) compileOptions.sourcemap = preprocessed.map; } - if (typeof preprocessed?.map === 'object') { - mapToRelative(preprocessed?.map, filename); - } + if (raw && svelteRequest.query.type === 'preprocessed') { // @ts-expect-error shortcut return /** @type {import('../types/compile.d.ts').CompileData} */ { preprocessed: preprocessed ?? { code } }; } - const finalCode = preprocessed ? preprocessed.code : code; + const finalCode = code; const dynamicCompileOptions = await options?.dynamicCompileOptions?.({ filename, code: finalCode, @@ -145,7 +119,7 @@ export function createCompileSvelte() { ); } } catch (e) { - enhanceCompileError(e, code, preprocessors); + enhanceCompileError(e, code, options.preprocess); throw e; } diff --git a/packages/vite-plugin-svelte/src/utils/constants.js b/packages/vite-plugin-svelte/src/utils/constants.js index 20cfc1a3e..449a05246 100644 --- a/packages/vite-plugin-svelte/src/utils/constants.js +++ b/packages/vite-plugin-svelte/src/utils/constants.js @@ -30,7 +30,4 @@ export const DEFAULT_SVELTE_EXT = ['.svelte']; export const DEFAULT_SVELTE_MODULE_INFIX = ['.svelte.']; export const DEFAULT_SVELTE_MODULE_EXT = ['.js', '.ts']; -export const SVELTE_VIRTUAL_STYLE_SUFFIX = '?svelte&type=style&lang.css'; -export const SVELTE_VIRTUAL_STYLE_ID_REGEX = new RegExp( - `${SVELTE_VIRTUAL_STYLE_SUFFIX.replace(/[?.]/g, '\\$&')}$` -); +export const SVELTE_VIRTUAL_STYLE_ID_REGEX = /[?&]svelte&type=style&lang.css$/; diff --git a/packages/vite-plugin-svelte/src/utils/id.js b/packages/vite-plugin-svelte/src/utils/id.js index 9bf967673..90bd16d9e 100644 --- a/packages/vite-plugin-svelte/src/utils/id.js +++ b/packages/vite-plugin-svelte/src/utils/id.js @@ -5,7 +5,8 @@ import { log } from './log.js'; import { DEFAULT_SVELTE_EXT, DEFAULT_SVELTE_MODULE_EXT, - DEFAULT_SVELTE_MODULE_INFIX + DEFAULT_SVELTE_MODULE_INFIX, + SVELTE_VIRTUAL_STYLE_ID_REGEX } from './constants.js'; import { arraify } from './options.js'; @@ -179,13 +180,15 @@ export function buildIdFilter(options) { .map(escapeRE) .join('|')})(?:[?#]|$)` ); - const filter = { + return { id: { include: [extensionsRE, .../**@type {Array}*/ arraify(include)], - exclude: /**@type {Array}*/ arraify(exclude) + exclude: /**@type {Array}*/ [ + SVELTE_VIRTUAL_STYLE_ID_REGEX, // exclude from regular pipeline, we load it in a separate plugin + ...arraify(exclude) + ] } }; - return filter; } /** diff --git a/packages/vite-plugin-svelte/src/utils/options.js b/packages/vite-plugin-svelte/src/utils/options.js index 0507dedfd..ae00abf28 100644 --- a/packages/vite-plugin-svelte/src/utils/options.js +++ b/packages/vite-plugin-svelte/src/utils/options.js @@ -9,7 +9,7 @@ const { //@ts-expect-error rolldownVersion not in type rolldownVersion } = vite; -import { isDebugNamespaceEnabled, log } from './log.js'; +import { log } from './log.js'; import { loadSvelteConfig } from './load-svelte-config.js'; import { DEFAULT_SVELTE_EXT, @@ -37,7 +37,6 @@ import { } from 'vitefu'; import { isCommonDepWithoutSvelteField } from './dependencies.js'; -import { VitePluginSvelteStats } from './vite-plugin-svelte-stats.js'; const allowedPluginOptions = new Set([ 'include', diff --git a/packages/vite-plugin-svelte/src/utils/vite-plugin-svelte-cache.js b/packages/vite-plugin-svelte/src/utils/vite-plugin-svelte-cache.js index f2fcde2a5..1a6580c31 100644 --- a/packages/vite-plugin-svelte/src/utils/vite-plugin-svelte-cache.js +++ b/packages/vite-plugin-svelte/src/utils/vite-plugin-svelte-cache.js @@ -1,17 +1,3 @@ -import { readFileSync } from 'node:fs'; -import { dirname } from 'node:path'; -import { findClosestPkgJsonPath } from 'vitefu'; -import { normalizePath } from 'vite'; - -/** - * @typedef {{ - * name: string; - * version: string; - * svelte?: string; - * path: string; - * }} PackageInfo - */ - /** * @class */ @@ -26,8 +12,6 @@ export class VitePluginSvelteCache { #dependants = new Map(); /** @type {Map} */ #errors = new Map(); - /** @type {PackageInfo[]} */ - #packageInfos = []; /** * @param {import('../types/compile.d.ts').CompileData} compileData @@ -63,7 +47,7 @@ export class VitePluginSvelteCache { * @param {import('../types/compile.d.ts').CompileData} compileData */ #updateCSS(compileData) { - this.#css.set(compileData.cssId, compileData.compiled.css); + this.#css.set(compileData.normalizedFilename, compileData.compiled.css); } /** @@ -132,14 +116,13 @@ export class VitePluginSvelteCache { } /** - * @param {string} cssId + * @param {import('../types/id.d.ts').SvelteRequest} svelteRequest * @returns {import('../types/compile.d.ts').Code | undefined | null} */ - getCSS(cssId) { - return this.#css.get(cssId); + getCSS(svelteRequest) { + return this.#css.get(svelteRequest.normalizedFilename); } - /** * @param {import('../types/id.d.ts').SvelteRequest} svelteRequest * @returns {import('../types/compile.d.ts').Code | undefined | null} @@ -150,6 +133,7 @@ export class VitePluginSvelteCache { return this.#js.get(svelteRequest.normalizedFilename); } } + /** * @param {import('../types/id.d.ts').SvelteRequest} svelteRequest * @returns {any} @@ -166,48 +150,4 @@ export class VitePluginSvelteCache { const dependants = this.#dependants.get(path); return dependants ? [...dependants] : []; } - - /** - * @param {string} file - * @returns {Promise} - */ - async getPackageInfo(file) { - let info = this.#packageInfos.find((pi) => file.startsWith(pi.path)); - if (!info) { - info = await findPackageInfo(file); - this.#packageInfos.push(info); - } - return info; - } -} - -/** - * utility to get some info from the closest package.json with a "name" set - * - * @param {string} file to find info for - * @returns {Promise} - */ -async function findPackageInfo(file) { - /** @type {PackageInfo} */ - const info = { - name: '$unknown', - version: '0.0.0-unknown', - path: '$unknown' - }; - let path = await findClosestPkgJsonPath(file, (pkgPath) => { - const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); - if (pkg.name != null) { - info.name = pkg.name; - if (pkg.version != null) { - info.version = pkg.version; - } - info.svelte = pkg.svelte; - return true; - } - return false; - }); - // return normalized path with appended '/' so .startsWith works for future file checks - path = normalizePath(dirname(path ?? file)) + '/'; - info.path = path; - return info; } diff --git a/packages/vite-plugin-svelte/src/utils/vite-plugin-svelte-stats.js b/packages/vite-plugin-svelte/src/utils/vite-plugin-svelte-stats.js index 6d8bcea42..4b7e825fd 100644 --- a/packages/vite-plugin-svelte/src/utils/vite-plugin-svelte-stats.js +++ b/packages/vite-plugin-svelte/src/utils/vite-plugin-svelte-stats.js @@ -1,6 +1,9 @@ import { log } from './log.js'; import { performance } from 'node:perf_hooks'; import { normalizePath } from 'vite'; +import { findClosestPkgJsonPath } from 'vitefu'; +import { readFileSync } from 'node:fs'; +import { dirname } from 'node:path'; /** @type {import('../types/vite-plugin-svelte-stats.d.ts').CollectionOptions} */ const defaultCollectionOptions = { @@ -63,19 +66,12 @@ function formatPackageStats(pkgStats) { * @class */ export class VitePluginSvelteStats { - // package directory -> package name - /** @type {import('./vite-plugin-svelte-cache.js').VitePluginSvelteCache} */ - #cache; + /** @type {PackageInfo[]} */ + #packageInfos = []; + /** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection[]} */ #collections = []; - /** - * @param {import('./vite-plugin-svelte-cache.js').VitePluginSvelteCache} cache - */ - constructor(cache) { - this.#cache = cache; - } - /** * @param {string} name * @param {Partial} [opts] @@ -173,7 +169,7 @@ export class VitePluginSvelteStats { async #aggregateStatsResult(collection) { const stats = collection.stats; for (const stat of stats) { - stat.pkg = (await this.#cache.getPackageInfo(stat.file)).name; + stat.pkg = (await this.#getPackageInfo(stat.file)).name; } // group stats @@ -197,4 +193,56 @@ export class VitePluginSvelteStats { groups.sort((a, b) => b.duration - a.duration); collection.packageStats = groups; } + /** + * @param {string} file + * @returns {Promise} + */ + async #getPackageInfo(file) { + let info = this.#packageInfos.find((pi) => file.startsWith(pi.path)); + if (!info) { + info = await findPackageInfo(file); + this.#packageInfos.push(info); + } + return info; + } +} + +/** + * @typedef {{ + * name: string; + * version: string; + * svelte?: string; + * path: string; + * }} PackageInfo + */ + +/** + * utility to get some info from the closest package.json with a "name" set + * + * @param {string} file to find info for + * @returns {Promise} + */ +async function findPackageInfo(file) { + /** @type {PackageInfo} */ + const info = { + name: '$unknown', + version: '0.0.0-unknown', + path: '$unknown' + }; + let path = await findClosestPkgJsonPath(file, (pkgPath) => { + const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); + if (pkg.name != null) { + info.name = pkg.name; + if (pkg.version != null) { + info.version = pkg.version; + } + info.svelte = pkg.svelte; + return true; + } + return false; + }); + // return normalized path with appended '/' so .startsWith works for future file checks + path = normalizePath(dirname(path ?? file)) + '/'; + info.path = path; + return info; } diff --git a/packages/vite-plugin-svelte/types/index.d.ts b/packages/vite-plugin-svelte/types/index.d.ts index aa741b9dd..d19e12fc7 100644 --- a/packages/vite-plugin-svelte/types/index.d.ts +++ b/packages/vite-plugin-svelte/types/index.d.ts @@ -206,6 +206,11 @@ declare module '@sveltejs/vite-plugin-svelte' { */ style?: boolean | InlineConfig | ResolvedConfig; } + /** + * returns a list of plugins to handle svelte files + * plugins are named `vite-plugin-svelte:` + * + * */ export function svelte(inlineOptions?: Partial): import("vite").Plugin[]; export function vitePreprocess(opts?: VitePreprocessOptions): import("svelte/compiler").PreprocessorGroup; export function loadSvelteConfig(viteConfig?: import("vite").UserConfig, inlineOptions?: Partial): Promise | undefined>; diff --git a/packages/vite-plugin-svelte/types/index.d.ts.map b/packages/vite-plugin-svelte/types/index.d.ts.map index 5d5287027..fb5d535bb 100644 --- a/packages/vite-plugin-svelte/types/index.d.ts.map +++ b/packages/vite-plugin-svelte/types/index.d.ts.map @@ -26,6 +26,6 @@ null, null ], - "mappings": ";;;;aAIYA,OAAOA;;WAETC,mBAAmBA;;;;;;;;;;;kBAWZC,aAAaA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAgGbC,YAAYA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAiDnBC,mBAAmBA;;;;;;;;;;;;;;;;WAgBnBC,oBAAoBA;;;;;;;;;;;;;;;MAezBC,SAASA;;kBAEGC,qBAAqBA;;;;;;;;;;;;;iBC/JtBC,MAAMA;iBCXNC,cAAcA;iBCORC,gBAAgBA", + "mappings": ";;;;aAIYA,OAAOA;;WAETC,mBAAmBA;;;;;;;;;;;kBAWZC,aAAaA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAgGbC,YAAYA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAiDnBC,mBAAmBA;;;;;;;;;;;;;;;;WAgBnBC,oBAAoBA;;;;;;;;;;;;;;;MAezBC,SAASA;;kBAEGC,qBAAqBA;;;;;;;;;;;;;;;;;;iBChLtBC,MAAMA;iBCMNC,cAAcA;iBCORC,gBAAgBA", "ignoreList": [] } \ No newline at end of file From 01061c9537a1ae350c16df8d58721ddffe78acdd Mon Sep 17 00:00:00 2001 From: dominikg Date: Mon, 23 Jun 2025 11:10:13 +0200 Subject: [PATCH 05/18] ensure hotUpdate returns non-svelte modules, cleanup --- .../vite-plugin-svelte/src/plugins/compile.js | 5 +- .../src/plugins/hot-update.js | 72 ++++++++++--------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/packages/vite-plugin-svelte/src/plugins/compile.js b/packages/vite-plugin-svelte/src/plugins/compile.js index 560cf901d..ae576e7e8 100644 --- a/packages/vite-plugin-svelte/src/plugins/compile.js +++ b/packages/vite-plugin-svelte/src/plugins/compile.js @@ -27,10 +27,7 @@ export function compile(api) { }, transform: { async handler(code, id) { - // TODO: hack work around access restriction to meta in vite dev - const svelteMeta = Object.entries(this.getModuleInfo(id)?.meta ?? {}).find( - ([key]) => key === 'svelte' - )?.[1]; + const svelteMeta = this.getModuleInfo(id)?.meta?.svelte; const cache = api.getEnvironmentCache(this); const ssr = this.environment.config.consumer === 'server'; const svelteRequest = api.idParser(id, ssr); diff --git a/packages/vite-plugin-svelte/src/plugins/hot-update.js b/packages/vite-plugin-svelte/src/plugins/hot-update.js index e9fbf90e5..8033f858a 100644 --- a/packages/vite-plugin-svelte/src/plugins/hot-update.js +++ b/packages/vite-plugin-svelte/src/plugins/hot-update.js @@ -75,38 +75,49 @@ export function hotUpdate(api) { transformResultCache.set(id, code); } }, + hotUpdate: { + order: 'post', + async handler(ctx) { + const svelteRequest = idParser(ctx.file, false, ctx.timestamp); + if (svelteRequest) { + const { modules } = ctx; + const svelteModules = []; + const nonSvelteModules = []; + for (const mod of modules) { + if (transformResultCache.has(mod.id)) { + svelteModules.push(mod); + } else { + nonSvelteModules.push(mod); + } + } - async hotUpdate(ctx) { - const svelteRequest = idParser(ctx.file, false, ctx.timestamp); - if (svelteRequest) { - const { modules } = ctx; - const svelteModules = modules.filter((m) => transformResultCache.has(m.id)); - if (svelteModules.length === 0) { - return; // nothing to do for us, unlikely to happen - } - const affectedModules = []; - const prevResults = svelteModules.map((m) => transformResultCache.get(m.id)); - for (let i = 0; i < svelteModules.length; i++) { - const mod = svelteModules[i]; - const prev = prevResults[i]; - await this.environment.transformRequest(mod.url); - const next = transformResultCache.get(mod.id); - if (!hasCodeChanged(prev, next, mod.id)) { - log.debug( - `skipping hot update for ${mod.id} because result is unchanged`, - undefined, - 'hmr' - ); - continue; + if (svelteModules.length === 0) { + return; // nothing to do for us } - affectedModules.push(mod); + const affectedModules = []; + const prevResults = svelteModules.map((m) => transformResultCache.get(m.id)); + for (let i = 0; i < svelteModules.length; i++) { + const mod = svelteModules[i]; + const prev = prevResults[i]; + await this.environment.transformRequest(mod.url); + const next = transformResultCache.get(mod.id); + if (hasCodeChanged(prev, next, mod.id)) { + affectedModules.push(mod); + } else { + log.debug( + `skipping hot update for ${mod.id} because result is unchanged`, + undefined, + 'hmr' + ); + } + } + log.debug( + `hotUpdate for ${svelteRequest.id} result: [${affectedModules.map((m) => m.id).join(', ')}]`, + undefined, + 'hmr' + ); + return [...affectedModules, ...nonSvelteModules]; } - log.debug( - `hotUpdate for ${svelteRequest.id} result: [${affectedModules.map((m) => m.id).join(', ')}]`, - undefined, - 'hmr' - ); - return affectedModules; } } }; @@ -123,13 +134,10 @@ export function hotUpdate(api) { function hasCodeChanged(prev, next, id) { const isStrictEqual = nullSafeEqual(prev, next); if (isStrictEqual) { - //console.log('strict equal ',{id,prev,next}) return false; } - ////console.log({normalizedNext,normalizedPrev}) const isLooseEqual = nullSafeEqual(normalize(prev), normalize(next)); if (!isStrictEqual && isLooseEqual) { - ////console.log('loose equal ',{filename,prev,next}) log.debug( `ignoring compiler output change for ${id} as it is equal to previous output after normalization`, undefined, From 04904c2ecdbc5c41d324ed28d81efa7fd53b6ae5 Mon Sep 17 00:00:00 2001 From: dominikg Date: Tue, 24 Jun 2025 19:25:55 +0200 Subject: [PATCH 06/18] fix: load raw&svelte files ourselves to prevent vite asset middleware from turning them into a default export, reorder plugins --- packages/vite-plugin-svelte/src/index.js | 7 +- .../src/plugins/compile-module.js | 2 +- .../vite-plugin-svelte/src/plugins/compile.js | 129 ++++++++++++++- .../src/plugins/load-custom.js | 150 ++---------------- .../src/plugins/preprocess.js | 51 +++--- .../vite-plugin-svelte/src/utils/compile.js | 17 +- 6 files changed, 182 insertions(+), 174 deletions(-) diff --git a/packages/vite-plugin-svelte/src/index.js b/packages/vite-plugin-svelte/src/index.js index 9a5ab91b7..83e0cc720 100644 --- a/packages/vite-plugin-svelte/src/index.js +++ b/packages/vite-plugin-svelte/src/index.js @@ -25,14 +25,15 @@ export function svelte(inlineOptions) { // @ts-expect-error initialize empty to guard against early use const api = {}; // initialized by configure plugin, used in others return [ + { name: 'vite-plugin-svelte' }, // marker for detection logic in other plugins that expect this name configure(api, inlineOptions), setupOptimizer(api), - preprocess(api), - compile(api), - hotUpdate(api), loadCompiledCss(api), loadCustom(api), + preprocess(api), + compile(api), compileModule(api), + hotUpdate(api), svelteInspector() ]; } diff --git a/packages/vite-plugin-svelte/src/plugins/compile-module.js b/packages/vite-plugin-svelte/src/plugins/compile-module.js index b7f9544e2..2ae406cfe 100644 --- a/packages/vite-plugin-svelte/src/plugins/compile-module.js +++ b/packages/vite-plugin-svelte/src/plugins/compile-module.js @@ -18,7 +18,7 @@ export function compileModule(api) { let idParser; /** @type {import('vite').Plugin} */ const plugin = { - name: 'vite-plugin-svelte-module', + name: 'vite-plugin-svelte:compile-module', enforce: 'post', async configResolved() { options = api.options; diff --git a/packages/vite-plugin-svelte/src/plugins/compile.js b/packages/vite-plugin-svelte/src/plugins/compile.js index ae576e7e8..20fa6c815 100644 --- a/packages/vite-plugin-svelte/src/plugins/compile.js +++ b/packages/vite-plugin-svelte/src/plugins/compile.js @@ -1,5 +1,6 @@ import { toRollupError } from '../utils/error.js'; -import { logCompilerWarnings } from '../utils/log.js'; +import { log, logCompilerWarnings } from '../utils/log.js'; +import fs from 'node:fs'; import { ensureWatchedFile } from '../utils/watch.js'; /** @@ -27,21 +28,90 @@ export function compile(api) { }, transform: { async handler(code, id) { - const svelteMeta = this.getModuleInfo(id)?.meta?.svelte; - const cache = api.getEnvironmentCache(this); const ssr = this.environment.config.consumer === 'server'; const svelteRequest = api.idParser(id, ssr); - if (!svelteRequest || svelteRequest.query.type === 'style' || svelteRequest.raw) { + if (!svelteRequest) { return; } + const cache = api.getEnvironmentCache(this); let compileData; try { - compileData = await compileSvelte(svelteRequest, code, options, svelteMeta?.preprocessed); + /** + * @type {import("../types/options.js").ResolvedOptions} + */ + const finalOptions = svelteRequest.raw + ? { + ...options, + // don't use dynamic vite-plugin-svelte defaults here to ensure stable result between ssr,dev and build + compilerOptions: { + dev: false, + css: 'external', + hmr: false, + ...svelteRequest.query.compilerOptions + }, + emitCss: true + } + : options; + const svelteMeta = this.getModuleInfo(id)?.meta?.svelte; + compileData = await compileSvelte( + svelteRequest, + code, + finalOptions, + svelteMeta?.preprocessed + ); } catch (e) { cache.setError(svelteRequest, e); throw toRollupError(e, options); } - logCompilerWarnings(svelteRequest, compileData.compiled.warnings, options); + if (compileData.compiled?.warnings) { + logCompilerWarnings(svelteRequest, compileData.compiled.warnings, options); + } + if (svelteRequest.raw) { + const query = svelteRequest.query; + let result; + if (query.type === 'style') { + result = compileData.compiled.css ?? { code: '', map: null }; + } else if (query.type === 'script') { + result = compileData.compiled.js; + } else if (query.type === 'preprocessed') { + result = compileData.preprocessed; + } else if (query.type === 'all' && query.raw) { + return allToRawExports(compileData, fs.readFileSync(compileData.filename, 'utf-8')); + } else { + throw new Error( + `invalid "type=${query.type}" in ${id}. supported are script, style, preprocessed, all` + ); + } + if (query.direct) { + const supportedDirectTypes = ['script', 'style']; + if (!supportedDirectTypes.includes(query.type)) { + throw new Error( + `invalid "type=${ + query.type + }" combined with direct in ${id}. supported are: ${supportedDirectTypes.join(', ')}` + ); + } + log.debug(`load returns direct result for ${id}`, undefined, 'load'); + let directOutput = result.code; + // @ts-expect-error might not be SourceMap but toUrl check should suffice + if (query.sourcemap && result.map?.toUrl) { + // @ts-expect-error toUrl might not exist + const map = `sourceMappingURL=${result.map.toUrl()}`; + if (query.type === 'style') { + directOutput += `\n\n/*# ${map} */\n`; + } else if (query.type === 'script') { + directOutput += `\n\n//# ${map}\n`; + } + } + return directOutput; + } else if (query.raw) { + log.debug(`load returns raw result for ${id}`, undefined, 'load'); + return toRawExports(result); + } else { + throw new Error(`invalid raw mode in ${id}, supported are raw, direct`); + } + } + cache.update(compileData); if (compileData.dependencies?.length) { if (options.server) { @@ -68,3 +138,50 @@ export function compile(api) { }; return plugin; } + +/** + * turn compileData and source into a flat list of raw exports + * + * @param {import('../types/compile.d.ts').CompileData} compileData + * @param {string} source + */ +function allToRawExports(compileData, source) { + // flatten CompileData + /** @type {Partial} */ + const exports = { + ...compileData, + ...compileData.compiled, + source + }; + delete exports.compiled; + delete exports.filename; // absolute path, remove to avoid it in output + return toRawExports(exports); +} + +/** + * turn object into raw exports. + * + * every prop is returned as a const export, and if prop 'code' exists it is additionally added as default export + * + * eg {'foo':'bar','code':'baz'} results in + * + * ```js + * export const code='baz' + * export const foo='bar' + * export default code + * ``` + * @param {object} object + * @returns {string} + */ +function toRawExports(object) { + let exports = + Object.entries(object) + .filter(([_key, value]) => typeof value !== 'function') // preprocess output has a toString function that's enumerable + .sort(([a], [b]) => (a < b ? -1 : a === b ? 0 : 1)) + .map(([key, value]) => `export const ${key}=${JSON.stringify(value)}`) + .join('\n') + '\n'; + if (Object.prototype.hasOwnProperty.call(object, 'code')) { + exports += 'export default code\n'; + } + return exports; +} diff --git a/packages/vite-plugin-svelte/src/plugins/load-custom.js b/packages/vite-plugin-svelte/src/plugins/load-custom.js index fe5908ad8..613a4bf51 100644 --- a/packages/vite-plugin-svelte/src/plugins/load-custom.js +++ b/packages/vite-plugin-svelte/src/plugins/load-custom.js @@ -1,8 +1,10 @@ import fs from 'node:fs'; -import { toRollupError } from '../utils/error.js'; import { log } from '../utils/log.js'; /** + * if svelte config includes files that vite treats as assets (e.g. .svg) + * we have to manually load them to avoid getting urls + * * @param {import('../types/plugin-api.d.ts').PluginAPI} api * @returns {import('vite').Plugin} */ @@ -23,22 +25,14 @@ export function loadCustom(api) { const ssr = config.consumer === 'server'; const svelteRequest = api.idParser(id, ssr); if (svelteRequest) { - const { filename, raw } = svelteRequest; - if (raw) { - const code = await compileRaw(svelteRequest, api.compileSvelte, api.options); - // prevent vite from injecting sourcemaps in the results. - return { - code, - map: { - mappings: '' - } - }; - } else { - // prevent vite asset plugin from loading files as url that should be compiled in transform - if (config.assetsInclude(filename)) { - log.debug(`load returns raw content for ${filename}`, undefined, 'load'); - return fs.readFileSync(filename, 'utf-8'); - } + const { filename, raw, query } = svelteRequest; + if (!query.url && (raw || config.assetsInclude(filename))) { + log.debug( + `loading ${filename} to prevent vite asset handling to turn it into a url by default`, + undefined, + 'load' + ); + return fs.readFileSync(filename, 'utf-8'); } } } @@ -46,125 +40,3 @@ export function loadCustom(api) { }; return plugin; } - -/** - * utility function to compile ?raw and ?direct requests in load hook - * - * @param {import('../types/id.d.ts').SvelteRequest} svelteRequest - * @param {import('../types/compile.d.ts').CompileSvelte} compileSvelte - * @param {import('../types/options.d.ts').ResolvedOptions} options - * @returns {Promise} - */ -async function compileRaw(svelteRequest, compileSvelte, options) { - const { id, filename, query } = svelteRequest; - - // raw svelte subrequest, compile on the fly and return requested subpart - let compileData; - const source = fs.readFileSync(filename, 'utf-8'); - try { - //avoid compileSvelte doing extra ssr stuff unless requested - svelteRequest.ssr = query.compilerOptions?.generate === 'server'; - compileData = await compileSvelte(svelteRequest, source, { - ...options, - // don't use dynamic vite-plugin-svelte defaults here to ensure stable result between ssr,dev and build - compilerOptions: { - dev: false, - css: 'external', - hmr: false, - ...svelteRequest.query.compilerOptions - }, - emitCss: true - }); - } catch (e) { - throw toRollupError(e, options); - } - let result; - if (query.type === 'style') { - result = compileData.compiled.css ?? { code: '', map: null }; - } else if (query.type === 'script') { - result = compileData.compiled.js; - } else if (query.type === 'preprocessed') { - result = compileData.preprocessed; - } else if (query.type === 'all' && query.raw) { - return allToRawExports(compileData, source); - } else { - throw new Error( - `invalid "type=${query.type}" in ${id}. supported are script, style, preprocessed, all` - ); - } - if (query.direct) { - const supportedDirectTypes = ['script', 'style']; - if (!supportedDirectTypes.includes(query.type)) { - throw new Error( - `invalid "type=${ - query.type - }" combined with direct in ${id}. supported are: ${supportedDirectTypes.join(', ')}` - ); - } - log.debug(`load returns direct result for ${id}`, undefined, 'load'); - let directOutput = result.code; - // @ts-expect-error might not be SourceMap but toUrl check should suffice - if (query.sourcemap && result.map?.toUrl) { - // @ts-expect-error toUrl might not exist - const map = `sourceMappingURL=${result.map.toUrl()}`; - if (query.type === 'style') { - directOutput += `\n\n/*# ${map} */\n`; - } else if (query.type === 'script') { - directOutput += `\n\n//# ${map}\n`; - } - } - return directOutput; - } else if (query.raw) { - log.debug(`load returns raw result for ${id}`, undefined, 'load'); - return toRawExports(result); - } else { - throw new Error(`invalid raw mode in ${id}, supported are raw, direct`); - } -} - -/** - * turn compileData and source into a flat list of raw exports - * - * @param {import('../types/compile.d.ts').CompileData} compileData - * @param {string} source - */ -function allToRawExports(compileData, source) { - // flatten CompileData - /** @type {Partial} */ - const exports = { - ...compileData, - ...compileData.compiled, - source - }; - delete exports.compiled; - delete exports.filename; // absolute path, remove to avoid it in output - return toRawExports(exports); -} - -/** - * turn object into raw exports. - * - * every prop is returned as a const export, and if prop 'code' exists it is additionally added as default export - * - * eg {'foo':'bar','code':'baz'} results in - * - * ```js - * export const code='baz' - * export const foo='bar' - * export default code - * ``` - * @param {object} object - * @returns {string} - */ -function toRawExports(object) { - let exports = - Object.entries(object) - .filter(([_key, value]) => typeof value !== 'function') // preprocess output has a toString function that's enumerable - .sort(([a], [b]) => (a < b ? -1 : a === b ? 0 : 1)) - .map(([key, value]) => `export const ${key}=${JSON.stringify(value)}`) - .join('\n') + '\n'; - if (Object.prototype.hasOwnProperty.call(object, 'code')) { - exports += 'export default code\n'; - } - return exports; -} diff --git a/packages/vite-plugin-svelte/src/plugins/preprocess.js b/packages/vite-plugin-svelte/src/plugins/preprocess.js index 5250b425d..ce05b6ce8 100644 --- a/packages/vite-plugin-svelte/src/plugins/preprocess.js +++ b/packages/vite-plugin-svelte/src/plugins/preprocess.js @@ -1,7 +1,8 @@ import { toRollupError } from '../utils/error.js'; import { mapToRelative } from '../utils/sourcemaps.js'; -import { createInjectScopeEverythingRulePreprocessorGroup } from '../utils/preprocess.js'; import * as svelte from 'svelte/compiler'; +import { log } from '../utils/log.js'; +import { arraify } from '../utils/options.js'; /** * @param {import('../types/plugin-api.d.ts').PluginAPI} api @@ -21,11 +22,20 @@ export function preprocess(api) { const plugin = { name: 'vite-plugin-svelte:preprocess', enforce: 'pre', - configResolved() { + configResolved(c) { //@ts-expect-error defined below but filter not in type plugin.transform.filter = api.idFilter; options = api.options; - preprocessSvelte = createPreprocessSvelte(); + if (arraify(options.preprocess).length > 0) { + preprocessSvelte = createPreprocessSvelte(options, c); + } else { + log.debug( + `disabling ${plugin.name} because no preprocessor is configured`, + undefined, + 'preprocess' + ); + delete plugin.transform; + } }, transform: { @@ -33,7 +43,7 @@ export function preprocess(api) { const cache = api.getEnvironmentCache(this); const ssr = this.environment.config.consumer === 'server'; const svelteRequest = api.idParser(id, ssr); - if (!svelteRequest || svelteRequest.query.type === 'style' || svelteRequest.raw) { + if (!svelteRequest) { return; } try { @@ -46,13 +56,24 @@ export function preprocess(api) { } }; return plugin; -} /** +} +/** + * @param {import("../types/options.js").ResolvedOptions} options + * @param {import("vite").ResolvedConfig} resolvedConfig * @returns {import('../types/compile.d.ts').PreprocessSvelte} */ -function createPreprocessSvelte() { +function createPreprocessSvelte(options, resolvedConfig) { /** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */ let stats; - const devStylePreprocessor = createInjectScopeEverythingRulePreprocessorGroup(); + /** @type {Array} */ + const preprocessors = arraify(options.preprocess); + + for (const preprocessor of preprocessors) { + if (preprocessor.style && '__resolvedConfig' in preprocessor.style) { + preprocessor.style.__resolvedConfig = resolvedConfig; + } + } + /** @type {import('../types/compile.d.ts').PreprocessSvelte} */ return async function preprocessSvelte(svelteRequest, code, options) { const { filename, ssr } = svelteRequest; @@ -69,7 +90,7 @@ function createPreprocessSvelte() { } else { // dev time ssr, it's a ssr request and there are no stats, assume new page load and start collecting if (ssr && !stats) { - stats = options.stats.startCollection('ssr compile'); + stats = options.stats.startCollection('ssr preprocess'); } // stats are being collected but this isn't an ssr request, assume page loaded and stop collecting if (!ssr && stats) { @@ -83,18 +104,8 @@ function createPreprocessSvelte() { } let preprocessed; - let preprocessors = options.preprocess; - if (!options.isBuild && options.emitCss && options.compilerOptions?.hmr) { - // inject preprocessor that ensures css hmr works better - if (!Array.isArray(preprocessors)) { - preprocessors = preprocessors - ? [preprocessors, devStylePreprocessor] - : [devStylePreprocessor]; - } else { - preprocessors = preprocessors.concat(devStylePreprocessor); - } - } - if (preprocessors) { + + if (preprocessors && preprocessors.length > 0) { try { const endStat = stats?.start(filename); preprocessed = await svelte.preprocess(code, preprocessors, { filename }); // full filename here so postcss works diff --git a/packages/vite-plugin-svelte/src/utils/compile.js b/packages/vite-plugin-svelte/src/utils/compile.js index 550898bab..2273c2e3c 100644 --- a/packages/vite-plugin-svelte/src/utils/compile.js +++ b/packages/vite-plugin-svelte/src/utils/compile.js @@ -58,10 +58,6 @@ export function createCompileSvelte() { generate: ssr ? 'server' : 'client' }; - if (compileOptions.hmr && options.emitCss) { - const hash = `s-${safeBase64Hash(normalizedFilename)}`; - compileOptions.cssHash = () => hash; - } if (preprocessed) { if (preprocessed.dependencies?.length) { const checked = checkPreprocessDependencies(filename, preprocessed.dependencies); @@ -82,7 +78,18 @@ export function createCompileSvelte() { preprocessed: preprocessed ?? { code } }; } - const finalCode = code; + let finalCode = code; + if (compileOptions.hmr && options.emitCss) { + const hash = `s-${safeBase64Hash(normalizedFilename)}`; + compileOptions.cssHash = () => hash; + const closeStylePos = code.lastIndexOf(''); + if (closeStylePos > -1) { + // inject rule that forces compile to attach scope class to every node in the template + // this reduces the amount of js hot updates when editing css in .svelte files + finalCode = finalCode.slice(0, closeStylePos) + ' *{}' + finalCode.slice(closeStylePos); + } + } + const dynamicCompileOptions = await options?.dynamicCompileOptions?.({ filename, code: finalCode, From 04531a9c90cb5478c0e9c8539084b4a32339a3c5 Mon Sep 17 00:00:00 2001 From: dominikg Date: Tue, 24 Jun 2025 22:21:53 +0200 Subject: [PATCH 07/18] heureka --- .../vite-plugin-svelte/src/plugins/compile.js | 23 +-------------- .../vite-plugin-svelte/src/utils/compile.js | 29 +++++++++++++++---- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/packages/vite-plugin-svelte/src/plugins/compile.js b/packages/vite-plugin-svelte/src/plugins/compile.js index 20fa6c815..a18735322 100644 --- a/packages/vite-plugin-svelte/src/plugins/compile.js +++ b/packages/vite-plugin-svelte/src/plugins/compile.js @@ -36,29 +36,8 @@ export function compile(api) { const cache = api.getEnvironmentCache(this); let compileData; try { - /** - * @type {import("../types/options.js").ResolvedOptions} - */ - const finalOptions = svelteRequest.raw - ? { - ...options, - // don't use dynamic vite-plugin-svelte defaults here to ensure stable result between ssr,dev and build - compilerOptions: { - dev: false, - css: 'external', - hmr: false, - ...svelteRequest.query.compilerOptions - }, - emitCss: true - } - : options; const svelteMeta = this.getModuleInfo(id)?.meta?.svelte; - compileData = await compileSvelte( - svelteRequest, - code, - finalOptions, - svelteMeta?.preprocessed - ); + compileData = await compileSvelte(svelteRequest, code, options, svelteMeta?.preprocessed); } catch (e) { cache.setError(svelteRequest, e); throw toRollupError(e, options); diff --git a/packages/vite-plugin-svelte/src/utils/compile.js b/packages/vite-plugin-svelte/src/utils/compile.js index 2273c2e3c..224848582 100644 --- a/packages/vite-plugin-svelte/src/utils/compile.js +++ b/packages/vite-plugin-svelte/src/utils/compile.js @@ -51,12 +51,31 @@ export function createCompileSvelte() { // also they for hmr updates too } } + + /* + + compilerOptions: { + dev: false, + css: 'external', + hmr: false, + ...svelteRequest.query.compilerOptions + }, + */ /** @type {import('svelte/compiler').CompileOptions} */ - const compileOptions = { - ...options.compilerOptions, - filename, - generate: ssr ? 'server' : 'client' - }; + const compileOptions = svelteRequest.raw + ? { + dev: false, + generate: 'client', + css: 'external', + hmr: false, + filename, + ...svelteRequest.query.compilerOptions + } + : { + ...options.compilerOptions, + filename, + generate: ssr ? 'server' : 'client' + }; if (preprocessed) { if (preprocessed.dependencies?.length) { From 7150c4c4f1bf18290eb8a262aa009913da1cd45d Mon Sep 17 00:00:00 2001 From: dominikg Date: Wed, 25 Jun 2025 00:12:59 +0200 Subject: [PATCH 08/18] fixes for rolldown: remove duplicate optimizer setup and use svelte5 syntax to mount test app --- .../autoprefixer-browerslist/src/main.js | 8 +- .../vite-plugin-svelte/src/utils/options.js | 82 +------------------ 2 files changed, 3 insertions(+), 87 deletions(-) diff --git a/packages/e2e-tests/autoprefixer-browerslist/src/main.js b/packages/e2e-tests/autoprefixer-browerslist/src/main.js index 47818da0c..071c75dc7 100644 --- a/packages/e2e-tests/autoprefixer-browerslist/src/main.js +++ b/packages/e2e-tests/autoprefixer-browerslist/src/main.js @@ -1,7 +1,3 @@ import App from './App.svelte'; - -if (App.toString().startsWith('class ')) { - new App({ target: document.body }); -} else { - import('svelte').then(({ mount }) => mount(App, { target: document.body })); -} +import { mount } from 'svelte'; +mount(App, { target: document.body }); diff --git a/packages/vite-plugin-svelte/src/utils/options.js b/packages/vite-plugin-svelte/src/utils/options.js index ae00abf28..39a4a6bc0 100644 --- a/packages/vite-plugin-svelte/src/utils/options.js +++ b/packages/vite-plugin-svelte/src/utils/options.js @@ -5,9 +5,7 @@ const { defaultServerMainFields, defaultClientConditions, defaultServerConditions, - normalizePath, - //@ts-expect-error rolldownVersion not in type - rolldownVersion + normalizePath } = vite; import { log } from './log.js'; import { loadSvelteConfig } from './load-svelte-config.js'; @@ -20,12 +18,6 @@ import { } from './constants.js'; import path from 'node:path'; -import { - optimizeSvelteModulePluginName, - optimizeSveltePluginName, - patchESBuildOptimizerPlugin, - patchRolldownOptimizerPlugin -} from './optimizer-plugins.js'; import { addExtraPreprocessors } from './preprocess.js'; import deepmerge from 'deepmerge'; import { @@ -379,46 +371,6 @@ export async function buildExtraViteConfig(options, config) { ] }; - // handle prebundling for svelte files - if (options.prebundleSvelteLibraries) { - extraViteConfig.optimizeDeps = { - ...extraViteConfig.optimizeDeps, - // Experimental Vite API to allow these extensions to be scanned and prebundled - extensions: options.extensions ?? ['.svelte'] - }; - // Add optimizer plugins to prebundle Svelte files. - // Currently a placeholder as more information is needed after Vite config is resolved, - // the added plugins are patched in `patchResolvedViteConfig()` - if (rolldownVersion) { - /** - * - * @param {string} name - * @returns {import('vite').Rollup.Plugin} - */ - const placeholderRolldownOptimizerPlugin = (name) => ({ - name, - options() {}, - buildStart() {}, - buildEnd() {}, - transform: { filter: { id: /^$/ }, handler() {} } - }); - //@ts-expect-error rolldown types not finished - extraViteConfig.optimizeDeps.rollupOptions = { - plugins: [ - placeholderRolldownOptimizerPlugin(optimizeSveltePluginName), - placeholderRolldownOptimizerPlugin(optimizeSvelteModulePluginName) - ] - }; - } else { - extraViteConfig.optimizeDeps.esbuildOptions = { - plugins: [ - { name: optimizeSveltePluginName, setup: () => {} }, - { name: optimizeSvelteModulePluginName, setup: () => {} } - ] - }; - } - } - // enable hmrPartialAccept if not explicitly disabled if (config.experimental?.hmrPartialAccept !== false) { log.debug('enabling "experimental.hmrPartialAccept" in vite config', undefined, 'config'); @@ -602,38 +554,6 @@ function buildExtraConfigForSvelte(config) { return { optimizeDeps: { include, exclude }, ssr: { noExternal, external } }; } -/** - * @param {import('vite').ResolvedConfig} viteConfig - * @param {import('../types/options.d.ts').ResolvedOptions} options - */ -export function patchResolvedViteConfig(viteConfig, options) { - if (options.preprocess) { - for (const preprocessor of arraify(options.preprocess)) { - if (preprocessor.style && '__resolvedConfig' in preprocessor.style) { - preprocessor.style.__resolvedConfig = viteConfig; - } - } - } - if (rolldownVersion) { - const plugins = - // @ts-expect-error not typed - viteConfig.optimizeDeps.rollupOptions?.plugins?.filter((p) => - [optimizeSveltePluginName, optimizeSvelteModulePluginName].includes(p.name) - ) ?? []; - for (const plugin of plugins) { - patchRolldownOptimizerPlugin(plugin, options); - } - } else { - const plugins = - viteConfig.optimizeDeps.esbuildOptions?.plugins?.filter((p) => - [optimizeSveltePluginName, optimizeSvelteModulePluginName].includes(p.name) - ) ?? []; - for (const plugin of plugins) { - patchESBuildOptimizerPlugin(plugin, options); - } - } -} - /** * Mutates `config` to ensure `resolve.mainFields` is set. If unset, it emulates Vite's default fallback. * @param {string} name From 210ff7caea4aa7bf0d850ace288695e176e92d6a Mon Sep 17 00:00:00 2001 From: dominikg Date: Wed, 25 Jun 2025 00:38:29 +0200 Subject: [PATCH 09/18] fix: add back .svelte optimizer extension, remove unused file --- .../src/plugins/setup-optimizer.js | 5 +- .../src/utils/optimizer-plugins.js | 195 ------------------ 2 files changed, 4 insertions(+), 196 deletions(-) delete mode 100644 packages/vite-plugin-svelte/src/utils/optimizer-plugins.js diff --git a/packages/vite-plugin-svelte/src/plugins/setup-optimizer.js b/packages/vite-plugin-svelte/src/plugins/setup-optimizer.js index 64aac611a..3b1493997 100644 --- a/packages/vite-plugin-svelte/src/plugins/setup-optimizer.js +++ b/packages/vite-plugin-svelte/src/plugins/setup-optimizer.js @@ -34,7 +34,10 @@ export function setupOptimizer(api) { apply: 'serve', config() { /** @type {import('vite').UserConfig['optimizeDeps']} */ - const optimizeDeps = {}; + const optimizeDeps = { + // Experimental Vite API to allow these extensions to be scanned and prebundled + extensions: ['.svelte'] + }; // Add optimizer plugins to prebundle Svelte files. // Currently, a placeholder as more information is needed after Vite config is resolved, // the added plugins are patched in configResolved below diff --git a/packages/vite-plugin-svelte/src/utils/optimizer-plugins.js b/packages/vite-plugin-svelte/src/utils/optimizer-plugins.js deleted file mode 100644 index d5cd7a259..000000000 --- a/packages/vite-plugin-svelte/src/utils/optimizer-plugins.js +++ /dev/null @@ -1,195 +0,0 @@ -import { readFileSync } from 'node:fs'; -import * as svelte from 'svelte/compiler'; -import { log } from './log.js'; -import { toESBuildError, toRollupError } from './error.js'; -import { safeBase64Hash } from './hash.js'; -import { normalize } from './id.js'; - -/** - * @typedef {NonNullable} EsbuildOptions - * @typedef {NonNullable[number]} EsbuildPlugin - */ -/** - * @typedef {NonNullable} RollupPlugin - */ - -export const optimizeSveltePluginName = 'vite-plugin-svelte:optimize'; -export const optimizeSvelteModulePluginName = 'vite-plugin-svelte-module:optimize'; - -/** - * @param {EsbuildPlugin} plugin - * @param {import('../types/options.d.ts').ResolvedOptions} options - */ -export function patchESBuildOptimizerPlugin(plugin, options) { - const components = plugin.name === optimizeSveltePluginName; - const compileFn = components ? compileSvelte : compileSvelteModule; - const statsName = components ? 'prebundle library components' : 'prebundle library modules'; - const filter = components ? /\.svelte(?:\?.*)?$/ : /\.svelte\.[jt]s(?:\?.*)?$/; - plugin.setup = (build) => { - if (build.initialOptions.plugins?.some((v) => v.name === 'vite:dep-scan')) return; - - /** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */ - let statsCollection; - build.onStart(() => { - statsCollection = options.stats?.startCollection(statsName, { - logResult: (c) => c.stats.length > 1 - }); - }); - build.onLoad({ filter }, async ({ path: filename }) => { - const code = readFileSync(filename, 'utf8'); - try { - const result = await compileFn(options, { filename, code }, statsCollection); - const contents = result.map - ? result.code + '//# sourceMappingURL=' + result.map.toUrl() - : result.code; - return { contents }; - } catch (e) { - return { errors: [toESBuildError(e, options)] }; - } - }); - build.onEnd(() => { - statsCollection?.finish(); - }); - }; -} - -/** - * @param {RollupPlugin} plugin - * @param {import('../types/options.d.ts').ResolvedOptions} options - */ -export function patchRolldownOptimizerPlugin(plugin, options) { - const components = plugin.name === optimizeSveltePluginName; - const compileFn = components ? compileSvelte : compileSvelteModule; - const statsName = components ? 'prebundle library components' : 'prebundle library modules'; - const includeRe = components ? /^[^?#]+\.svelte(?:[?#]|$)/ : /^[^?#]+\.svelte\.[jt]s(?:[?#]|$)/; - /** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */ - let statsCollection; - - plugin.options = (opts) => { - // @ts-expect-error plugins is an array here - const isScanner = opts.plugins.some( - (/** @type {{ name: string; }} */ p) => p.name === 'vite:dep-scan:resolve' - ); - if (isScanner) { - delete plugin.buildStart; - delete plugin.transform; - delete plugin.buildEnd; - } else { - plugin.transform = { - filter: { id: includeRe }, - /** - * @param {string} code - * @param {string} filename - */ - async handler(code, filename) { - try { - return await compileFn(options, { filename, code }, statsCollection); - } catch (e) { - throw toRollupError(e, options); - } - } - }; - plugin.buildStart = () => { - statsCollection = options.stats?.startCollection(statsName, { - logResult: (c) => c.stats.length > 1 - }); - }; - plugin.buildEnd = () => { - statsCollection?.finish(); - }; - } - }; -} - -/** - * @param {import('../types/options.d.ts').ResolvedOptions} options - * @param {{ filename: string, code: string }} input - * @param {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection} [statsCollection] - * @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, - filename, - generate: 'client' - }; - - if (compileOptions.hmr && options.emitCss) { - const hash = `s-${safeBase64Hash(normalize(filename, options.root))}`; - compileOptions.cssHash = () => hash; - } - - let preprocessed; - - if (options.preprocess) { - try { - preprocessed = await svelte.preprocess(code, options.preprocess, { filename }); - } catch (e) { - e.message = `Error while preprocessing ${filename}${e.message ? ` - ${e.message}` : ''}`; - throw e; - } - if (preprocessed.map) compileOptions.sourcemap = preprocessed.map; - } - - const finalCode = preprocessed ? preprocessed.code : code; - - const dynamicCompileOptions = await options?.dynamicCompileOptions?.({ - filename, - code: finalCode, - compileOptions - }); - - if (dynamicCompileOptions && log.debug.enabled) { - log.debug( - `dynamic compile options for ${filename}: ${JSON.stringify(dynamicCompileOptions)}`, - undefined, - 'compile' - ); - } - - const finalCompileOptions = dynamicCompileOptions - ? { - ...compileOptions, - ...dynamicCompileOptions - } - : compileOptions; - const endStat = statsCollection?.start(filename); - const compiled = svelte.compile(finalCode, finalCompileOptions); - if (endStat) { - endStat(); - } - return { - ...compiled.js, - moduleType: 'js' - }; -} - -/** - * @param {import('../types/options.d.ts').ResolvedOptions} options - * @param {{ filename: string; code: string }} input - * @param {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection} [statsCollection] - * @returns {Promise} - */ -async function compileSvelteModule(options, { filename, code }, statsCollection) { - const endStat = statsCollection?.start(filename); - const compiled = svelte.compileModule(code, { - dev: options.compilerOptions?.dev ?? true, // default to dev: true because prebundling is only used in dev - filename, - generate: 'client' - }); - if (endStat) { - endStat(); - } - return { - ...compiled.js, - moduleType: 'js' - }; -} From 40b8dab26a9933ba3a119de63577b0e859a3980a Mon Sep 17 00:00:00 2001 From: dominikg Date: Wed, 25 Jun 2025 00:49:41 +0200 Subject: [PATCH 10/18] chore: remove unused files & code --- .../src/handle-hot-update.js | 145 ------------------ .../src/utils/dependencies.js | 29 ---- .../vite-plugin-svelte/src/utils/load-raw.js | 125 --------------- .../src/utils/preprocess.js | 27 ---- 4 files changed, 326 deletions(-) delete mode 100644 packages/vite-plugin-svelte/src/handle-hot-update.js delete mode 100644 packages/vite-plugin-svelte/src/utils/load-raw.js diff --git a/packages/vite-plugin-svelte/src/handle-hot-update.js b/packages/vite-plugin-svelte/src/handle-hot-update.js deleted file mode 100644 index 82758e55a..000000000 --- a/packages/vite-plugin-svelte/src/handle-hot-update.js +++ /dev/null @@ -1,145 +0,0 @@ -import { log, logCompilerWarnings } from './utils/log.js'; -import { toRollupError } from './utils/error.js'; - -/** - * Vite-specific HMR handling - * - * @param {Function} compileSvelte - * @param {import('vite').HmrContext} ctx - * @param {import('./types/id.d.ts').SvelteRequest} svelteRequest - * @param {import('./utils/vite-plugin-svelte-cache.js').VitePluginSvelteCache} cache - * @param {import('./types/options.d.ts').ResolvedOptions} options - * @returns {Promise} - */ -export async function handleHotUpdate(compileSvelte, ctx, svelteRequest, cache, options) { - if (!cache.has(svelteRequest)) { - // file hasn't been requested yet (e.g. async component) - log.debug( - `handleHotUpdate called before initial transform for ${svelteRequest.id}`, - undefined, - 'hmr' - ); - return; - } - const { read, server, modules } = ctx; - - const cachedJS = cache.getJS(svelteRequest); - const cachedCss = cache.getCSS(svelteRequest); - - const content = await read(); - /** @type {import('./types/compile.d.ts').CompileData} */ - let compileData; - try { - compileData = await compileSvelte(svelteRequest, content, options); - cache.update(compileData); - } catch (e) { - cache.setError(svelteRequest, e); - throw toRollupError(e, options); - } - - const affectedModules = [...modules]; - - const cssIdx = modules.findIndex((m) => m.id === svelteRequest.cssId); - if (cssIdx > -1) { - const cssUpdated = cssChanged(cachedCss, compileData.compiled.css); - if (!cssUpdated) { - log.debug(`skipping unchanged css for ${svelteRequest.cssId}`, undefined, 'hmr'); - affectedModules.splice(cssIdx, 1); - } - } - const jsIdx = modules.findIndex((m) => m.id === svelteRequest.id); - if (jsIdx > -1) { - const jsUpdated = jsChanged(cachedJS, compileData.compiled.js, svelteRequest.filename); - if (!jsUpdated) { - log.debug(`skipping unchanged js for ${svelteRequest.id}`, undefined, 'hmr'); - affectedModules.splice(jsIdx, 1); - // transform won't be called, log warnings here - logCompilerWarnings(svelteRequest, compileData.compiled.warnings, options); - } - } - - // TODO is this enough? see also: https://github.com/vitejs/vite/issues/2274 - const ssrModulesToInvalidate = affectedModules.filter((m) => !!m.ssrTransformResult); - if (ssrModulesToInvalidate.length > 0) { - log.debug( - `invalidating modules ${ssrModulesToInvalidate.map((m) => m.id).join(', ')}`, - undefined, - 'hmr' - ); - ssrModulesToInvalidate.forEach((moduleNode) => server.moduleGraph.invalidateModule(moduleNode)); - } - if (affectedModules.length > 0) { - log.debug( - `handleHotUpdate for ${svelteRequest.id} result: ${affectedModules - .map((m) => m.id) - .join(', ')}`, - undefined, - 'hmr' - ); - } - return affectedModules; -} - -/** - * @param {import('./types/compile.d.ts').Code | null} [prev] - * @param {import('./types/compile.d.ts').Code | null} [next] - * @returns {boolean} - */ -function cssChanged(prev, next) { - return !isCodeEqual(prev?.code, next?.code); -} - -/** - * @param {import('./types/compile.d.ts').Code | null} [prev] - * @param {import('./types/compile.d.ts').Code | null} [next] - * @param {string} [filename] - * @returns {boolean} - */ -function jsChanged(prev, next, filename) { - const prevJs = prev?.code; - const nextJs = next?.code; - const isStrictEqual = isCodeEqual(prevJs, nextJs); - if (isStrictEqual) { - return false; - } - const isLooseEqual = isCodeEqual(normalizeJsCode(prevJs), normalizeJsCode(nextJs)); - if (!isStrictEqual && isLooseEqual) { - log.debug( - `ignoring compiler output js change for ${filename} as it is equal to previous output after normalization`, - undefined, - 'hmr' - ); - } - return !isLooseEqual; -} - -/** - * @param {string} [prev] - * @param {string} [next] - * @returns {boolean} - */ -function isCodeEqual(prev, next) { - if (!prev && !next) { - return true; - } - if ((!prev && next) || (prev && !next)) { - return false; - } - return prev === next; -} - -/** - * remove code that only changes metadata and does not require a js update for the component to keep working - * - * 1) add_location() calls. These add location metadata to elements, only used by some dev tools - * 2) ... maybe more (or less) in the future - * - * @param {string} [code] - * @returns {string | undefined} - */ -function normalizeJsCode(code) { - if (!code) { - return code; - } - return code.replace(/\s*\badd_location\s*\([^)]*\)\s*;?/g, ''); -} diff --git a/packages/vite-plugin-svelte/src/utils/dependencies.js b/packages/vite-plugin-svelte/src/utils/dependencies.js index 30caf5901..341bf3cd0 100644 --- a/packages/vite-plugin-svelte/src/utils/dependencies.js +++ b/packages/vite-plugin-svelte/src/utils/dependencies.js @@ -1,32 +1,3 @@ -import path from 'node:path'; -import fs from 'node:fs/promises'; -import { findDepPkgJsonPath } from 'vitefu'; - -/** - * @typedef {{ - * dir: string; - * pkg: Record; - * }} DependencyData - */ - -/** - * @param {string} dep - * @param {string} parent - * @returns {Promise} - */ -export async function resolveDependencyData(dep, parent) { - const depDataPath = await findDepPkgJsonPath(dep, parent); - if (!depDataPath) return undefined; - try { - return { - dir: path.dirname(depDataPath), - pkg: JSON.parse(await fs.readFile(depDataPath, 'utf-8')) - }; - } catch { - return undefined; - } -} - const COMMON_DEPENDENCIES_WITHOUT_SVELTE_FIELD = [ '@lukeed/uuid', '@playwright/test', diff --git a/packages/vite-plugin-svelte/src/utils/load-raw.js b/packages/vite-plugin-svelte/src/utils/load-raw.js deleted file mode 100644 index f800f1b47..000000000 --- a/packages/vite-plugin-svelte/src/utils/load-raw.js +++ /dev/null @@ -1,125 +0,0 @@ -import fs from 'node:fs'; -import { toRollupError } from './error.js'; -import { log } from './log.js'; - -/** - * utility function to compile ?raw and ?direct requests in load hook - * - * @param {import('../types/id.d.ts').SvelteRequest} svelteRequest - * @param {import('../types/compile.d.ts').CompileSvelte} compileSvelte - * @param {import('../types/options.d.ts').ResolvedOptions} options - * @returns {Promise} - */ -export async function loadRaw(svelteRequest, compileSvelte, options) { - const { id, filename, query } = svelteRequest; - - // raw svelte subrequest, compile on the fly and return requested subpart - let compileData; - const source = fs.readFileSync(filename, 'utf-8'); - try { - //avoid compileSvelte doing extra ssr stuff unless requested - svelteRequest.ssr = query.compilerOptions?.generate === 'server'; - compileData = await compileSvelte(svelteRequest, source, { - ...options, - // don't use dynamic vite-plugin-svelte defaults here to ensure stable result between ssr,dev and build - compilerOptions: { - dev: false, - css: 'external', - hmr: false, - ...svelteRequest.query.compilerOptions - }, - emitCss: true - }); - } catch (e) { - throw toRollupError(e, options); - } - let result; - if (query.type === 'style') { - result = compileData.compiled.css ?? { code: '', map: null }; - } else if (query.type === 'script') { - result = compileData.compiled.js; - } else if (query.type === 'preprocessed') { - result = compileData.preprocessed; - } else if (query.type === 'all' && query.raw) { - return allToRawExports(compileData, source); - } else { - throw new Error( - `invalid "type=${query.type}" in ${id}. supported are script, style, preprocessed, all` - ); - } - if (query.direct) { - const supportedDirectTypes = ['script', 'style']; - if (!supportedDirectTypes.includes(query.type)) { - throw new Error( - `invalid "type=${ - query.type - }" combined with direct in ${id}. supported are: ${supportedDirectTypes.join(', ')}` - ); - } - log.debug(`load returns direct result for ${id}`, undefined, 'load'); - let directOutput = result.code; - // @ts-expect-error might not be SourceMap but toUrl check should suffice - if (query.sourcemap && result.map?.toUrl) { - // @ts-expect-error toUrl might not exist - const map = `sourceMappingURL=${result.map.toUrl()}`; - if (query.type === 'style') { - directOutput += `\n\n/*# ${map} */\n`; - } else if (query.type === 'script') { - directOutput += `\n\n//# ${map}\n`; - } - } - return directOutput; - } else if (query.raw) { - log.debug(`load returns raw result for ${id}`, undefined, 'load'); - return toRawExports(result); - } else { - throw new Error(`invalid raw mode in ${id}, supported are raw, direct`); - } -} - -/** - * turn compileData and source into a flat list of raw exports - * - * @param {import('../types/compile.d.ts').CompileData} compileData - * @param {string} source - */ -function allToRawExports(compileData, source) { - // flatten CompileData - /** @type {Partial} */ - const exports = { - ...compileData, - ...compileData.compiled, - source - }; - delete exports.compiled; - delete exports.filename; // absolute path, remove to avoid it in output - return toRawExports(exports); -} - -/** - * turn object into raw exports. - * - * every prop is returned as a const export, and if prop 'code' exists it is additionally added as default export - * - * eg {'foo':'bar','code':'baz'} results in - * - * ```js - * export const code='baz' - * export const foo='bar' - * export default code - * ``` - * @param {object} object - * @returns {string} - */ -function toRawExports(object) { - let exports = - Object.entries(object) - .filter(([_key, value]) => typeof value !== 'function') // preprocess output has a toString function that's enumerable - .sort(([a], [b]) => (a < b ? -1 : a === b ? 0 : 1)) - .map(([key, value]) => `export const ${key}=${JSON.stringify(value)}`) - .join('\n') + '\n'; - if (Object.prototype.hasOwnProperty.call(object, 'code')) { - exports += 'export default code\n'; - } - return exports; -} 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 diff --git a/packages/e2e-tests/import-queries/__tests__/__snapshots__/svelte-5/ssr-preprocessed.txt b/packages/e2e-tests/import-queries/__tests__/__snapshots__/svelte-5/ssr-preprocessed.txt deleted file mode 100644 index a6320b265..000000000 --- a/packages/e2e-tests/import-queries/__tests__/__snapshots__/svelte-5/ssr-preprocessed.txt +++ /dev/null @@ -1,14 +0,0 @@ - - - - - diff --git a/packages/e2e-tests/import-queries/__tests__/__snapshots__/svelte-5/style.txt b/packages/e2e-tests/import-queries/__tests__/__snapshots__/svelte-5/style.txt deleted file mode 100644 index 2e590fb1d..000000000 --- a/packages/e2e-tests/import-queries/__tests__/__snapshots__/svelte-5/style.txt +++ /dev/null @@ -1,3 +0,0 @@ -button.svelte-d8vj6a { - color: #000099; -} \ No newline at end of file diff --git a/packages/e2e-tests/import-queries/__tests__/import-queries.spec.ts b/packages/e2e-tests/import-queries/__tests__/import-queries.spec.ts index 74c680808..3ee27dadc 100644 --- a/packages/e2e-tests/import-queries/__tests__/import-queries.spec.ts +++ b/packages/e2e-tests/import-queries/__tests__/import-queries.spec.ts @@ -1,4 +1,4 @@ -import { browserLogs, fetchFromPage, getText, isBuild, testDir } from '~utils'; +import { browserLogs, getText, isBuild, testDir } from '~utils'; import { createServer, ViteDevServer } from 'vite'; import { VERSION } from 'svelte/compiler'; @@ -22,114 +22,6 @@ describe('raw', () => { const result = await getText('#raw'); await expect(result).toMatchFileSnapshot(snapshotFilename('raw')); }); - - test('Dummy.svelte?raw&svelte&type=preprocessed', async () => { - const result = await getText('#preprocessed'); - await expect(result).toMatchFileSnapshot(snapshotFilename('preprocessed')); - }); - - test('Dummy.svelte?raw&svelte&type=script', async () => { - const result = await getText('#script'); - expect(result).toContain('export default function Dummy'); - }); - - test('Dummy.svelte?raw&svelte&type=script&compilerOptions={"customElement":true}', async () => { - const result = await getText('#wcScript'); - expect(result).toContain('$.create_custom_element(Dummy,'); - }); - - test('Dummy.svelte?raw&svelte&type=style', async () => { - const result = await getText('#style'); - await expect(result).toMatchFileSnapshot(snapshotFilename('style')); - }); - - test('Dummy.svelte?raw&svelte&type=all&sourcemap', async () => { - const result = JSON.parse(await getText('#all')); - expect(result.ast).toBeDefined(); - expect(result.js).toBeDefined(); - expect(result.js.code).toBeDefined(); - expect(result.js.map).toBeDefined(); - expect(result.css).toBeDefined(); - expect(result.css.code).toBeDefined(); - expect(result.css.map).toBeDefined(); - expect(result.preprocessed).toBeDefined(); - expect(result.preprocessed.code).toBeDefined(); - expect(result.preprocessed.map).toBeDefined(); - }); - - describe.runIf(!isBuild)('mixed exports', () => { - test('Dummy.svelte?raw&svelte&type=preprocessed', async () => { - const module = await fetchFromPage('src/Dummy.svelte?raw&svelte&type=preprocessed').then( - (res) => res.text() - ); - expect(module).toContain('export const code="