This commit is contained in:
2026-01-22 17:13:23 -06:00
parent 02b0f053d6
commit f15f7b8e38
1728 changed files with 925278 additions and 1 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 [these people](https://github.com/sveltejs/vite-plugin-svelte/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+28
View File
@@ -0,0 +1,28 @@
# @sveltejs/vite-plugin-svelte
The official [Svelte](https://svelte.dev) plugin for [Vite](https://vitejs.dev).
## Usage
```js
// vite.config.js
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [
svelte({
/* plugin options */
})
]
});
```
## Documentation
- [Plugin options](../../docs/config.md)
- [FAQ](../../docs/faq.md)
## License
[MIT](./LICENSE)
+58
View File
@@ -0,0 +1,58 @@
{
"name": "@sveltejs/vite-plugin-svelte",
"version": "6.2.4",
"license": "MIT",
"author": "dominikg",
"files": [
"src",
"types"
],
"type": "module",
"types": "types/index.d.ts",
"exports": {
".": {
"types": "./types/index.d.ts",
"default": "./src/index.js"
}
},
"engines": {
"node": "^20.19 || ^22.12 || >=24"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sveltejs/vite-plugin-svelte.git",
"directory": "packages/vite-plugin-svelte"
},
"keywords": [
"vite-plugin",
"vite plugin",
"vite",
"svelte"
],
"bugs": {
"url": "https://github.com/sveltejs/vite-plugin-svelte/issues"
},
"homepage": "https://github.com/sveltejs/vite-plugin-svelte#readme",
"dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
"deepmerge": "^4.3.1",
"magic-string": "^0.30.21",
"obug": "^2.1.0",
"vitefu": "^1.1.1"
},
"peerDependencies": {
"svelte": "^5.0.0",
"vite": "^6.3.0 || ^7.0.0"
},
"devDependencies": {
"@types/debug": "^4.1.12",
"sass": "^1.94.2",
"svelte": "^5.45.4",
"vite": "^7.2.6"
},
"scripts": {
"check:publint": "publint --strict",
"check:types": "tsc --noEmit",
"generate:types": "dts-buddy -m \"@sveltejs/vite-plugin-svelte:src/public.d.ts\""
}
}
+42
View File
@@ -0,0 +1,42 @@
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 { 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:<task>`
*
* @param {Partial<import('./public.d.ts').Options>} [inlineOptions]
* @returns {import('vite').Plugin[]}
*/
export function svelte(inlineOptions) {
if (process.env.DEBUG != null) {
log.setLevel('debug');
}
/** @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 [
{ name: 'vite-plugin-svelte' }, // marker for detection logic in other plugins that expect this name
configure(api, inlineOptions),
setupOptimizer(api),
loadCompiledCss(api),
loadCustom(api),
preprocess(api),
compile(api),
compileModule(api),
hotUpdate(api),
svelteInspector()
];
}
export { vitePreprocess } from './preprocess.js';
export { loadSvelteConfig } from './utils/load-svelte-config.js';
+130
View File
@@ -0,0 +1,130 @@
import { buildModuleIdFilter, buildModuleIdParser } from '../utils/id.js';
import * as svelteCompiler from 'svelte/compiler';
import { log, logCompilerWarnings } from '../utils/log.js';
import { toRollupError } from '../utils/error.js';
import { isSvelteWithAsync } from '../utils/svelte-version.js';
/**
* @param {import('../types/plugin-api.d.ts').PluginAPI} api
* @returns {import('vite').Plugin}
*/
export function compileModule(api) {
/**
* @type {import("../types/options.js").ResolvedOptions}
*/
let options;
/**
* @type {import("../types/id.js").ModuleIdParser}
*/
let idParser;
/**
* @type {import('svelte/compiler').ModuleCompileOptions}
*/
let staticModuleCompileOptions;
/** @type {import('vite').Plugin} */
const plugin = {
name: 'vite-plugin-svelte:compile-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);
staticModuleCompileOptions = filterNonModuleCompilerOptions(options.compilerOptions);
},
transform: {
async handler(code, id) {
const ssr = this.environment.config.consumer === 'server';
const moduleRequest = idParser(id, ssr);
if (!moduleRequest) {
return;
}
const filename = moduleRequest.filename;
/** @type {import('svelte/compiler').CompileOptions} */
const compileOptions = {
...staticModuleCompileOptions,
dev: !this.environment.config.isProduction,
generate: ssr ? 'server' : 'client',
filename
};
const dynamicCompileOptions = await options?.dynamicCompileOptions?.({
filename,
code,
compileOptions
});
if (dynamicCompileOptions && log.debug.enabled) {
log.debug(
`dynamic compile options for ${filename}: ${JSON.stringify(dynamicCompileOptions)}`,
undefined,
'compileModule'
);
}
const finalCompileOptions = dynamicCompileOptions
? {
...compileOptions,
...dynamicCompileOptions
}
: compileOptions;
if (dynamicCompileOptions?.experimental) {
finalCompileOptions.experimental = {
...compileOptions.experimental,
...dynamicCompileOptions.experimental
};
}
const finalModuleCompileOptions = filterNonModuleCompilerOptions(finalCompileOptions);
if (log.debug.enabled) {
log.debug(
`final ModuleCompileOptions for ${filename}: ${JSON.stringify(finalModuleCompileOptions)}`,
undefined,
'compileModule'
);
}
try {
const compileResult = svelteCompiler.compileModule(code, finalModuleCompileOptions);
logCompilerWarnings(moduleRequest, compileResult.warnings, options);
return compileResult.js;
} catch (e) {
throw toRollupError(e, options);
}
}
}
};
return plugin;
}
/**
*
* @param {import('svelte/compiler').CompileOptions} compilerOptions
* @return {import('svelte/compiler').ModuleCompileOptions}
*/
function filterNonModuleCompilerOptions(compilerOptions) {
/** @type {Array<keyof import('svelte/compiler').ModuleCompileOptions>} */
const knownModuleCompileOptionNames = ['dev', 'generate', 'filename', 'rootDir', 'warningFilter'];
if (isSvelteWithAsync) {
knownModuleCompileOptionNames.push('experimental');
}
// not typed but this is temporary until svelte itself ignores CompileOptions passed to compileModule
const experimentalModuleCompilerOptionNames = ['async'];
/** @type {import('svelte/compiler').ModuleCompileOptions} */
const filtered = filterByPropNames(compilerOptions, knownModuleCompileOptionNames);
if (filtered.experimental) {
filtered.experimental = filterByPropNames(
filtered.experimental,
experimentalModuleCompilerOptionNames
);
}
return filtered;
}
/**
*
* @param {object} o
* @param {string[]} names
* @returns {object}
*/
function filterByPropNames(o, names) {
return Object.fromEntries(Object.entries(o).filter(([name]) => names.includes(name)));
}
+65
View File
@@ -0,0 +1,65 @@
import { toRollupError } from '../utils/error.js';
import { logCompilerWarnings } from '../utils/log.js';
/**
* @param {import('../types/plugin-api.d.ts').PluginAPI} api
* @returns {import('vite').Plugin}
*/
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.filter;
options = api.options;
compileSvelte = api.compileSvelte;
},
transform: {
async handler(code, id) {
const ssr = this.environment.config.consumer === 'server';
const svelteRequest = api.idParser(id, ssr);
if (!svelteRequest || svelteRequest.raw) {
return;
}
let compileData;
try {
compileData = await compileSvelte(
svelteRequest,
code,
options,
this.getCombinedSourcemap()
);
} catch (e) {
throw toRollupError(e, options);
}
if (compileData.compiled?.warnings) {
logCompilerWarnings(svelteRequest, compileData.compiled.warnings, options);
}
return {
...compileData.compiled.js,
moduleType: 'js',
meta: {
vite: {
lang: compileData.lang
},
svelte: {
css: compileData.compiled.css
}
}
};
}
}
};
return plugin;
}
+97
View File
@@ -0,0 +1,97 @@
import process from 'node:process';
import { isDebugNamespaceEnabled, log } from '../utils/log.js';
import * as vite from 'vite';
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-ignore rolldownVersion
const { version: viteVersion, rolldownVersion } = vite;
/**
* @param {Partial<import('../public.d.ts').Options>} [inlineOptions]
* @param {import('../types/plugin-api.d.ts').PluginAPI} api
* @returns {import('vite').Plugin}
*/
export function configure(api, inlineOptions) {
if (rolldownVersion) {
log.warn.once(
`!!! Support for vite 8 beta in vite-plugin-svelte is experimental (rolldown: ${rolldownVersion}, vite: ${viteVersion}) !!!
See https://github.com/sveltejs/vite-plugin-svelte/issues/1143 for a list of known issues and to report feedback.`.replace(
/\t+/g,
'\t'
)
);
}
validateInlineOptions(inlineOptions);
/**
* @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: {
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;
if (isDebugNamespaceEnabled('stats')) {
api.options.stats = new VitePluginSvelteStats();
}
api.filter = buildIdFilter(options);
api.idFilter = api.filter;
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;
}
};
}
+196
View File
@@ -0,0 +1,196 @@
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<string|null,string>}
*/
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.filter.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.filter.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);
} 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);
}
},
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);
}
}
if (svelteModules.length === 0) {
return; // nothing to do for us
}
const affectedModules = [];
const prevResults = svelteModules.map((m) => transformResultCache.get(m.id));
/** @type {Set<string>} */
const seen = new Set();
/** @param {string} url */
const transformRequest = async (url) => {
if (!seen.has(url)) {
seen.add(url);
await this.environment.transformRequest(url);
}
};
// Transform the Svelte component itself first, so that the
// CSS cache always gets updated.
await transformRequest(svelteRequest.filename);
for (let i = 0; i < svelteModules.length; i++) {
const mod = svelteModules[i];
const prev = prevResults[i];
await 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];
}
}
}
};
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) {
return false;
}
const isLooseEqual = nullSafeEqual(normalize(prev), normalize(next));
if (!isStrictEqual && isLooseEqual) {
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) location numbers argument from $.add_locations calls in svelte output eg [[1,2],[3,4]]
* 2) timestamp queries added to imports by vite eg ?t=0123456789123
*
* @param {string | null | undefined } code
* @returns {string | null | undefined}
*/
function normalize(code) {
if (code == null) {
return code;
}
return (
code
// svelte5 $.add_locations line numbers argument [[1,2],[3,4]]
// uses matching group replace to keep the template argument intact
.replace(/(\$\.add_locations\(.*), (\[\[[\d, [\]]+]])\)/g, '$1, []')
// vite import analysis timestamp queries, ?t=0123456789123&
.replace(/[?&]t=\d{13}\b/g, '')
);
}
@@ -0,0 +1,66 @@
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(api) {
let useLocalCache = false;
/** @type{Map<string,any>} */
const buildWatchCssCache = new Map();
return {
name: 'vite-plugin-svelte:load-compiled-css',
configResolved(c) {
const isDev = c.command === 'serve';
const isBuildWatch = !!c.build?.watch;
useLocalCache = isDev || isBuildWatch;
},
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 ssr = this.environment.config.consumer === 'server';
const svelteRequest = api.idParser(id, ssr);
if (!svelteRequest) {
return;
}
let cachedCss = this.getModuleInfo(svelteRequest.filename)?.meta.svelte?.css;
// in `build --watch` or dev ssr reloads getModuleInfo only returns changed module data.
// To ensure virtual css is loaded unchanged, we cache it here separately
if (useLocalCache) {
if (cachedCss) {
buildWatchCssCache.set(svelteRequest.filename, cachedCss);
} else {
cachedCss = buildWatchCssCache.get(svelteRequest.filename);
}
}
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;
} else {
log.warn(`failed to load virtual css module ${id}`, undefined, 'load');
}
}
}
};
}
+42
View File
@@ -0,0 +1,42 @@
import fs from 'node:fs';
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}
*/
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.filter;
},
load: {
//filter: is set in configResolved
async handler(id) {
const config = this.environment.config;
const ssr = config.consumer === 'server';
const svelteRequest = api.idParser(id, ssr);
if (svelteRequest) {
const { filename, query } = svelteRequest;
if (!query.url && 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');
}
}
}
}
};
return plugin;
}
+208
View File
@@ -0,0 +1,208 @@
import { toRollupError } from '../utils/error.js';
import { mapToRelative } from '../utils/sourcemaps.js';
import * as svelte from 'svelte/compiler';
import { log } from '../utils/log.js';
import { arraify } from '../utils/options.js';
import fs from 'node:fs';
import path from 'node:path';
/**
* @param {import('../types/plugin-api.d.ts').PluginAPI} api
* @returns {import('vite').Plugin}
*/
export function preprocess(api) {
/**
* @type {import("../types/options.js").ResolvedOptions}
*/
let options;
/**
* @type {DependenciesCache}
*/
let dependenciesCache;
/**
* @type {import("../types/compile.d.ts").PreprocessSvelte}
*/
let preprocessSvelte;
/** @type {import('vite').Plugin} */
const plugin = {
name: 'vite-plugin-svelte:preprocess',
enforce: 'pre',
configResolved(c) {
options = api.options;
if (arraify(options.preprocess).length > 0) {
preprocessSvelte = createPreprocessSvelte(options, c);
// @ts-expect-error defined below but filter not in type
plugin.transform.filter = api.filter;
} else {
log.debug(
`disabling ${plugin.name} because no preprocessor is configured`,
undefined,
'preprocess'
);
// @ts-expect-error force set undefined to clear memory
preprocessSvelte = undefined;
// @ts-expect-error defined below but filter not in type
plugin.transform.filter = { id: /$./ }; // never match
}
},
configureServer(server) {
dependenciesCache = new DependenciesCache(server);
},
buildStart() {
dependenciesCache?.clear();
},
transform: {
async handler(code, id) {
const ssr = this.environment.config.consumer === 'server';
const svelteRequest = api.idParser(id, ssr);
if (!svelteRequest) {
return;
}
try {
const preprocessed = await preprocessSvelte(svelteRequest, code, options);
dependenciesCache?.update(svelteRequest, preprocessed?.dependencies ?? []);
if (!preprocessed) {
return;
}
if (options.isBuild && this.environment.config.build.watch && preprocessed.dependencies) {
for (const dep of preprocessed.dependencies) {
this.addWatchFile(dep);
}
}
/** @type {import('vite').Rollup.SourceDescription}*/
const result = { code: preprocessed.code };
if (preprocessed.map) {
// @ts-expect-error type differs but should work
result.map = preprocessed.map;
}
return result;
} catch (e) {
throw toRollupError(e, options);
}
}
}
};
return plugin;
}
/**
* @param {import("../types/options.js").ResolvedOptions} options
* @param {import("vite").ResolvedConfig} resolvedConfig
* @returns {import('../types/compile.d.ts').PreprocessSvelte}
*/
function createPreprocessSvelte(options, resolvedConfig) {
/** @type {Array<import('svelte/compiler').PreprocessorGroup>} */
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) {
const { filename } = svelteRequest;
let preprocessed;
if (preprocessors && preprocessors.length > 0) {
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 (typeof preprocessed?.map === 'object') {
mapToRelative(preprocessed?.map, filename);
}
return preprocessed;
}
};
}
/**
* @class
*
* caches dependencies of preprocessed files and emit change events on dependants
*/
class DependenciesCache {
/** @type {Map<string, string[]>} */
#dependencies = new Map();
/** @type {Map<string, Set<string>>} */
#dependants = new Map();
/** @type {import('vite').ViteDevServer} */
#server;
/**
*
* @param {import('vite').ViteDevServer} server
*/
constructor(server) {
this.#server = server;
/** @type {(filename: string) => void} */
const emitChangeEventOnDependants = (filename) => {
const dependants = this.#dependants.get(filename);
dependants?.forEach((dependant) => {
if (fs.existsSync(dependant)) {
log.debug(
`emitting virtual change event for "${dependant}" because dependency "${filename}" changed`,
undefined,
'hmr'
);
server.watcher.emit('change', dependant);
}
});
};
server.watcher.on('change', emitChangeEventOnDependants);
server.watcher.on('unlink', emitChangeEventOnDependants);
}
/**
* @param {string} file
*/
#ensureWatchedFile(file) {
const root = this.#server.config.root;
if (
file &&
// only need to watch if out of root
!file.startsWith(root + '/') &&
// some rollup plugins use null bytes for private resolved Ids
!file.includes('\0') &&
fs.existsSync(file)
) {
// resolve file to normalized system path
this.#server.watcher.add(path.resolve(file));
}
}
clear() {
this.#dependencies.clear();
this.#dependants.clear();
}
/**
*
* @param {import('../types/id.d.ts').SvelteRequest} svelteRequest
* @param {string[]} dependencies
*/
update(svelteRequest, dependencies) {
const id = svelteRequest.normalizedFilename;
const prevDependencies = this.#dependencies.get(id) || [];
this.#dependencies.set(id, dependencies);
const removed = prevDependencies.filter((d) => !dependencies.includes(d));
const added = dependencies.filter((d) => !prevDependencies.includes(d));
added.forEach((d) => {
this.#ensureWatchedFile(d);
if (!this.#dependants.has(d)) {
this.#dependants.set(d, new Set());
}
/** @type {Set<string>} */ (this.#dependants.get(d)).add(svelteRequest.filename);
});
removed.forEach((d) => {
/** @type {Set<string>} */ (this.#dependants.get(d)).delete(svelteRequest.filename);
});
}
}
@@ -0,0 +1,340 @@
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-ignore not typed on vite
const { rolldownVersion } = vite;
/**
* @typedef {NonNullable<import('vite').DepOptimizationOptions['esbuildOptions']>} EsbuildOptions
* @typedef {NonNullable<EsbuildOptions['plugins']>[number]} EsbuildPlugin
*/
/**
* @typedef {NonNullable<import('vite').Rollup.Plugin>} 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(api) {
/** @type {import('vite').ResolvedConfig} */
let viteConfig;
return {
name: 'vite-plugin-svelte:setup-optimizer',
apply: 'serve',
config() {
/** @type {import('vite').UserConfig['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
if (rolldownVersion) {
//@ts-ignore rolldown types not finished
optimizeDeps.rolldownOptions = {
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.rolldownOptions?.plugins?.filter((p) =>
[optimizeSveltePluginName, optimizeSvelteModulePluginName].includes(p.name)
) ?? [];
for (const plugin of plugins) {
patchRolldownOptimizerPlugin(plugin, api.options);
}
} else {
const plugins =
optimizeDeps.esbuildOptions?.plugins?.filter((p) =>
[optimizeSveltePluginName, optimizeSvelteModulePluginName].includes(p.name)
) ?? [];
for (const plugin of plugins) {
patchESBuildOptimizerPlugin(plugin, api.options);
}
}
},
async buildStart() {
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
*/
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<import('../types/compile.d.ts').Code>}
*/
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<import('../types/compile.d.ts').Code>}
*/
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<boolean>} 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<import('../types/options.d.ts').ResolvedOptions>}
*/
function generateSvelteMetadata(options) {
/** @type {Record<string, any>} */
const metadata = {};
for (const key of PREBUNDLE_SENSITIVE_OPTIONS) {
metadata[key] = options[key];
}
return metadata;
}
+166
View File
@@ -0,0 +1,166 @@
import process from 'node:process';
import * as vite from 'vite';
import { mapToRelative, removeLangSuffix } from './utils/sourcemaps.js';
const {
isCSSRequest,
preprocessCSS,
resolveConfig,
transformWithEsbuild,
//@ts-ignore rolldown types don't exist
rolldownVersion,
//@ts-ignore rolldown types don't exist
transformWithOxc
} = vite;
/**
* @typedef {(code: string, filename: string) => Promise<{ code: string; map?: any; deps?: Set<string> }>} CssTransform
*/
const supportedScriptLangs = ['ts'];
export const lang_sep = '.vite-preprocess';
/**
* @param {import('./public.d.ts').VitePreprocessOptions} [opts]
* @returns {import('svelte/compiler').PreprocessorGroup}
*/
export function vitePreprocess(opts) {
/** @type {import('svelte/compiler').PreprocessorGroup} */
const preprocessor = { name: 'vite-preprocess' };
if (opts?.script === true) {
preprocessor.script = rolldownVersion ? viteScriptOxc().script : viteScript().script;
}
if (opts?.style !== false) {
const styleOpts = typeof opts?.style == 'object' ? opts?.style : undefined;
preprocessor.style = viteStyle(styleOpts).style;
}
return preprocessor;
}
/**
* @returns {{ script: import('svelte/compiler').Preprocessor }}
*/
function viteScript() {
return {
async script({ attributes, content, filename = '' }) {
const lang = /** @type {string} */ (attributes.lang);
if (!supportedScriptLangs.includes(lang)) return;
const { code, map } = await transformWithEsbuild(content, filename, {
loader: /** @type {import('vite').ESBuildOptions['loader']} */ (lang),
target: 'esnext',
tsconfigRaw: {
compilerOptions: {
// svelte typescript needs this flag to work with type imports
importsNotUsedAsValues: 'preserve',
preserveValueImports: true
}
}
});
mapToRelative(map, filename);
return {
code,
map
};
}
};
}
/**
* @returns {{ script: import('svelte/compiler').Preprocessor }}
*/
function viteScriptOxc() {
return {
async script({ attributes, content, filename = '' }) {
if (typeof attributes.lang !== 'string' || !supportedScriptLangs.includes(attributes.lang)) {
return;
}
const lang = /** @type {'ts'} */ (attributes.lang);
const { code, map } = await transformWithOxc(content, filename, {
lang,
target: 'esnext'
// TODO, how to pass tsconfig compilerOptions (or not needed as config is loaded for file
/*tsconfigRaw: {
compilerOptions: {
// svelte typescript needs this flag to work with type imports
importsNotUsedAsValues: 'preserve',
preserveValueImports: true
}
}*/
});
mapToRelative(map, filename);
return {
code,
map
};
}
};
}
/**
* @param {import('vite').ResolvedConfig | import('vite').InlineConfig} config
* @returns {{ style: import('svelte/compiler').Preprocessor }}
*/
function viteStyle(config = {}) {
/** @type {Promise<CssTransform> | CssTransform} */
let cssTransform;
/** @type {import('svelte/compiler').Preprocessor} */
const style = async ({ attributes, content, filename = '' }) => {
const ext = attributes.lang ? `.${attributes.lang}` : '.css';
if (attributes.lang && !isCSSRequest(ext)) return;
if (!cssTransform) {
cssTransform = createCssTransform(style, config).then((t) => (cssTransform = t));
}
const transform = await cssTransform;
const suffix = `${lang_sep}${ext}`;
const moduleId = `${filename}${suffix}`;
const { code, map, deps } = await transform(content, moduleId);
removeLangSuffix(map, suffix);
mapToRelative(map, filename);
const dependencies = deps ? Array.from(deps).filter((d) => !d.endsWith(suffix)) : undefined;
return {
code,
map: map ?? undefined,
dependencies
};
};
// @ts-expect-error tag so can be found by v-p-s
style.__resolvedConfig = null;
return { style };
}
/**
* @param {import('svelte/compiler').Preprocessor} style
* @param {import('vite').ResolvedConfig | import('vite').InlineConfig} config
* @returns {Promise<CssTransform>}
*/
async function createCssTransform(style, config) {
/** @type {import('vite').ResolvedConfig} */
let resolvedConfig;
// @ts-expect-error special prop added if running in v-p-s
if (style.__resolvedConfig) {
// @ts-expect-error not typed
resolvedConfig = style.__resolvedConfig;
} else if (isResolvedConfig(config)) {
resolvedConfig = config;
} else {
// default to "build" if no NODE_ENV is set to avoid running in dev mode for svelte-check etc.
const useBuild = !process.env.NODE_ENV || process.env.NODE_ENV === 'production';
const command = useBuild ? 'build' : 'serve';
const defaultMode = useBuild ? 'production' : 'development';
resolvedConfig = await resolveConfig(config, command, defaultMode, defaultMode, false);
}
return async (code, filename) => {
return preprocessCSS(code, filename, resolvedConfig);
};
}
/**
* @param {any} config
* @returns {config is import('vite').ResolvedConfig}
*/
function isResolvedConfig(config) {
return !!config.inlineConfig;
}
+217
View File
@@ -0,0 +1,217 @@
import type { InlineConfig, ResolvedConfig } from 'vite';
import type { CompileOptions, Warning, PreprocessorGroup } from 'svelte/compiler';
import type { Options as InspectorOptions } from '@sveltejs/vite-plugin-svelte-inspector';
export type Options = Omit<SvelteConfig, 'vitePlugin'> & PluginOptionsInline;
interface PluginOptionsInline extends PluginOptions {
/**
* Path to a svelte config file, either absolute or relative to Vite root
*
* set to `false` to ignore the svelte config file
*
* @see https://vitejs.dev/config/#root
*/
configFile?: string | false;
}
export interface PluginOptions {
/**
* A `picomatch` pattern, or array of patterns, which specifies the files the plugin should
* operate on. By default, all svelte files are included.
*
* @see https://github.com/micromatch/picomatch
*/
include?: Arrayable<string | RegExp>;
/**
* A `picomatch` pattern, or array of patterns, which specifies the files to be ignored by the
* plugin. By default, no files are ignored.
*
* @see https://github.com/micromatch/picomatch
*/
exclude?: Arrayable<string | RegExp>;
/**
* Emit Svelte styles as virtual CSS files for Vite and other plugins to process
*
* @default true
*/
emitCss?: boolean;
/**
* Enable or disable Hot Module Replacement.
* Deprecated, use compilerOptions.hmr instead!
*
* @deprecated
* @default true for development, always false for production
*/
hot?: boolean;
/**
* Some Vite plugins can contribute additional preprocessors by defining `api.sveltePreprocess`.
* If you don't want to use them, set this to true to ignore them all or use an array of strings
* with plugin names to specify which.
*
* @default false
*/
ignorePluginPreprocessors?: boolean | string[];
/**
* vite-plugin-svelte automatically handles excluding svelte libraries and reinclusion of their dependencies
* in vite.optimizeDeps.
*
* `disableDependencyReinclusion: true` disables all reinclusions
* `disableDependencyReinclusion: ['foo','bar']` disables reinclusions for dependencies of foo and bar
*
* This should be used for hybrid packages that contain both node and browser dependencies, eg Routify
*
* @default false
*/
disableDependencyReinclusion?: boolean | string[];
/**
* Enable support for Vite's dependency optimization to prebundle Svelte libraries.
*
* To disable prebundling for a specific library, add it to `optimizeDeps.exclude`.
*
* @default true for dev, false for build
*/
prebundleSvelteLibraries?: boolean;
/**
* toggle/configure Svelte Inspector
*
* @default unset for dev, always false for build
*/
inspector?: InspectorOptions | boolean;
/**
* A function to update `compilerOptions` before compilation
*
* `data.filename` - The file to be compiled
* `data.code` - The preprocessed Svelte code
* `data.compileOptions` - The current compiler options
*
* To change part of the compiler options, return an object with the changes you need.
*
* @example
* ```
* ({ filename, compileOptions }) => {
* // Dynamically set runes mode per Svelte file
* if (forceRunesMode(filename) && !compileOptions.runes) {
* return { runes: true };
* }
* }
* ```
*/
dynamicCompileOptions?: (data: {
filename: string;
code: string;
compileOptions: Partial<CompileOptions>;
}) => Promise<Partial<CompileOptions> | void> | Partial<CompileOptions> | void;
/**
* These options are considered experimental and breaking changes to them can occur in any release
*/
experimental?: ExperimentalOptions;
}
export interface SvelteConfig {
/**
* A list of file extensions to be compiled by Svelte
*
* @default ['.svelte']
*/
extensions?: string[];
/**
* An array of preprocessors to transform the Svelte source code before compilation
*
* @see https://svelte.dev/docs/svelte/svelte-compiler#PreprocessorGroup
*/
preprocess?: Arrayable<PreprocessorGroup>;
/**
* The options to be passed to the Svelte compiler. A few options are set by default,
* including `dev` and `css`. However, some options are non-configurable, like
* `filename`, `format`, `generate`, and `cssHash` (in dev).
*
* @see https://svelte.dev/docs/svelte/svelte-compiler#CompileOptions
*/
compilerOptions?: Omit<CompileOptions, 'filename' | 'format' | 'generate'>;
/**
* Handles warning emitted from the Svelte compiler
*
* warnings emitted for files in node_modules are logged at the debug level, to see them run
* `DEBUG=vite-plugin-svelte:node-modules-onwarn pnpm build`
*
* @example
* ```
* (warning, defaultHandler) => {
* // ignore some warnings
* if (!['foo','bar'].includes(warning.code)) {
* defaultHandler(warning);
* }
* }
* ```
*
*/
onwarn?: (warning: Warning, defaultHandler: (warning: Warning) => void) => void;
/**
* Options for vite-plugin-svelte
*/
vitePlugin?: PluginOptions;
}
/**
* These options are considered experimental and breaking changes to them can occur in any release
*/
interface ExperimentalOptions {
/**
* send a websocket message with svelte compiler warnings during dev
*
*/
sendWarningsToBrowser?: boolean;
/**
* disable svelte field resolve warnings
*
* @default false
*/
disableSvelteResolveWarnings?: boolean;
/**
* disable api.sveltePreprocess deprecation warnings
*
* @default false
*/
disableApiSveltePreprocessWarnings?: boolean;
compileModule?: CompileModuleOptions;
}
interface CompileModuleOptions {
/**
* infix that must be present in filename
* @default ['.svelte.']
*/
infixes?: string[];
/**
* module extensions
* @default ['.ts','.js']
*/
extensions?: string[];
include?: Arrayable<string | RegExp>;
exclude?: Arrayable<string | RegExp>;
}
type Arrayable<T> = T | T[];
export interface VitePreprocessOptions {
/**
* preprocess script block with vite pipeline.
* Since svelte5 this is not needed for typescript anymore
*
* @default false
*/
script?: boolean;
/**
* preprocess style blocks with vite pipeline
*/
style?: boolean | InlineConfig | ResolvedConfig;
}
// eslint-disable-next-line n/no-missing-import
export * from './index.js';
+37
View File
@@ -0,0 +1,37 @@
import type { Processed, CompileResult } from 'svelte/compiler';
import type { SvelteRequest } from './id.d.ts';
import type { ResolvedOptions } from './options.d.ts';
import type { CustomPluginOptionsVite, Rollup } from 'vite';
export type CompileSvelte = (
svelteRequest: SvelteRequest,
code: string,
options: Partial<ResolvedOptions>,
sourcemap?: Rollup.SourceMap
) => Promise<CompileData>;
export type PreprocessSvelte = (
svelteRequest: SvelteRequest,
code: string,
options: Partial<ResolvedOptions>
) => Promise<Processed | undefined>;
export interface Code {
code: string;
map?: any;
dependencies?: any[];
hasGlobal?: boolean;
moduleType?: string; //vite-8 beta
meta?: {
vite?: CustomPluginOptionsVite;
};
}
export interface CompileData {
filename: string;
normalizedFilename: string;
cssId: string;
lang: string;
compiled: CompileResult;
ssr: boolean | undefined;
}
+46
View File
@@ -0,0 +1,46 @@
export type SvelteQueryTypes = 'style';
export interface RequestQuery {
// our own
svelte?: boolean;
type?: SvelteQueryTypes;
// vite specific
url?: boolean;
raw?: boolean;
direct?: boolean;
inline?: boolean;
}
export interface SvelteRequest {
id: string;
cssId: string;
filename: string;
normalizedFilename: string;
query: RequestQuery;
timestamp: number;
ssr: boolean;
raw: boolean;
}
export interface SvelteModuleRequest {
id: string;
filename: string;
normalizedFilename: string;
query: RequestQuery;
timestamp: number;
ssr: boolean;
}
export type IdParser = (id: string, ssr: boolean, timestamp?: number) => SvelteRequest | undefined;
export type IdFilter = {
id: {
include: Array<string | RegExp>;
exclude: Array<string | RegExp>;
};
};
export type ModuleIdParser = (
id: string,
ssr: boolean,
timestamp?: number
) => SvelteModuleRequest | undefined;
+24
View File
@@ -0,0 +1,24 @@
import type { Warning } from 'svelte/compiler';
export interface LogFn extends SimpleLogFn {
(message: string, payload?: unknown, namespace?: string): void;
enabled: boolean;
once: SimpleLogFn;
}
export interface SimpleLogFn {
(message: string, payload?: unknown, namespace?: string): void;
}
export type SvelteWarningsMessage = {
id: string;
filename: string;
normalizedFilename: string;
timestamp: number;
warnings: Warning[]; // allWarnings filtered by warnings where onwarn did not call the default handler
allWarnings: Warning[]; // includes warnings filtered by onwarn and our extra vite plugin svelte warnings
rawWarnings: Warning[]; // raw compiler output
};
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
+21
View File
@@ -0,0 +1,21 @@
import type { CompileOptions } from 'svelte/compiler';
import type { ViteDevServer } from 'vite';
// eslint-disable-next-line n/no-missing-import
import { VitePluginSvelteStats } from '../utils/vite-plugin-svelte-stats.js';
import type { Options } from '../public.d.ts';
export interface PreResolvedOptions extends Options {
// these options are non-nullable after resolve
compilerOptions: CompileOptions;
// extra options
root: string;
isBuild: boolean;
isServe: boolean;
isDebug: boolean;
}
export interface ResolvedOptions extends PreResolvedOptions {
isProduction: boolean;
server?: ViteDevServer;
stats?: VitePluginSvelteStats;
}
+15
View File
@@ -0,0 +1,15 @@
import type { ResolvedOptions } from './options.d.ts';
import type { IdFilter, IdParser } from './id.d.ts';
import type { CompileSvelte } from './compile.d.ts';
export interface PluginAPI {
options: ResolvedOptions;
filter: IdFilter;
idParser: IdParser;
compileSvelte: CompileSvelte;
/**
* @deprecated use 'filter' instead
* // TODO remove in next major
*/
idFilter: IdFilter;
}
@@ -0,0 +1,30 @@
export interface Stat {
file: string;
pkg?: string;
start: number;
end: number;
}
export interface StatCollection {
name: string;
options: CollectionOptions;
start: (file: string) => () => void;
stats: Stat[];
packageStats?: PackageStats[];
collectionStart: number;
duration?: number;
finish: () => Promise<void> | void;
finished: boolean;
}
export interface PackageStats {
pkg: string;
files: number;
duration: number;
}
export interface CollectionOptions {
logInProgress: (collection: StatCollection, now: number) => boolean;
logResult: (collection: StatCollection) => boolean;
}
+152
View File
@@ -0,0 +1,152 @@
import * as svelte from 'svelte/compiler';
import { safeBase64Hash } from './hash.js';
import { log } from './log.js';
import { mapToRelative } from './sourcemaps.js';
import { enhanceCompileError } from './error.js';
// TODO this is a patched version of https://github.com/sveltejs/vite-plugin-svelte/pull/796/files#diff-3bce0b33034aad4b35ca094893671f7e7ddf4d27254ae7b9b0f912027a001b15R10
// which is closer to the other regexes in at least not falling into commented script
// but ideally would be shared exactly with svelte and other tools that use it
const scriptLangRE =
/<!--[^]*?-->|<script\s+(?:[^>]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/g;
/**
* @returns {import('../types/compile.d.ts').CompileSvelte}
*/
export function createCompileSvelte() {
/** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */
let stats;
/** @type {import('../types/compile.d.ts').CompileSvelte} */
return async function compileSvelte(svelteRequest, code, options, sourcemap) {
const { filename, normalizedFilename, cssId, ssr, raw } = svelteRequest;
const { emitCss = true } = options;
/** @type {import('svelte/compiler').Warning[]} */
const warnings = [];
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'} compile`, {
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
}
}
/** @type {import('svelte/compiler').CompileOptions} */
const compileOptions = {
...options.compilerOptions,
filename,
generate: ssr ? 'server' : 'client'
};
let finalCode = code;
if (compileOptions.hmr && options.emitCss) {
const hash = `s-${safeBase64Hash(normalizedFilename)}`;
compileOptions.cssHash = () => hash;
const closeStylePos = code.lastIndexOf('</style>');
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,
compileOptions
});
if (dynamicCompileOptions && log.debug.enabled) {
log.debug(
`dynamic compile options for ${filename}: ${JSON.stringify(dynamicCompileOptions)}`,
undefined,
'compile'
);
}
const finalCompileOptions = dynamicCompileOptions
? {
...compileOptions,
...dynamicCompileOptions
}
: compileOptions;
if (sourcemap) {
finalCompileOptions.sourcemap = sourcemap;
}
const endStat = stats?.start(filename);
/** @type {import('svelte/compiler').CompileResult} */
let compiled;
try {
compiled = svelte.compile(finalCode, { ...finalCompileOptions, filename });
// patch output with partial accept until svelte does it
// TODO remove later
if (
options.server?.config.experimental.hmrPartialAccept &&
compiled.js.code.includes('import.meta.hot.accept(')
) {
compiled.js.code = compiled.js.code.replaceAll(
'import.meta.hot.accept(',
'import.meta.hot.acceptExports(["default"],'
);
}
} catch (e) {
enhanceCompileError(e, code, options.preprocess);
throw e;
}
if (endStat) {
endStat();
}
mapToRelative(compiled.js?.map, filename);
mapToRelative(compiled.css?.map, filename);
if (warnings.length) {
if (!compiled.warnings) {
compiled.warnings = [];
}
compiled.warnings.push(...warnings);
}
if (!raw) {
// wire css import and code for hmr
const hasCss = compiled.css?.code?.trim()?.length ?? 0 > 0;
// compiler might not emit css with mode none or it may be empty
if (emitCss && hasCss) {
// TODO properly update sourcemap?
compiled.js.code += `\nimport ${JSON.stringify(cssId)};\n`;
}
}
let lang = 'js';
for (const match of code.matchAll(scriptLangRE)) {
if (match[2]) {
lang = match[2];
break;
}
}
return {
filename,
normalizedFilename,
cssId,
lang,
compiled,
ssr
};
};
}
+36
View File
@@ -0,0 +1,36 @@
import { createRequire } from 'node:module';
const sveltePkg = createRequire(import.meta.url)('svelte/package.json');
// list of svelte runtime dependencies to optimize together with svelte itself
export const SVELTE_RUNTIME_DEPENDENCIES = [
'clsx' // avoids dev server restart after page load with npm + vite6 (see #1067)
].filter((dep) => !!sveltePkg.dependencies?.[dep]);
export const SVELTE_IMPORTS = Object.entries(sveltePkg.exports)
.map(([name, config]) => {
// ignore type only
if (typeof config === 'object' && Object.keys(config).length === 1 && config.types) {
return '';
}
// ignore names
if (name === './package.json' || name === './compiler') {
return '';
}
return name.replace(/^\./, 'svelte');
})
.filter((s) => s.length > 0);
export const SVELTE_EXPORT_CONDITIONS = ['svelte'];
export const FAQ_LINK_MISSING_EXPORTS_CONDITION =
'https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/faq.md#missing-exports-condition';
export const LINK_TRANSFORM_WITH_PLUGIN =
'https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/advanced-usage.md#transform-svelte-files-with-vite-plugins';
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_ID_REGEX = /[?&]svelte&type=style&lang.css$/;
+60
View File
@@ -0,0 +1,60 @@
const COMMON_DEPENDENCIES_WITHOUT_SVELTE_FIELD = [
'@lukeed/uuid',
'@playwright/test',
'@sveltejs/kit',
'@sveltejs/package',
'@sveltejs/vite-plugin-svelte',
'autoprefixer',
'cookie',
'dotenv',
'esbuild',
'eslint',
'jest',
'mdsvex',
'playwright',
'postcss',
'prettier',
'svelte',
'svelte2tsx',
'svelte-check',
'svelte-preprocess',
'tslib',
'typescript',
'vite',
'vitest',
'__vite-browser-external' // see https://github.com/sveltejs/vite-plugin-svelte/issues/362
];
const COMMON_PREFIXES_WITHOUT_SVELTE_FIELD = [
'@fontsource/',
'@postcss-plugins/',
'@rollup/',
'@sveltejs/adapter-',
'@types/',
'@typescript-eslint/',
'eslint-',
'jest-',
'postcss-plugin-',
'prettier-plugin-',
'rollup-plugin-',
'vite-plugin-'
];
/**
* Test for common dependency names that tell us it is not a package including a svelte field, eg. eslint + plugins.
*
* This speeds up the find process as we don't have to try and require the package.json for all of them
*
* @param {string} dependency
* @returns {boolean} true if it is a dependency without a svelte field
*/
export function isCommonDepWithoutSvelteField(dependency) {
return (
COMMON_DEPENDENCIES_WITHOUT_SVELTE_FIELD.includes(dependency) ||
COMMON_PREFIXES_WITHOUT_SVELTE_FIELD.some(
(prefix) =>
prefix.startsWith('@')
? dependency.startsWith(prefix)
: dependency.substring(dependency.lastIndexOf('/') + 1).startsWith(prefix) // check prefix omitting @scope/
)
);
}
+167
View File
@@ -0,0 +1,167 @@
import { buildExtendedLogMessage } from './log.js';
/**
* convert an error thrown by svelte.compile to a RollupError so that vite displays it in a user friendly way
* @param {import('svelte/compiler').Warning & Error & {frame?: string}} error a svelte compiler error, which is a mix of Warning and an error
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {import('vite').Rollup.RollupError} the converted error
*/
export function toRollupError(error, options) {
const { filename, frame, start, code, name, stack } = error;
/** @type {import('vite').Rollup.RollupError} */
const rollupError = {
name, // needed otherwise sveltekit coalesce_to_error turns it into a string
id: filename,
message: buildExtendedLogMessage(error), // include filename:line:column so that it's clickable
frame: formatFrameForVite(frame),
code,
stack: options.isBuild || options.isDebug || !frame ? stack : ''
};
if (start) {
rollupError.loc = {
line: start.line,
column: start.column,
file: filename
};
}
return rollupError;
}
/**
* convert an error thrown by svelte.compile to an esbuild PartialMessage
* @param {import('svelte/compiler').Warning & Error & {frame?: string}} error a svelte compiler error, which is a mix of Warning and an error
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {any} the converted error as esbuild PartialMessage
*
* note: typed any to avoid esbuild devDependency for a single internal type import
*/
export function toESBuildError(error, options) {
const { filename, frame, start, stack } = error;
/** @type any */
const partialMessage = {
text: buildExtendedLogMessage(error)
};
if (start) {
partialMessage.location = {
line: start.line,
column: start.column,
file: filename,
lineText: lineFromFrame(start.line, frame) // needed to get a meaningful error message on cli
};
}
if (options.isBuild || options.isDebug || !frame) {
partialMessage.detail = stack;
}
return partialMessage;
}
/**
* extract line with number from codeframe
*
* @param {number} lineNo
* @param {string} [frame]
* @returns {string}
*/
function lineFromFrame(lineNo, frame) {
if (!frame) {
return '';
}
const lines = frame.split('\n');
const errorLine = lines.find((line) => line.trimStart().startsWith(`${lineNo}: `));
return errorLine ? errorLine.substring(errorLine.indexOf(': ') + 3) : '';
}
/**
* vite error overlay expects a specific format to show frames
* this reformats svelte frame (colon separated, less whitespace)
* to one that vite displays on overlay ( pipe separated, more whitespace)
* e.g.
* ```
* 1: foo
* 2: bar;
* ^
* 3: baz
* ```
* to
* ```
* 1 | foo
* 2 | bar;
* ^
* 3 | baz
* ```
* @see https://github.com/vitejs/vite/blob/96591bf9989529de839ba89958755eafe4c445ae/packages/vite/src/client/overlay.ts#L116
* @param {string} [frame]
* @returns {string}
*/
function formatFrameForVite(frame) {
if (!frame) {
return '';
}
return frame
.split('\n')
.map((line) => (line.match(/^\s+\^/) ? ' ' + line : ' ' + line.replace(':', ' | ')))
.join('\n');
}
/**
*
* @param {string} code the svelte error code
* @see https://github.com/sveltejs/svelte/blob/main/packages/svelte/src/compiler/errors.js
* @returns {boolean}
*/
function couldBeFixedByCssPreprocessor(code) {
return code === 'expected_token' || code === 'unexpected_eof' || code?.startsWith('css_');
}
/**
* @param {import('svelte/compiler').Warning & Error} err a svelte compiler error, which is a mix of Warning and an error
* @param {string} originalCode
* @param {import('../public.d.ts').Options['preprocess']} [preprocessors]
*/
export function enhanceCompileError(err, originalCode, preprocessors) {
preprocessors = arraify(preprocessors ?? []);
/** @type {string[]} */
const additionalMessages = [];
// Handle incorrect CSS preprocessor usage
if (couldBeFixedByCssPreprocessor(err.code)) {
// Reference from Svelte: https://github.com/sveltejs/svelte/blob/9926347ad9dbdd0f3324d5538e25dcb7f5e442f8/packages/svelte/src/compiler/preprocess/index.js#L257
const styleRe =
/<!--[^]*?-->|<style((?:\s+[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)|\s+[^=>'"/]+)*\s*)(?:\/>|>([\S\s]*?)<\/style>)/g;
let m;
while ((m = styleRe.exec(originalCode))) {
if (m[0]?.startsWith('<!--')) {
continue;
}
// Warn missing lang attribute
if (!m[1]?.includes('lang=')) {
additionalMessages.push('Did you forget to add a lang attribute to your style tag?');
}
// Warn missing style preprocessor
if (
preprocessors.every((p) => p.style == null || p.name === 'inject-scope-everything-rule')
) {
const preprocessorType = m[1]?.match(/lang="(.+?)"/)?.[1] ?? 'style';
additionalMessages.push(
`Did you forget to add a ${preprocessorType} preprocessor? See https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/preprocess.md for more information.`
);
}
}
}
if (additionalMessages.length) {
err.message += '\n\n- ' + additionalMessages.join('\n- ');
}
return err;
}
/**
* @param {T | T[]} value
* @template T
*/
function arraify(value) {
return Array.isArray(value) ? value : [value];
}
+43
View File
@@ -0,0 +1,43 @@
import crypto from 'node:crypto';
const hashes = Object.create(null);
//TODO shorter?
const hash_length = 12;
/**
* replaces +/= in base64 output so they don't interfere
*
* @param {string} input
* @returns {string} base64 hash safe to use in any context
*/
export function safeBase64Hash(input) {
if (hashes[input]) {
return hashes[input];
}
//TODO if performance really matters, use a faster one like xx-hash etc.
// should be evenly distributed because short input length and similarities in paths could cause collisions otherwise
// OR DON'T USE A HASH AT ALL, what about a simple counter?
const md5 = crypto.createHash('md5');
md5.update(input);
const hash = toSafe(md5.digest('base64')).slice(0, hash_length);
hashes[input] = hash;
return hash;
}
/** @type {Record<string, string>} */
const replacements = {
'+': '-',
'/': '_',
'=': ''
};
const replaceRE = new RegExp(`[${Object.keys(replacements).join('')}]`, 'g');
/**
* @param {string} base64
* @returns {string}
*/
function toSafe(base64) {
return base64.replace(replaceRE, (x) => replacements[x]);
}
+274
View File
@@ -0,0 +1,274 @@
import { normalizePath } from 'vite';
import fs from 'node:fs';
import process from 'node:process';
import { log } from './log.js';
import {
DEFAULT_SVELTE_EXT,
DEFAULT_SVELTE_MODULE_EXT,
DEFAULT_SVELTE_MODULE_INFIX,
SVELTE_VIRTUAL_STYLE_ID_REGEX
} from './constants.js';
import { arraify } from './options.js';
const VITE_FS_PREFIX = '/@fs/';
const IS_WINDOWS = process.platform === 'win32';
const SUPPORTED_COMPILER_OPTIONS = ['generate', 'dev', 'css', 'customElement', 'immutable'];
const TYPES_WITH_COMPILER_OPTIONS = ['style', 'script', 'all'];
/**
* @param {string} id
* @returns {{ filename: string, rawQuery: string }}
*/
function splitId(id) {
const parts = id.split('?', 2);
const filename = parts[0];
const rawQuery = parts[1];
return { filename, rawQuery };
}
/**
* @param {string} id
* @param {string} filename
* @param {string} rawQuery
* @param {string} root
* @param {number} timestamp
* @param {boolean} ssr
* @returns {import('../types/id.d.ts').SvelteRequest | undefined}
*/
function parseToSvelteRequest(id, filename, rawQuery, root, timestamp, ssr) {
const query = parseRequestQuery(rawQuery);
const rawOrDirect = !!(query.raw || query.direct);
if (query.url || (!query.svelte && rawOrDirect)) {
// skip requests with special vite tags
return;
}
const raw = rawOrDirect;
const normalizedFilename = normalize(filename, root);
const cssId = createVirtualImportId(filename, root, 'style');
return {
id,
filename,
normalizedFilename,
cssId,
query,
timestamp,
ssr,
raw
};
}
/**
* @param {string} filename
* @param {string} root
* @param {import('../types/id.d.ts').SvelteQueryTypes} type
* @returns {string}
*/
function createVirtualImportId(filename, root, type) {
const parts = ['svelte', `type=${type}`];
if (type === 'style') {
parts.push('lang.css');
}
if (existsInRoot(filename, root)) {
filename = root + filename;
} else if (filename.startsWith(VITE_FS_PREFIX)) {
filename = IS_WINDOWS
? filename.slice(VITE_FS_PREFIX.length) // remove /@fs/ from /@fs/C:/...
: filename.slice(VITE_FS_PREFIX.length - 1); // remove /@fs from /@fs/home/user
}
// return same virtual id format as vite-plugin-vue eg ...App.svelte?svelte&type=style&lang.css
return `${filename}?${parts.join('&')}`;
}
/**
* @param {string} rawQuery
* @returns {import('../types/id.d.ts').RequestQuery}
*/
function parseRequestQuery(rawQuery) {
const query = Object.fromEntries(new URLSearchParams(rawQuery));
for (const key in query) {
if (query[key] === '') {
// @ts-expect-error not boolean
query[key] = true;
}
}
const compilerOptions = query.compilerOptions;
if (compilerOptions) {
if (!((query.raw || query.direct) && TYPES_WITH_COMPILER_OPTIONS.includes(query.type))) {
throw new Error(
`Invalid compilerOptions in query ${rawQuery}. CompilerOptions are only supported for raw or direct queries with type in "${TYPES_WITH_COMPILER_OPTIONS.join(
', '
)}" e.g. '?svelte&raw&type=script&compilerOptions={"generate":"server","dev":false}`
);
}
try {
const parsed = JSON.parse(compilerOptions);
const invalid = Object.keys(parsed).filter(
(key) => !SUPPORTED_COMPILER_OPTIONS.includes(key)
);
if (invalid.length) {
throw new Error(
`Invalid compilerOptions in query ${rawQuery}: ${invalid.join(
', '
)}. Supported: ${SUPPORTED_COMPILER_OPTIONS.join(', ')}`
);
}
query.compilerOptions = parsed;
} catch (e) {
log.error('failed to parse request query compilerOptions', e);
throw e;
}
}
return /** @type {import('../types/id.d.ts').RequestQuery}*/ query;
}
/**
* posixify and remove root at start
*
* @param {string} filename
* @param {string} normalizedRoot
* @returns {string}
*/
export function normalize(filename, normalizedRoot) {
return stripRoot(normalizePath(filename), normalizedRoot);
}
/**
* @param {string} filename
* @param {string} root
* @returns {boolean}
*/
function existsInRoot(filename, root) {
if (filename.startsWith(VITE_FS_PREFIX)) {
return false; // vite already tagged it as out of root
}
return fs.existsSync(root + filename);
}
/**
* @param {string} normalizedFilename
* @param {string} normalizedRoot
* @returns {string}
*/
function stripRoot(normalizedFilename, normalizedRoot) {
return normalizedFilename.startsWith(normalizedRoot + '/')
? normalizedFilename.slice(normalizedRoot.length)
: normalizedFilename;
}
/**
*
* @param {string} s
* @returns {string}
*/
function escapeRE(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {import('../types/id.d.ts').IdFilter}
*/
export function buildIdFilter(options) {
const { include = [], exclude = [], extensions = DEFAULT_SVELTE_EXT } = options;
// this regex combines configured extensions and looks for them at the end of the string or directly before first ? or #
const extensionsRE = new RegExp(
`^[^?#]+\\.(?:${extensions
.map((e) => (e.startsWith('.') ? e.slice(1) : e))
.map(escapeRE)
.join('|')})(?:[?#]|$)`
);
return {
id: {
include: [extensionsRE, .../**@type {Array<string|RegExp>}*/ arraify(include)],
exclude: /**@type {Array<string|RegExp>}*/ [
'\0',
SVELTE_VIRTUAL_STYLE_ID_REGEX, // exclude from regular pipeline, we load it in a separate plugin
...arraify(exclude)
]
}
};
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {import('../types/id.d.ts').IdParser}
*/
export function buildIdParser(options) {
const normalizedRoot = normalizePath(options.root);
return (id, ssr, timestamp = Date.now()) => {
const { filename, rawQuery } = splitId(id);
return parseToSvelteRequest(id, filename, rawQuery, normalizedRoot, timestamp, ssr);
};
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {import('../types/id.d.ts').IdFilter}
*/
export function buildModuleIdFilter(options) {
const {
infixes = DEFAULT_SVELTE_MODULE_INFIX,
include = [],
exclude = [],
extensions = DEFAULT_SVELTE_MODULE_EXT
} = options.experimental?.compileModule ?? {};
// this regex combines configured infixes and extensions
// and looks for them at the end of the string or directly before first ? or #
const infixWithExtRE = new RegExp(
`^[^?#]+(?:${infixes.map(escapeRE).join('|')})(?:[^.\\\\/]+\\.)*(?:${extensions
.map((e) => (e.startsWith('.') ? e.slice(1) : e))
.map(escapeRE)
.join('|')})(?:[?#]|$)`
);
return {
id: {
include: [infixWithExtRE, .../**@type {Array<string|RegExp>}*/ arraify(include)],
exclude: ['\0', .../**@type {Array<string|RegExp>}*/ arraify(exclude)]
}
};
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {import('../types/id.d.ts').ModuleIdParser}
*/
export function buildModuleIdParser(options) {
const root = options.root;
const normalizedRoot = normalizePath(root);
return (id, ssr, timestamp = Date.now()) => {
const { filename, rawQuery } = splitId(id);
return parseToSvelteModuleRequest(id, filename, rawQuery, normalizedRoot, timestamp, ssr);
};
}
/**
* @param {string} id
* @param {string} filename
* @param {string} rawQuery
* @param {string} root
* @param {number} timestamp
* @param {boolean} ssr
* @returns {import('../types/id.d.ts').SvelteModuleRequest | undefined}
*/
function parseToSvelteModuleRequest(id, filename, rawQuery, root, timestamp, ssr) {
const query = parseRequestQuery(rawQuery);
if (query.url || query.raw || query.direct) {
// skip requests with special vite tags
return;
}
const normalizedFilename = normalize(filename, root);
return {
id,
filename,
normalizedFilename,
query,
timestamp,
ssr
};
}
@@ -0,0 +1,80 @@
import path from 'node:path';
import process from 'node:process';
import fs from 'node:fs';
import { pathToFileURL } from 'node:url';
import { log } from './log.js';
export const knownSvelteConfigNames = ['js', 'ts', 'mjs', 'mts'].map(
(ext) => `svelte.config.${ext}`
);
/**
* @param {string} filePath
* @param {number} timestamp
*/
async function dynamicImportDefault(filePath, timestamp) {
return await import(filePath + '?t=' + timestamp).then((m) => m.default);
}
/**
* @param {import('vite').UserConfig} [viteConfig]
* @param {Partial<import('../public.d.ts').Options>} [inlineOptions]
* @returns {Promise<Partial<import('../public.d.ts').SvelteConfig> | undefined>}
*/
export async function loadSvelteConfig(viteConfig, inlineOptions) {
if (inlineOptions?.configFile === false) {
return;
}
const configFile = findConfigToLoad(viteConfig, inlineOptions);
if (configFile) {
try {
const result = await dynamicImportDefault(
pathToFileURL(configFile).href,
fs.statSync(configFile).mtimeMs
);
if (result != null) {
return {
...result,
configFile
};
} else {
throw new Error(`invalid export in ${configFile}`);
}
} catch (e) {
log.error(`failed to import config ${configFile}`, e);
throw e;
}
}
}
/**
* @param {import('vite').UserConfig | undefined} viteConfig
* @param {Partial<import('../public.d.ts').Options> | undefined} inlineOptions
* @returns {string | undefined}
*/
function findConfigToLoad(viteConfig, inlineOptions) {
const root = viteConfig?.root || process.cwd();
if (inlineOptions?.configFile) {
const abolutePath = path.isAbsolute(inlineOptions.configFile)
? inlineOptions.configFile
: path.resolve(root, inlineOptions.configFile);
if (!fs.existsSync(abolutePath)) {
throw new Error(`failed to find svelte config file ${abolutePath}.`);
}
return abolutePath;
} else {
const existingKnownConfigFiles = knownSvelteConfigNames
.map((candidate) => path.resolve(root, candidate))
.filter((file) => fs.existsSync(file));
if (existingKnownConfigFiles.length === 0) {
log.info(`no Svelte config found at ${root} - using default configuration.`);
return;
} else if (existingKnownConfigFiles.length > 1) {
log.warn(
`found more than one svelte config file, using ${existingKnownConfigFiles[0]}. you should only have one!`,
existingKnownConfigFiles
);
}
return existingKnownConfigFiles[0];
}
}
+256
View File
@@ -0,0 +1,256 @@
/* eslint-disable no-console */
// eslint-disable-next-line n/no-unsupported-features/node-builtins
import { styleText } from 'node:util';
const cyan = (/** @type {string} */ txt) => styleText('cyan', txt);
const yellow = (/** @type {string} */ txt) => styleText('yellow', txt);
const red = (/** @type {string} */ txt) => styleText('red', txt);
import { createDebug, enabled } from 'obug';
/** @type {import('../types/log.d.ts').LogLevel[]} */
const levels = ['debug', 'info', 'warn', 'error', 'silent'];
const prefix = 'vite-plugin-svelte';
/** @type {Record<import('../types/log.d.ts').LogLevel, any>} */
const loggers = {
debug: {
log: createDebug(`${prefix}`),
enabled: false,
isDebug: true
},
info: {
color: cyan,
log: console.log,
enabled: true
},
warn: {
color: yellow,
log: console.warn,
enabled: true
},
error: {
color: red,
log: console.error,
enabled: true
},
silent: {
enabled: false
}
};
/** @type {import('../types/log.d.ts').LogLevel} */
let _level = 'info';
/**
* @param {import('../types/log.d.ts').LogLevel} level
* @returns {void}
*/
function setLevel(level) {
if (level === _level) {
return;
}
const levelIndex = levels.indexOf(level);
if (levelIndex > -1) {
_level = level;
for (let i = 0; i < levels.length; i++) {
loggers[levels[i]].enabled = i >= levelIndex;
}
} else {
_log(loggers.error, `invalid log level: ${level} `);
}
}
/**
* @param {any} logger
* @param {string} message
* @param {any} [payload]
* @param {string} [namespace]
* @returns
*/
function _log(logger, message, payload, namespace) {
if (!logger.enabled) {
return;
}
if (logger.isDebug) {
let log = logger.log;
if (namespace) {
if (!isDebugNamespaceEnabled(namespace)) {
return;
}
log = logger.log.extend(namespace);
}
if (payload !== undefined) {
log(message, payload);
} else {
log(message);
}
} else {
logger.log(
logger.color(
`${new Date().toLocaleTimeString()} [${prefix}${
namespace ? `:${namespace}` : ''
}] ${message}`
)
);
if (payload) {
logger.log(payload);
}
}
}
/**
* @param {import('../types/log.d.ts').LogLevel} level
* @returns {import('../types/log.d.ts').LogFn}
*/
function createLogger(level) {
const logger = loggers[level];
const logFn = /** @type {import('../types/log.d.ts').LogFn} */ (_log.bind(null, logger));
/** @type {Set<string>} */
const logged = new Set();
/** @type {import('../types/log.d.ts').SimpleLogFn} */
const once = function (message, payload, namespace) {
if (!logger.enabled || logged.has(message)) {
return;
}
logged.add(message);
logFn.apply(null, [message, payload, namespace]);
};
Object.defineProperty(logFn, 'enabled', {
get() {
return logger.enabled;
}
});
Object.defineProperty(logFn, 'once', {
get() {
return once;
}
});
return logFn;
}
export const log = {
debug: createLogger('debug'),
info: createLogger('info'),
warn: createLogger('warn'),
error: createLogger('error'),
setLevel
};
/**
* @param {import('../types/id.d.ts').SvelteRequest | import('../types/id.d.ts').SvelteModuleRequest} svelteRequest
* @param {import('svelte/compiler').Warning[]} warnings
* @param {import('../types/options.d.ts').ResolvedOptions} options
*/
export function logCompilerWarnings(svelteRequest, warnings, options) {
const { emitCss, onwarn, isBuild } = options;
const sendViaWS = !isBuild && options.experimental?.sendWarningsToBrowser;
let warn = isBuild ? warnBuild : warnDev;
/** @type {import('svelte/compiler').Warning[]} */
const handledByDefaultWarn = [];
const allWarnings = warnings?.filter((w) => !ignoreCompilerWarning(w, isBuild, emitCss));
if (sendViaWS) {
const _warn = warn;
/** @type {(w: import('svelte/compiler').Warning) => void} */
warn = (w) => {
handledByDefaultWarn.push(w);
_warn(w);
};
}
allWarnings.forEach((warning) => {
if (onwarn) {
onwarn(warning, warn);
} else {
warn(warning);
}
});
if (sendViaWS) {
/** @type {import('../types/log.d.ts').SvelteWarningsMessage} */
const message = {
id: svelteRequest.id,
filename: svelteRequest.filename,
normalizedFilename: svelteRequest.normalizedFilename,
timestamp: svelteRequest.timestamp,
warnings: handledByDefaultWarn, // allWarnings filtered by warnings where onwarn did not call the default handler
allWarnings, // includes warnings filtered by onwarn and our extra vite plugin svelte warnings
rawWarnings: warnings // raw compiler output
};
log.debug(`sending svelte:warnings message for ${svelteRequest.normalizedFilename}`);
options.server?.ws?.send('svelte:warnings', message);
}
}
/**
* @param {import('svelte/compiler').Warning} warning
* @param {boolean} isBuild
* @param {boolean} [emitCss]
* @returns {boolean}
*/
function ignoreCompilerWarning(warning, isBuild, emitCss) {
return (
(!emitCss && warning.code === 'css_unused_selector') || // same as rollup-plugin-svelte
(!isBuild && isNoScopableElementWarning(warning))
);
}
/**
*
* @param {import('svelte/compiler').Warning} warning
* @returns {boolean}
*/
function isNoScopableElementWarning(warning) {
// see https://github.com/sveltejs/vite-plugin-svelte/issues/153
return warning.code === 'css_unused_selector' && warning.message.includes('"*"');
}
/**
* @param {import('svelte/compiler').Warning} w
*/
function warnDev(w) {
if (w.filename?.includes('node_modules')) {
if (isDebugNamespaceEnabled('node-modules-onwarn')) {
log.debug(buildExtendedLogMessage(w), undefined, 'node-modules-onwarn');
}
} else if (log.info.enabled) {
log.info(buildExtendedLogMessage(w));
}
}
/**
* @param {import('svelte/compiler').Warning & {frame?: string}} w
*/
function warnBuild(w) {
if (w.filename?.includes('node_modules')) {
if (isDebugNamespaceEnabled('node-modules-onwarn')) {
log.debug(buildExtendedLogMessage(w), w.frame, 'node-modules-onwarn');
}
} else if (log.warn.enabled) {
log.warn(buildExtendedLogMessage(w), w.frame);
}
}
/**
* @param {import('svelte/compiler').Warning} w
*/
export function buildExtendedLogMessage(w) {
const parts = [];
if (w.filename) {
parts.push(w.filename);
}
if (w.start) {
parts.push(':', w.start.line, ':', w.start.column);
}
if (w.message) {
if (parts.length > 0) {
parts.push(' ');
}
parts.push(w.message);
}
return parts.join('');
}
/**
* @param {string} namespace
* @returns {boolean}
*/
export function isDebugNamespaceEnabled(namespace) {
return enabled(`${prefix}:${namespace}`);
}
+618
View File
@@ -0,0 +1,618 @@
import process from 'node:process';
import * as vite from 'vite';
const {
defaultClientMainFields,
defaultServerMainFields,
defaultClientConditions,
defaultServerConditions,
normalizePath,
searchForWorkspaceRoot,
// @ts-ignore
rolldownVersion
} = vite;
import { log } from './log.js';
import { loadSvelteConfig } from './load-svelte-config.js';
import {
DEFAULT_SVELTE_EXT,
FAQ_LINK_MISSING_EXPORTS_CONDITION,
SVELTE_EXPORT_CONDITIONS,
SVELTE_IMPORTS,
SVELTE_RUNTIME_DEPENDENCIES
} from './constants.js';
import path from 'node:path';
import { addExtraPreprocessors } from './preprocess.js';
import deepmerge from 'deepmerge';
import {
crawlFrameworkPkgs,
isDepExcluded,
isDepExternaled,
isDepIncluded,
isDepNoExternaled
} from 'vitefu';
import { isCommonDepWithoutSvelteField } from './dependencies.js';
const allowedPluginOptions = new Set([
'include',
'exclude',
'emitCss',
'hot',
'ignorePluginPreprocessors',
'disableDependencyReinclusion',
'prebundleSvelteLibraries',
'inspector',
'dynamicCompileOptions',
'experimental'
]);
const knownRootOptions = new Set(['extensions', 'compilerOptions', 'preprocess', 'onwarn']);
const allowedInlineOptions = new Set(['configFile', ...allowedPluginOptions, ...knownRootOptions]);
/**
* @param {Partial<import('../public.d.ts').Options>} [inlineOptions]
*/
export function validateInlineOptions(inlineOptions) {
const invalidKeys = Object.keys(inlineOptions || {}).filter(
(key) => !allowedInlineOptions.has(key)
);
if (invalidKeys.length) {
log.warn(`invalid plugin options "${invalidKeys.join(', ')}" in inline config`, inlineOptions);
}
}
/**
* @param {Partial<import('../public.d.ts').SvelteConfig>} [config]
* @returns {Partial<import('../public.d.ts').Options> | undefined}
*/
function convertPluginOptions(config) {
if (!config) {
return;
}
const invalidRootOptions = Object.keys(config).filter((key) => allowedPluginOptions.has(key));
if (invalidRootOptions.length > 0) {
throw new Error(
`Invalid options in svelte config. Move the following options into 'vitePlugin:{...}': ${invalidRootOptions.join(
', '
)}`
);
}
if (!config.vitePlugin) {
return config;
}
const pluginOptions = config.vitePlugin;
const pluginOptionKeys = Object.keys(pluginOptions);
const rootOptionsInPluginOptions = pluginOptionKeys.filter((key) => knownRootOptions.has(key));
if (rootOptionsInPluginOptions.length > 0) {
throw new Error(
`Invalid options in svelte config under vitePlugin:{...}', move them to the config root : ${rootOptionsInPluginOptions.join(
', '
)}`
);
}
const duplicateOptions = pluginOptionKeys.filter((key) =>
Object.prototype.hasOwnProperty.call(config, key)
);
if (duplicateOptions.length > 0) {
throw new Error(
`Invalid duplicate options in svelte config under vitePlugin:{...}', they are defined in root too and must only exist once: ${duplicateOptions.join(
', '
)}`
);
}
const unknownPluginOptions = pluginOptionKeys.filter((key) => !allowedPluginOptions.has(key));
if (unknownPluginOptions.length > 0) {
log.warn(
`ignoring unknown plugin options in svelte config under vitePlugin:{...}: ${unknownPluginOptions.join(
', '
)}`
);
unknownPluginOptions.forEach((unkownOption) => {
// @ts-expect-error not typed
delete pluginOptions[unkownOption];
});
}
/** @type {import('../public.d.ts').Options} */
const result = {
...config,
...pluginOptions
};
// @ts-expect-error it exists
delete result.vitePlugin;
return result;
}
/**
* used in config phase, merges the default options, svelte config, and inline options
* @param {Partial<import('../public.d.ts').Options> | undefined} inlineOptions
* @param {import('vite').UserConfig} viteUserConfig
* @param {import('vite').ConfigEnv} viteEnv
* @returns {Promise<import('../types/options.d.ts').PreResolvedOptions>}
*/
export async function preResolveOptions(inlineOptions, viteUserConfig, viteEnv) {
if (!inlineOptions) {
inlineOptions = {};
}
/** @type {import('vite').UserConfig} */
const viteConfigWithResolvedRoot = {
...viteUserConfig,
root: resolveViteRoot(viteUserConfig)
};
const isBuild = viteEnv.command === 'build';
/** @type {Partial<import('../types/options.d.ts').PreResolvedOptions>} */
const defaultOptions = {
extensions: DEFAULT_SVELTE_EXT,
emitCss: true,
prebundleSvelteLibraries: !isBuild
};
const svelteConfig = convertPluginOptions(
await loadSvelteConfig(viteConfigWithResolvedRoot, inlineOptions)
);
/** @type {Partial<import('../types/options.d.ts').PreResolvedOptions>} */
const extraOptions = {
root: viteConfigWithResolvedRoot.root,
isBuild,
isServe: viteEnv.command === 'serve',
isDebug: process.env.DEBUG != null
};
const merged = /** @type {import('../types/options.d.ts').PreResolvedOptions} */ (
mergeConfigs(defaultOptions, svelteConfig, inlineOptions, extraOptions)
);
// configFile of svelteConfig contains the absolute path it was loaded from,
// prefer it over the possibly relative inline path
if (svelteConfig?.configFile) {
merged.configFile = svelteConfig.configFile;
}
return merged;
}
/**
* @template T
* @param {(Partial<T> | undefined)[]} configs
* @returns T
*/
function mergeConfigs(...configs) {
/** @type {Partial<T>} */
let result = {};
for (const config of configs.filter((x) => x != null)) {
result = deepmerge(result, /** @type {Partial<T>} */ (config), {
// replace arrays
arrayMerge: (target, source) => source ?? target
});
}
return /** @type {T} */ result;
}
/**
* used in configResolved phase, merges a contextual default config, pre-resolved options, and some preprocessors. also validates the final config.
*
* @param {import('../types/options.d.ts').PreResolvedOptions} preResolveOptions
* @param {import('vite').ResolvedConfig} viteConfig
* @returns {import('../types/options.d.ts').ResolvedOptions}
*/
export function resolveOptions(preResolveOptions, viteConfig) {
const css = preResolveOptions.emitCss ? 'external' : 'injected';
/** @type {Partial<import('../public.d.ts').Options>} */
const defaultOptions = {
compilerOptions: {
css,
dev: !viteConfig.isProduction,
hmr:
!viteConfig.isProduction &&
!preResolveOptions.isBuild &&
viteConfig.server &&
viteConfig.server.hmr !== false
}
};
/** @type {Partial<import('../types/options.d.ts').ResolvedOptions>} */
const extraOptions = {
root: viteConfig.root,
isProduction: viteConfig.isProduction
};
const merged = /** @type {import('../types/options.d.ts').ResolvedOptions}*/ (
mergeConfigs(defaultOptions, preResolveOptions, extraOptions)
);
removeIgnoredOptions(merged);
handleDeprecatedOptions(merged);
addExtraPreprocessors(merged, viteConfig);
enforceOptionsForHmr(merged, viteConfig);
enforceOptionsForProduction(merged);
return merged;
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @param {import('vite').ResolvedConfig} viteConfig
*/
function enforceOptionsForHmr(options, viteConfig) {
if (options.hot) {
log.warn(
'svelte 5 has hmr integrated in core. Please remove the vitePlugin.hot option and use compilerOptions.hmr instead'
);
delete options.hot;
options.compilerOptions.hmr = true;
}
if (options.compilerOptions.hmr && viteConfig.server?.hmr === false) {
log.warn(
'vite config server.hmr is false but compilerOptions.hmr is true. Forcing compilerOptions.hmr to false as it would not work.'
);
options.compilerOptions.hmr = false;
}
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
*/
function enforceOptionsForProduction(options) {
if (options.isProduction) {
if (options.compilerOptions.hmr) {
log.warn(
'you are building for production but compilerOptions.hmr is true, forcing it to false'
);
options.compilerOptions.hmr = false;
}
if (options.compilerOptions.dev) {
log.warn(
'you are building for production but compilerOptions.dev is true, forcing it to false'
);
options.compilerOptions.dev = false;
}
}
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
*/
function removeIgnoredOptions(options) {
const ignoredCompilerOptions = ['generate', 'format', 'filename'];
if (options.compilerOptions.hmr && options.emitCss) {
ignoredCompilerOptions.push('cssHash');
}
const passedCompilerOptions = Object.keys(options.compilerOptions || {});
const passedIgnored = passedCompilerOptions.filter((o) => ignoredCompilerOptions.includes(o));
if (passedIgnored.length) {
log.warn(
`The following Svelte compilerOptions are controlled by vite-plugin-svelte and essential to its functionality. User-specified values are ignored. Please remove them from your configuration: ${passedIgnored.join(
', '
)}`
);
passedIgnored.forEach((ignored) => {
// @ts-expect-error string access
delete options.compilerOptions[ignored];
});
}
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
*/
function handleDeprecatedOptions(options) {
const experimental = /** @type {Record<string, any>} */ (options.experimental);
if (experimental) {
for (const promoted of ['prebundleSvelteLibraries', 'inspector', 'dynamicCompileOptions']) {
if (experimental[promoted]) {
//@ts-expect-error untyped assign
options[promoted] = experimental[promoted];
delete experimental[promoted];
log.warn(
`Option "experimental.${promoted}" is no longer experimental and has moved to "${promoted}". Please update your Svelte or Vite config.`
);
}
}
if (experimental.generateMissingPreprocessorSourcemaps) {
log.warn('experimental.generateMissingPreprocessorSourcemaps has been removed.');
}
}
}
/**
* vite passes unresolved `root`option to config hook but we need the resolved value, so do it here
*
* @see https://github.com/sveltejs/vite-plugin-svelte/issues/113
* @see https://github.com/vitejs/vite/blob/43c957de8a99bb326afd732c962f42127b0a4d1e/packages/vite/src/node/config.ts#L293
*
* @param {import('vite').UserConfig} viteConfig
* @returns {string | undefined}
*/
function resolveViteRoot(viteConfig) {
return normalizePath(viteConfig.root ? path.resolve(viteConfig.root) : process.cwd());
}
/**
* @param {import('../types/options.d.ts').PreResolvedOptions} options
* @param {import('vite').UserConfig} config
* @returns {Promise<Partial<import('vite').UserConfig>>}
*/
export async function buildExtraViteConfig(options, config) {
/** @type {Partial<import('vite').UserConfig>} */
const extraViteConfig = {
resolve: {
dedupe: [...SVELTE_IMPORTS]
}
// this option is still awaiting a PR in vite to be supported
// see https://github.com/sveltejs/vite-plugin-svelte/issues/60
// knownJsSrcExtensions: options.extensions
};
const extraSvelteConfig = buildExtraConfigForSvelte(config);
const extraDepsConfig = await buildExtraConfigForDependencies(options, config);
// merge extra svelte and deps config, but make sure dep values are not contradicting svelte
extraViteConfig.optimizeDeps = {
include: [
...extraSvelteConfig.optimizeDeps.include,
...extraDepsConfig.optimizeDeps.include.filter(
(dep) => !isDepExcluded(dep, extraSvelteConfig.optimizeDeps.exclude)
)
],
exclude: [
...extraSvelteConfig.optimizeDeps.exclude,
...extraDepsConfig.optimizeDeps.exclude.filter(
(dep) => !isDepIncluded(dep, extraSvelteConfig.optimizeDeps.include)
)
]
};
extraViteConfig.ssr = {
external: [
...extraSvelteConfig.ssr.external,
...extraDepsConfig.ssr.external.filter(
(dep) => !isDepNoExternaled(dep, extraSvelteConfig.ssr.noExternal)
)
],
noExternal: [
...extraSvelteConfig.ssr.noExternal,
...extraDepsConfig.ssr.noExternal.filter(
(dep) => !isDepExternaled(dep, extraSvelteConfig.ssr.external)
)
]
};
// enable hmrPartialAccept if not explicitly disabled
if (config.experimental?.hmrPartialAccept !== false) {
log.debug('enabling "experimental.hmrPartialAccept" in vite config', undefined, 'config');
extraViteConfig.experimental = { hmrPartialAccept: true };
}
validateViteConfig(extraViteConfig, config, options);
return extraViteConfig;
}
/**
* @param {Partial<import('vite').UserConfig>} extraViteConfig
* @param {import('vite').UserConfig} config
* @param {import('../types/options.d.ts').PreResolvedOptions} options
*/
function validateViteConfig(extraViteConfig, config, options) {
const { prebundleSvelteLibraries, isBuild } = options;
if (prebundleSvelteLibraries) {
/** @type {(option: 'dev' | 'build' | boolean)=> boolean} */
const isEnabled = (option) => option !== true && option !== (isBuild ? 'build' : 'dev');
/** @type {(name: string, value: 'dev' | 'build' | boolean, recommendation: string)=> void} */
const logWarning = (name, value, recommendation) =>
log.warn.once(
`Incompatible options: \`prebundleSvelteLibraries: true\` and vite \`${name}: ${JSON.stringify(
value
)}\` ${isBuild ? 'during build.' : '.'} ${recommendation}`
);
const viteOptimizeDepsDisabled = config.optimizeDeps?.disabled ?? 'build'; // fall back to vite default
const isOptimizeDepsEnabled = isEnabled(viteOptimizeDepsDisabled);
if (!isBuild && !isOptimizeDepsEnabled) {
logWarning(
'optimizeDeps.disabled',
viteOptimizeDepsDisabled,
'Forcing `optimizeDeps.disabled: "build"`. Disable prebundleSvelteLibraries or update your vite config to enable optimizeDeps during dev.'
);
if (!extraViteConfig.optimizeDeps) {
extraViteConfig.optimizeDeps = {};
}
extraViteConfig.optimizeDeps.disabled = 'build';
} else if (isBuild && isOptimizeDepsEnabled) {
logWarning(
'optimizeDeps.disabled',
viteOptimizeDepsDisabled,
'Disable optimizeDeps or prebundleSvelteLibraries for build if you experience errors.'
);
}
}
if (rolldownVersion && isBuild) {
// read user config inlineConst value
const inlineConst =
//@ts-ignore optimization only exists in vite-8 beta
config.build?.rolldownOptions?.optimization?.inlineConst ??
//@ts-ignore optimization only exists in vite-8 beta
config.build?.rollupOptions?.optimization?.inlineConst;
if (inlineConst === false) {
log.warn(
'Your rolldown config contains `optimization.inlineConst: false`. This can lead to increased bundle size and leaked server code in client build.'
);
}
}
}
/**
* @param {import('../types/options.d.ts').PreResolvedOptions} options
* @param {import('vite').UserConfig} config
* @returns {Promise<import('vitefu').CrawlFrameworkPkgsResult>}
*/
async function buildExtraConfigForDependencies(options, config) {
// extra handling for svelte dependencies in the project
const packagesWithoutSvelteExportsCondition = new Set();
const depsConfig = await crawlFrameworkPkgs({
root: options.root,
workspaceRoot: searchForWorkspaceRoot(options.root),
isBuild: options.isBuild,
viteUserConfig: config,
isFrameworkPkgByJson(pkgJson) {
let hasSvelteCondition = false;
if (typeof pkgJson.exports === 'object') {
// use replacer as a simple way to iterate over nested keys
JSON.stringify(pkgJson.exports, (key, value) => {
if (SVELTE_EXPORT_CONDITIONS.includes(key)) {
hasSvelteCondition = true;
}
return value;
});
}
const hasSvelteField = !!pkgJson.svelte;
if (hasSvelteField && !hasSvelteCondition) {
packagesWithoutSvelteExportsCondition.add(`${pkgJson.name}@${pkgJson.version}`);
}
return hasSvelteCondition || hasSvelteField;
},
isSemiFrameworkPkgByJson(pkgJson) {
return !!pkgJson.dependencies?.svelte || !!pkgJson.peerDependencies?.svelte;
},
isFrameworkPkgByName(pkgName) {
const isNotSveltePackage = isCommonDepWithoutSvelteField(pkgName);
if (isNotSveltePackage) {
return false;
} else {
return undefined;
}
}
});
if (
!options.isBuild &&
!options.experimental?.disableSvelteResolveWarnings &&
packagesWithoutSvelteExportsCondition?.size > 0
) {
log.info.once(
`The following packages have a svelte field in their package.json but no exports condition for svelte.\n\n${[
...packagesWithoutSvelteExportsCondition
].join('\n')}\n\nPlease see ${FAQ_LINK_MISSING_EXPORTS_CONDITION} for details.`
);
}
log.debug('extra config for dependencies generated by vitefu', depsConfig, 'config');
if (options.prebundleSvelteLibraries) {
// prebundling enabled, so we don't need extra dependency excludes
depsConfig.optimizeDeps.exclude = [];
// but keep dependency reinclusions of explicit user excludes
const userExclude = config.optimizeDeps?.exclude;
depsConfig.optimizeDeps.include = !userExclude
? []
: depsConfig.optimizeDeps.include.filter((dep) => {
// reincludes look like this: foo > bar > baz
// in case foo or bar are excluded, we have to retain the reinclude even with prebundling
return (
dep.includes('>') &&
dep
.split('>')
.slice(0, -1)
.some((d) => isDepExcluded(d.trim(), userExclude))
);
});
}
if (options.disableDependencyReinclusion === true) {
depsConfig.optimizeDeps.include = depsConfig.optimizeDeps.include.filter(
(dep) => !dep.includes('>')
);
} else if (Array.isArray(options.disableDependencyReinclusion)) {
const disabledDeps = options.disableDependencyReinclusion;
depsConfig.optimizeDeps.include = depsConfig.optimizeDeps.include.filter((dep) => {
if (!dep.includes('>')) return true;
const trimDep = dep.replace(/\s+/g, '');
return disabledDeps.some((disabled) => trimDep.includes(`${disabled}>`));
});
}
log.debug('post-processed extra config for dependencies', depsConfig, 'config');
return depsConfig;
}
/**
* @param {import('vite').UserConfig} config
* @returns {import('vite').UserConfig & { optimizeDeps: { include: string[], exclude:string[] }, ssr: { noExternal:(string|RegExp)[], external: string[] } } }
*/
function buildExtraConfigForSvelte(config) {
// include svelte imports for optimization unless explicitly excluded
/** @type {string[]} */
const include = [];
/** @type {string[]} */
const exclude = [];
if (!isDepExcluded('svelte', config.optimizeDeps?.exclude ?? [])) {
const svelteImportsToInclude = SVELTE_IMPORTS.filter(
(si) => !(si.endsWith('/server') || si.includes('/server/'))
);
svelteImportsToInclude.push(...SVELTE_RUNTIME_DEPENDENCIES.map((dep) => `svelte > ${dep}`));
log.debug(
`adding bare svelte packages and runtime dependencies to optimizeDeps.include: ${svelteImportsToInclude.join(', ')} `,
undefined,
'config'
);
include.push(...svelteImportsToInclude);
} else {
log.debug(
'"svelte" is excluded in optimizeDeps.exclude, skipped adding it to include.',
undefined,
'config'
);
}
/** @type {(string | RegExp)[]} */
const noExternal = [];
/** @type {string[]} */
const external = [];
// add svelte to ssr.noExternal unless it is present in ssr.external
// so it is correctly resolving according to the conditions in sveltes exports map
if (!isDepExternaled('svelte', config.ssr?.external ?? [])) {
noExternal.push('svelte', /^svelte\//);
}
// esm-env needs to be bundled by default for the development/production condition
// be properly used by svelte
if (!isDepExternaled('esm-env', config.ssr?.external ?? [])) {
noExternal.push('esm-env');
}
return { optimizeDeps: { include, exclude }, ssr: { noExternal, external } };
}
/**
* Mutates `config` to ensure `resolve.mainFields` is set. If unset, it emulates Vite's default fallback.
* @param {string} name
* @param {import('vite').EnvironmentOptions} config
* @param {{ isSsrTargetWebworker?: boolean }} opts
*/
export function ensureConfigEnvironmentMainFields(name, config, opts) {
config.resolve ??= {};
if (config.resolve.mainFields == null) {
if (config.consumer === 'client' || name === 'client' || opts.isSsrTargetWebworker) {
config.resolve.mainFields = [...defaultClientMainFields];
} else {
config.resolve.mainFields = [...defaultServerMainFields];
}
}
return true;
}
/**
* Mutates `config` to ensure `resolve.conditions` is set. If unset, it emulates Vite's default fallback.
* @param {string} name
* @param {import('vite').EnvironmentOptions} config
* @param {{ isSsrTargetWebworker?: boolean }} opts
*/
export function ensureConfigEnvironmentConditions(name, config, opts) {
config.resolve ??= {};
if (config.resolve.conditions == null) {
if (config.consumer === 'client' || name === 'client' || opts.isSsrTargetWebworker) {
config.resolve.conditions = [...defaultClientConditions];
} else {
config.resolve.conditions = [...defaultServerConditions];
}
}
}
/**
* @template T
* @param {T | T[] | null | undefined } value
* @returns {T[]}
*/
export function arraify(value) {
return value == null ? [] : Array.isArray(value) ? value : [value];
}
+89
View File
@@ -0,0 +1,89 @@
import { log } from './log.js';
import { LINK_TRANSFORM_WITH_PLUGIN } from './constants.js';
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @param {import('vite').ResolvedConfig} config
* @returns {{
* prependPreprocessors: import('svelte/compiler').PreprocessorGroup[],
* appendPreprocessors: import('svelte/compiler').PreprocessorGroup[]
* }}
*/
function buildExtraPreprocessors(options, config) {
/** @type {import('svelte/compiler').PreprocessorGroup[]} */
const prependPreprocessors = [];
/** @type {import('svelte/compiler').PreprocessorGroup[]} */
const appendPreprocessors = [];
/** @type {import('vite').Plugin[]} */
const pluginsWithPreprocessors = config.plugins.filter((p) => p?.api?.sveltePreprocess);
if (
!options.isBuild &&
!options.experimental?.disableApiSveltePreprocessWarnings &&
pluginsWithPreprocessors.length > 0
) {
log.info.once(
`The following plugins use the deprecated 'plugin.api.sveltePreprocess' field: ${pluginsWithPreprocessors
.map((p) => p.name)
.join(', ')}
Please contact their maintainers and ask them to use a vite plugin transform instead.
See ${LINK_TRANSFORM_WITH_PLUGIN} for more information.
`.replace(/\t+/g, '\t')
);
}
/** @type {import('vite').Plugin[]} */
const ignored = [];
/** @type {import('vite').Plugin[]} */
const included = [];
for (const p of pluginsWithPreprocessors) {
if (
options.ignorePluginPreprocessors === true ||
(Array.isArray(options.ignorePluginPreprocessors) &&
options.ignorePluginPreprocessors?.includes(p.name))
) {
ignored.push(p);
} else {
included.push(p);
}
}
if (ignored.length > 0) {
log.debug(
`Ignoring svelte preprocessors defined by these vite plugins: ${ignored
.map((p) => p.name)
.join(', ')}`,
undefined,
'preprocess'
);
}
if (included.length > 0) {
log.debug(
`Adding svelte preprocessors defined by these vite plugins: ${included
.map((p) => p.name)
.join(', ')}`,
undefined,
'preprocess'
);
appendPreprocessors.push(...pluginsWithPreprocessors.map((p) => p.api.sveltePreprocess));
}
return { prependPreprocessors, appendPreprocessors };
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @param {import('vite').ResolvedConfig} config
*/
export function addExtraPreprocessors(options, config) {
const { prependPreprocessors, appendPreprocessors } = buildExtraPreprocessors(options, config);
if (prependPreprocessors.length > 0 || appendPreprocessors.length > 0) {
if (!options.preprocess) {
options.preprocess = [...prependPreprocessors, ...appendPreprocessors];
} else if (Array.isArray(options.preprocess)) {
options.preprocess.unshift(...prependPreprocessors);
options.preprocess.push(...appendPreprocessors);
} else {
options.preprocess = [...prependPreprocessors, options.preprocess, ...appendPreprocessors];
}
}
}
+82
View File
@@ -0,0 +1,82 @@
import path from 'node:path';
import process from 'node:process';
const IS_WINDOWS = process.platform === 'win32';
/**
* @typedef {{
* file?: string;
* sources?: string[];
* sourceRoot?: string;
* }} SourceMapFileRefs
*/
/**
* convert absolute paths in sourcemap file refs to their relative equivalents to avoid leaking fs info
*
* map is modified in place.
*
* @param {SourceMapFileRefs | undefined} map sourcemap
* @param {string} filename absolute path to file the sourcemap is for
*/
export function mapToRelative(map, filename) {
if (!map) {
return;
}
const sourceRoot = map.sourceRoot;
const dirname = path.dirname(filename);
/** @type {(s: string) => string} */
const toRelative = (s) => {
if (!s) {
return s;
}
/** @type {string} */
let sourcePath;
if (s.startsWith('file:///')) {
// windows has file:///C:/foo and posix has file:///foo, so we have to remove one extra on windows
sourcePath = s.slice(IS_WINDOWS ? 8 : 7);
} else if (sourceRoot) {
const sep = sourceRoot[sourceRoot.length - 1] === '/' || s[0] === '/' ? '' : '/';
sourcePath = `${sourceRoot}${sep}${s}`;
} else {
sourcePath = s;
}
return path.isAbsolute(sourcePath) ? path.relative(dirname, sourcePath) : sourcePath;
};
if (map.file) {
map.file = path.basename(filename);
}
if (map.sources) {
map.sources = map.sources.map(toRelative);
}
if (map.sourceRoot) {
// we have prepended sourceRoot and computed relative paths from it
// remove it here to avoid downstream processing prepending it again
delete map.sourceRoot;
}
}
/**
* vitePreprocess uses an extra lang extension to tell vite about the type of preprocessor to use
* This function removes it afterwards to get back working file refs
*
* map is modified in place.
*
* @param {SourceMapFileRefs | undefined} map the output sourcemap
* @param {string} suffix the suffix to remove
*/
export function removeLangSuffix(map, suffix) {
if (!map) {
return;
}
/** @type {(s:string)=> string} */
const removeSuffix = (s) => (s?.endsWith(suffix) ? s.slice(0, -1 * suffix.length) : s);
if (map.file) {
map.file = removeSuffix(map.file);
}
if (map.sources) {
map.sources = map.sources.map(removeSuffix);
}
}
+46
View File
@@ -0,0 +1,46 @@
import { VERSION } from 'svelte/compiler';
/**
* @type {boolean}
*/
export const isSvelteWithAsync = gte(VERSION, '5.36.0');
/**
* split semver string and convert to number, ignores non digits in tag
* @param {string} semver
* @return {number[]} [major,minor,patch,tag]
*/
function splitToNumbers(semver) {
const num = semver
.replace(/[^\d.-]/g, '')
.split(/[.-]+/, 4)
.map(Number);
while (num.length < 3) {
num.push(0);
}
if (num.length < 4) {
num.push(Infinity);
}
return num;
}
/**
* compare semver versions, tags are compared by their numeric part only
*
* @param {string} a semver version
* @param {string} b semver version
* @return {boolean} true if a is greater or equal to b
*/
export function gte(a, b) {
const aNum = splitToNumbers(a);
const bNum = splitToNumbers(b);
for (let i = 0; i < aNum.length; i++) {
if (aNum[i] < bNum[i]) {
return false;
}
if (aNum[i] > bNum[i]) {
return true;
}
}
return true;
}
@@ -0,0 +1,248 @@
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 = {
// log after 500ms and more than one file processed
logInProgress: (c, now) => now - c.collectionStart > 500 && c.stats.length > 1,
// always log results
logResult: () => true
};
/**
* @param {number} n
* @returns
*/
function humanDuration(n) {
// 99.9ms 0.10s
return n < 100 ? `${n.toFixed(1)}ms` : `${(n / 1000).toFixed(2)}s`;
}
/**
* @param {import('../types/vite-plugin-svelte-stats.d.ts').PackageStats[]} pkgStats
* @returns {string}
*/
function formatPackageStats(pkgStats) {
const statLines = pkgStats.map((pkgStat) => {
const duration = pkgStat.duration;
const avg = duration / pkgStat.files;
return [pkgStat.pkg, `${pkgStat.files}`, humanDuration(duration), humanDuration(avg)];
});
statLines.unshift(['package', 'files', 'time', 'avg']);
const columnWidths = statLines.reduce(
(widths, row) => {
for (let i = 0; i < row.length; i++) {
const cell = row[i];
if (widths[i] < cell.length) {
widths[i] = cell.length;
}
}
return widths;
},
statLines[0].map(() => 0)
);
const table = statLines
.map((row) =>
row
.map((cell, i) => {
if (i === 0) {
return cell.padEnd(columnWidths[i], ' ');
} else {
return cell.padStart(columnWidths[i], ' ');
}
})
.join('\t')
)
.join('\n');
return table;
}
/**
* @class
*/
export class VitePluginSvelteStats {
/** @type {PackageInfo[]} */
#packageInfos = [];
/** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection[]} */
#collections = [];
/**
* @param {string} name
* @param {Partial<import('../types/vite-plugin-svelte-stats.d.ts').CollectionOptions>} [opts]
* @returns {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection}
*/
startCollection(name, opts) {
const options = {
...defaultCollectionOptions,
...opts
};
/** @type {import('../types/vite-plugin-svelte-stats.d.ts').Stat[]} */
const stats = [];
const collectionStart = performance.now();
const _this = this;
let hasLoggedProgress = false;
/** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection} */
const collection = {
name,
options,
stats,
collectionStart,
finished: false,
start(file) {
if (collection.finished) {
throw new Error('called after finish() has been used');
}
file = normalizePath(file);
const start = performance.now();
/** @type {import('../types/vite-plugin-svelte-stats.d.ts').Stat} */
const stat = { file, start, end: start };
return () => {
const now = performance.now();
stat.end = now;
stats.push(stat);
if (!hasLoggedProgress && options.logInProgress(collection, now)) {
hasLoggedProgress = true;
log.debug(`${name} in progress ...`, undefined, 'stats');
}
};
},
async finish() {
await _this.#finish(collection);
}
};
_this.#collections.push(collection);
return collection;
}
async finishAll() {
await Promise.all(this.#collections.map((c) => c.finish()));
}
/**
* @param {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection} collection
*/
async #finish(collection) {
try {
collection.finished = true;
const now = performance.now();
collection.duration = now - collection.collectionStart;
const logResult = collection.options.logResult(collection);
if (logResult) {
await this.#aggregateStatsResult(collection);
log.debug(
`${collection.name} done.\n${formatPackageStats(
/** @type {import('../types/vite-plugin-svelte-stats.d.ts').PackageStats[]}*/ (
collection.packageStats
)
)}`,
undefined,
'stats'
);
}
// cut some ties to free it for garbage collection
const index = this.#collections.indexOf(collection);
this.#collections.splice(index, 1);
collection.stats.length = 0;
collection.stats = [];
if (collection.packageStats) {
collection.packageStats.length = 0;
collection.packageStats = [];
}
collection.start = () => () => {};
collection.finish = () => {};
} catch (e) {
// this should not happen, but stats taking also should not break the process
log.debug.once(`failed to finish stats for ${collection.name}\n`, e, 'stats');
}
}
/**
* @param {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection} collection
*/
async #aggregateStatsResult(collection) {
const stats = collection.stats;
for (const stat of stats) {
stat.pkg = (await this.#getPackageInfo(stat.file)).name;
}
// group stats
/** @type {Record<string, import('../types/vite-plugin-svelte-stats.d.ts').PackageStats>} */
const grouped = {};
stats.forEach((stat) => {
const pkg = /** @type {string} */ (stat.pkg);
let group = grouped[pkg];
if (!group) {
group = grouped[pkg] = {
files: 0,
duration: 0,
pkg
};
}
group.files += 1;
group.duration += stat.end - stat.start;
});
const groups = Object.values(grouped);
groups.sort((a, b) => b.duration - a.duration);
collection.packageStats = groups;
}
/**
* @param {string} file
* @returns {Promise<PackageInfo>}
*/
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<PackageInfo>}
*/
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;
}
+95
View File
@@ -0,0 +1,95 @@
import fs from 'node:fs';
import { log } from './log.js';
import { knownSvelteConfigNames } from './load-svelte-config.js';
import path from 'node:path';
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {void}
*/
export function setupWatchers(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<string, Function[]>} */
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)));
}
});
}
/**
* taken from vite utils
* @param {import('vite').FSWatcher} watcher
* @param {string | null} file
* @param {string} root
* @returns {void}
*/
export function ensureWatchedFile(watcher, file, root) {
if (
file &&
// only need to watch if out of root
!file.startsWith(root + '/') &&
// some rollup plugins use null bytes for private resolved Ids
!file.includes('\0') &&
fs.existsSync(file)
) {
// resolve file to normalized system path
watcher.add(path.resolve(file));
}
}
+228
View File
@@ -0,0 +1,228 @@
declare module '@sveltejs/vite-plugin-svelte' {
import type { InlineConfig, ResolvedConfig } from 'vite';
import type { CompileOptions, Warning, PreprocessorGroup } from 'svelte/compiler';
import type { Options as InspectorOptions } from '@sveltejs/vite-plugin-svelte-inspector';
export type Options = Omit<SvelteConfig, 'vitePlugin'> & PluginOptionsInline;
interface PluginOptionsInline extends PluginOptions {
/**
* Path to a svelte config file, either absolute or relative to Vite root
*
* set to `false` to ignore the svelte config file
*
* @see https://vitejs.dev/config/#root
*/
configFile?: string | false;
}
export interface PluginOptions {
/**
* A `picomatch` pattern, or array of patterns, which specifies the files the plugin should
* operate on. By default, all svelte files are included.
*
* @see https://github.com/micromatch/picomatch
*/
include?: Arrayable<string | RegExp>;
/**
* A `picomatch` pattern, or array of patterns, which specifies the files to be ignored by the
* plugin. By default, no files are ignored.
*
* @see https://github.com/micromatch/picomatch
*/
exclude?: Arrayable<string | RegExp>;
/**
* Emit Svelte styles as virtual CSS files for Vite and other plugins to process
*
* @default true
*/
emitCss?: boolean;
/**
* Enable or disable Hot Module Replacement.
* Deprecated, use compilerOptions.hmr instead!
*
* @deprecated
* @default true for development, always false for production
*/
hot?: boolean;
/**
* Some Vite plugins can contribute additional preprocessors by defining `api.sveltePreprocess`.
* If you don't want to use them, set this to true to ignore them all or use an array of strings
* with plugin names to specify which.
*
* @default false
*/
ignorePluginPreprocessors?: boolean | string[];
/**
* vite-plugin-svelte automatically handles excluding svelte libraries and reinclusion of their dependencies
* in vite.optimizeDeps.
*
* `disableDependencyReinclusion: true` disables all reinclusions
* `disableDependencyReinclusion: ['foo','bar']` disables reinclusions for dependencies of foo and bar
*
* This should be used for hybrid packages that contain both node and browser dependencies, eg Routify
*
* @default false
*/
disableDependencyReinclusion?: boolean | string[];
/**
* Enable support for Vite's dependency optimization to prebundle Svelte libraries.
*
* To disable prebundling for a specific library, add it to `optimizeDeps.exclude`.
*
* @default true for dev, false for build
*/
prebundleSvelteLibraries?: boolean;
/**
* toggle/configure Svelte Inspector
*
* @default unset for dev, always false for build
*/
inspector?: InspectorOptions | boolean;
/**
* A function to update `compilerOptions` before compilation
*
* `data.filename` - The file to be compiled
* `data.code` - The preprocessed Svelte code
* `data.compileOptions` - The current compiler options
*
* To change part of the compiler options, return an object with the changes you need.
*
* @example
* ```
* ({ filename, compileOptions }) => {
* // Dynamically set runes mode per Svelte file
* if (forceRunesMode(filename) && !compileOptions.runes) {
* return { runes: true };
* }
* }
* ```
*/
dynamicCompileOptions?: (data: {
filename: string;
code: string;
compileOptions: Partial<CompileOptions>;
}) => Promise<Partial<CompileOptions> | void> | Partial<CompileOptions> | void;
/**
* These options are considered experimental and breaking changes to them can occur in any release
*/
experimental?: ExperimentalOptions;
}
export interface SvelteConfig {
/**
* A list of file extensions to be compiled by Svelte
*
* @default ['.svelte']
*/
extensions?: string[];
/**
* An array of preprocessors to transform the Svelte source code before compilation
*
* @see https://svelte.dev/docs/svelte/svelte-compiler#PreprocessorGroup
*/
preprocess?: Arrayable<PreprocessorGroup>;
/**
* The options to be passed to the Svelte compiler. A few options are set by default,
* including `dev` and `css`. However, some options are non-configurable, like
* `filename`, `format`, `generate`, and `cssHash` (in dev).
*
* @see https://svelte.dev/docs/svelte/svelte-compiler#CompileOptions
*/
compilerOptions?: Omit<CompileOptions, 'filename' | 'format' | 'generate'>;
/**
* Handles warning emitted from the Svelte compiler
*
* warnings emitted for files in node_modules are logged at the debug level, to see them run
* `DEBUG=vite-plugin-svelte:node-modules-onwarn pnpm build`
*
* @example
* ```
* (warning, defaultHandler) => {
* // ignore some warnings
* if (!['foo','bar'].includes(warning.code)) {
* defaultHandler(warning);
* }
* }
* ```
*
*/
onwarn?: (warning: Warning, defaultHandler: (warning: Warning) => void) => void;
/**
* Options for vite-plugin-svelte
*/
vitePlugin?: PluginOptions;
}
/**
* These options are considered experimental and breaking changes to them can occur in any release
*/
interface ExperimentalOptions {
/**
* send a websocket message with svelte compiler warnings during dev
*
*/
sendWarningsToBrowser?: boolean;
/**
* disable svelte field resolve warnings
*
* @default false
*/
disableSvelteResolveWarnings?: boolean;
/**
* disable api.sveltePreprocess deprecation warnings
*
* @default false
*/
disableApiSveltePreprocessWarnings?: boolean;
compileModule?: CompileModuleOptions;
}
interface CompileModuleOptions {
/**
* infix that must be present in filename
* @default ['.svelte.']
*/
infixes?: string[];
/**
* module extensions
* @default ['.ts','.js']
*/
extensions?: string[];
include?: Arrayable<string | RegExp>;
exclude?: Arrayable<string | RegExp>;
}
type Arrayable<T> = T | T[];
export interface VitePreprocessOptions {
/**
* preprocess script block with vite pipeline.
* Since svelte5 this is not needed for typescript anymore
*
* @default false
*/
script?: boolean;
/**
* preprocess style blocks with vite pipeline
*/
style?: boolean | InlineConfig | ResolvedConfig;
}
/**
* returns a list of plugins to handle svelte files
* plugins are named `vite-plugin-svelte:<task>`
*
* */
export function svelte(inlineOptions?: Partial<Options>): import("vite").Plugin[];
export function vitePreprocess(opts?: VitePreprocessOptions): import("svelte/compiler").PreprocessorGroup;
export function loadSvelteConfig(viteConfig?: import("vite").UserConfig, inlineOptions?: Partial<Options>): Promise<Partial<SvelteConfig> | undefined>;
export {};
}
//# sourceMappingURL=index.d.ts.map
+31
View File
@@ -0,0 +1,31 @@
{
"version": 3,
"file": "index.d.ts",
"names": [
"Options",
"PluginOptionsInline",
"PluginOptions",
"SvelteConfig",
"ExperimentalOptions",
"CompileModuleOptions",
"Arrayable",
"VitePreprocessOptions",
"svelte",
"vitePreprocess",
"loadSvelteConfig"
],
"sources": [
"../src/public.d.ts",
"../src/index.js",
"../src/preprocess.js",
"../src/utils/load-svelte-config.js"
],
"sourcesContent": [
null,
null,
null,
null
],
"mappings": ";;;;aAIYA,OAAOA;;WAETC,mBAAmBA;;;;;;;;;;;kBAWZC,aAAaA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAgGbC,YAAYA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAiDnBC,mBAAmBA;;;;;;;;;;;;;;;;;;;;;;;WAuBnBC,oBAAoBA;;;;;;;;;;;;;;;MAezBC,SAASA;;kBAEGC,qBAAqBA;;;;;;;;;;;;;;;;;;iBCvLtBC,MAAMA;iBCMNC,cAAcA;iBCFRC,gBAAgBA",
"ignoreList": []
}