feat: docker compose maybe

This commit is contained in:
2023-11-13 16:10:04 -05:00
parent 180b261e40
commit b625ccd8d6
8031 changed files with 2182966 additions and 0 deletions

21
node_modules/@sveltejs/vite-plugin-svelte/LICENSE generated vendored Normal file
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
node_modules/@sveltejs/vite-plugin-svelte/README.md generated vendored Normal file
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)

59
node_modules/@sveltejs/vite-plugin-svelte/package.json generated vendored Normal file
View File

@ -0,0 +1,59 @@
{
"name": "@sveltejs/vite-plugin-svelte",
"version": "2.5.2",
"license": "MIT",
"author": "dominikg",
"files": [
"src"
],
"type": "module",
"types": "src/index.d.ts",
"exports": {
".": {
"types": "./src/index.d.ts",
"import": "./src/index.js"
},
"./package.json": "./package.json"
},
"engines": {
"node": "^14.18.0 || >= 16"
},
"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": "^1.0.4",
"debug": "^4.3.4",
"deepmerge": "^4.3.1",
"kleur": "^4.1.5",
"magic-string": "^0.30.3",
"svelte-hmr": "^0.15.3",
"vitefu": "^0.2.4"
},
"peerDependencies": {
"svelte": "^3.54.0 || ^4.0.0 || ^5.0.0-next.0",
"vite": "^4.0.0"
},
"devDependencies": {
"@types/debug": "^4.1.8",
"esbuild": "^0.19.3",
"svelte": "^4.2.0",
"vite": "^4.4.9"
},
"scripts": {
"check:publint": "publint --strict",
"check:types": "tsc --noEmit"
}
}

View File

@ -0,0 +1,133 @@
import { log, logCompilerWarnings } from './utils/log.js';
import { toRollupError } from './utils/error.js';
/**
* Vite-specific HMR handling
*
* @param {Function} compileSvelte
* @param {import('vite').HmrContext} ctx
* @param {import('./types/id.d.ts').SvelteRequest} svelteRequest
* @param {import('./utils/vite-plugin-svelte-cache').VitePluginSvelteCache} cache
* @param {import('./types/options.d.ts').ResolvedOptions} options
* @returns {Promise<import('vite').ModuleNode[] | void>}
*/
export async function handleHotUpdate(compileSvelte, ctx, svelteRequest, cache, options) {
if (!cache.has(svelteRequest)) {
// file hasn't been requested yet (e.g. async component)
log.debug(`handleHotUpdate called before initial transform for ${svelteRequest.id}`);
return;
}
const { read, server, modules } = ctx;
const cachedJS = cache.getJS(svelteRequest);
const cachedCss = cache.getCSS(svelteRequest);
const content = await read();
/** @type {import('./types/compile.d.ts').CompileData} */
let compileData;
try {
compileData = await compileSvelte(svelteRequest, content, options);
cache.update(compileData);
} catch (e) {
cache.setError(svelteRequest, e);
throw toRollupError(e, options);
}
const affectedModules = [...modules];
const cssIdx = modules.findIndex((m) => m.id === svelteRequest.cssId);
if (cssIdx > -1) {
const cssUpdated = cssChanged(cachedCss, compileData.compiled.css);
if (!cssUpdated) {
log.debug(`skipping unchanged css for ${svelteRequest.cssId}`);
affectedModules.splice(cssIdx, 1);
}
}
const jsIdx = modules.findIndex((m) => m.id === svelteRequest.id);
if (jsIdx > -1) {
const jsUpdated = jsChanged(cachedJS, compileData.compiled.js, svelteRequest.filename);
if (!jsUpdated) {
log.debug(`skipping unchanged js for ${svelteRequest.id}`);
affectedModules.splice(jsIdx, 1);
// transform won't be called, log warnings here
logCompilerWarnings(svelteRequest, compileData.compiled.warnings, options);
}
}
// TODO is this enough? see also: https://github.com/vitejs/vite/issues/2274
const ssrModulesToInvalidate = affectedModules.filter((m) => !!m.ssrTransformResult);
if (ssrModulesToInvalidate.length > 0) {
log.debug(`invalidating modules ${ssrModulesToInvalidate.map((m) => m.id).join(', ')}`);
ssrModulesToInvalidate.forEach((moduleNode) => server.moduleGraph.invalidateModule(moduleNode));
}
if (affectedModules.length > 0) {
log.debug(
`handleHotUpdate for ${svelteRequest.id} result: ${affectedModules
.map((m) => m.id)
.join(', ')}`
);
}
return affectedModules;
}
/**
* @param {import('./types/compile.d.ts').Code} [prev]
* @param {import('./types/compile.d.ts').Code} [next]
* @returns {boolean}
*/
function cssChanged(prev, next) {
return !isCodeEqual(prev?.code, next?.code);
}
/**
* @param {import('./types/compile.d.ts').Code} [prev]
* @param {import('./types/compile.d.ts').Code} [next]
* @param {string} [filename]
* @returns {boolean}
*/
function jsChanged(prev, next, filename) {
const prevJs = prev?.code;
const nextJs = next?.code;
const isStrictEqual = isCodeEqual(prevJs, nextJs);
if (isStrictEqual) {
return false;
}
const isLooseEqual = isCodeEqual(normalizeJsCode(prevJs), normalizeJsCode(nextJs));
if (!isStrictEqual && isLooseEqual) {
log.warn(
`ignoring compiler output js change for ${filename} as it is equal to previous output after normalization`
);
}
return !isLooseEqual;
}
/**
* @param {string} [prev]
* @param {string} [next]
* @returns {boolean}
*/
function isCodeEqual(prev, next) {
if (!prev && !next) {
return true;
}
if ((!prev && next) || (prev && !next)) {
return false;
}
return prev === next;
}
/**
* remove code that only changes metadata and does not require a js update for the component to keep working
*
* 1) add_location() calls. These add location metadata to elements, only used by some dev tools
* 2) ... maybe more (or less) in the future
*
* @param {string} [code]
* @returns {string | undefined}
*/
function normalizeJsCode(code) {
if (!code) {
return code;
}
return code.replace(/\s*\badd_location\s*\([^)]*\)\s*;?/g, '');
}

View File

@ -0,0 +1,225 @@
import type { InlineConfig, ResolvedConfig, UserConfig, Plugin } from 'vite';
import type { CompileOptions, Warning } from 'svelte/types/compiler/interfaces';
import type { PreprocessorGroup } from 'svelte/types/compiler/preprocess';
import type { Options as InspectorOptions } from '@sveltejs/vite-plugin-svelte-inspector';
type Options = Omit<SvelteOptions, '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;
}
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>;
/**
* 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>;
/**
* Emit Svelte styles as virtual CSS files for Vite and other plugins to process
*
* @default true
*/
emitCss?: boolean;
/**
* Enable or disable Hot Module Replacement.
*
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
*
* DO NOT CUSTOMIZE SVELTE-HMR OPTIONS UNLESS YOU KNOW EXACTLY WHAT YOU ARE DOING
*
* YOU HAVE BEEN WARNED
*
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
*
* Set an object to pass custom options to svelte-hmr
*
* @see https://github.com/rixo/svelte-hmr#options
* @default true for development, always false for production
*/
hot?:
| boolean
| {
injectCss?: boolean;
partialAccept?: boolean;
[key: string]: any;
};
/**
* 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;
/**
* These options are considered experimental and breaking changes to them can occur in any release
*/
experimental?: ExperimentalOptions;
}
interface SvelteOptions {
/**
* 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_preprocess
*/
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_compile
*/
compilerOptions?: Omit<CompileOptions, 'filename' | 'format' | 'generate'>;
/**
* Handles warning emitted from the Svelte compiler
*/
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 {
/**
* 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 hydration per Svelte file
* if (compileWithHydratable(filename) && !compileOptions.hydratable) {
* return { hydratable: true };
* }
* }
* ```
*/
dynamicCompileOptions?: (data: {
filename: string;
code: string;
compileOptions: Partial<CompileOptions>;
}) => Promise<Partial<CompileOptions> | void> | Partial<CompileOptions> | void;
/**
* send a websocket message with svelte compiler warnings during dev
*
*/
sendWarningsToBrowser?: boolean;
/**
* disable svelte field resolve warnings
*
* @default false
*/
disableSvelteResolveWarnings?: boolean;
/**
* Options for compiling Svelte JS/TS modules
*/
compileModule?: CompileModuleOptions;
}
interface CompileModuleOptions {
extensions?: string[];
include?: Arrayable<string>;
exclude?: Arrayable<string>;
}
type ModuleFormat = NonNullable<'esm'>;
type CssHashGetter = NonNullable<CompileOptions['cssHash']>;
type Arrayable<T> = T | T[];
interface VitePreprocessOptions {
script?: boolean;
style?: boolean | InlineConfig | ResolvedConfig;
}
declare function vitePreprocess(opts?: VitePreprocessOptions): PreprocessorGroup;
declare function loadSvelteConfig(
viteConfig?: UserConfig,
inlineOptions?: Partial<Options>
): Promise<Partial<SvelteOptions> | undefined>;
declare function svelte(inlineOptions?: Partial<Options>): Plugin[];
export {
Arrayable,
CssHashGetter,
ModuleFormat,
Options,
PluginOptions,
SvelteOptions,
loadSvelteConfig,
svelte,
VitePreprocessOptions,
vitePreprocess
};
// reexported types
export { CompileOptions, Warning } from 'svelte/types/compiler/interfaces';
export {
MarkupPreprocessor,
Preprocessor,
PreprocessorGroup,
Processed
} from 'svelte/types/compiler/preprocess';

317
node_modules/@sveltejs/vite-plugin-svelte/src/index.js generated vendored Normal file
View File

@ -0,0 +1,317 @@
import fs from 'node:fs';
import { version as viteVersion } from 'vite';
import * as svelteCompiler from 'svelte/compiler';
import { svelteInspector } from '@sveltejs/vite-plugin-svelte-inspector';
import { isDepExcluded } from 'vitefu';
import { handleHotUpdate } from './handle-hot-update.js';
import { log, logCompilerWarnings } from './utils/log.js';
import { createCompileSvelte } from './utils/compile.js';
import { buildIdParser, buildModuleIdParser } from './utils/id.js';
import {
buildExtraViteConfig,
validateInlineOptions,
resolveOptions,
patchResolvedViteConfig,
preResolveOptions
} from './utils/options.js';
import { ensureWatchedFile, setupWatchers } from './utils/watch.js';
import { resolveViaPackageJsonSvelte } from './utils/resolve.js';
import { toRollupError } from './utils/error.js';
import { saveSvelteMetadata } from './utils/optimizer.js';
import { VitePluginSvelteCache } from './utils/vite-plugin-svelte-cache.js';
import { loadRaw } from './utils/load-raw.js';
import { FAQ_LINK_CONFLICTS_IN_SVELTE_RESOLVE } from './utils/constants.js';
import { isSvelte3, isSvelte5 } from './utils/svelte-version.js';
const isVite4_0 = viteVersion.startsWith('4.0');
/** @type {import('./index.d.ts').svelte} */
export function svelte(inlineOptions) {
if (process.env.DEBUG != null) {
log.setLevel('debug');
}
validateInlineOptions(inlineOptions);
const cache = new VitePluginSvelteCache();
// updated in configResolved hook
/** @type {import('./types/id.d.ts').IdParser} */
let requestParser;
/** @type {import('./types/id.d.ts').ModuleIdParser} */
let moduleRequestParser;
/** @type {import('./types/options.d.ts').ResolvedOptions} */
let options;
/** @type {import('vite').ResolvedConfig} */
let viteConfig;
/** @type {import('./types/compile.d.ts').CompileSvelte} */
let compileSvelte;
/* eslint-enable no-unused-vars */
/** @type {Promise<import('vite').Rollup.PartialResolvedId | null>} */
let resolvedSvelteSSR;
/** @type {Set<string>} */
let packagesWithResolveWarnings;
/** @type {import('./types/plugin-api.d.ts').PluginAPI} */
const api = {};
/** @type {import('vite').Plugin[]} */
const plugins = [
{
name: 'vite-plugin-svelte',
// make sure our resolver runs before vite internal resolver to resolve svelte field correctly
enforce: 'pre',
api,
async config(config, configEnv) {
// setup logger
if (process.env.DEBUG) {
log.setLevel('debug');
} else if (config.logLevel) {
log.setLevel(config.logLevel);
}
// @ts-expect-error temporarily lend the options variable until fixed in configResolved
options = await preResolveOptions(inlineOptions, config, configEnv);
// extra vite config
const extraViteConfig = await buildExtraViteConfig(options, config);
log.debug('additional vite config', extraViteConfig);
return extraViteConfig;
},
async configResolved(config) {
options = resolveOptions(options, config, cache);
patchResolvedViteConfig(config, options);
requestParser = buildIdParser(options);
compileSvelte = createCompileSvelte(options);
viteConfig = config;
// TODO deep clone to avoid mutability from outside?
api.options = options;
log.debug('resolved options', options);
},
async buildStart() {
packagesWithResolveWarnings = new Set();
if (!options.prebundleSvelteLibraries) return;
const isSvelteMetadataChanged = await saveSvelteMetadata(viteConfig.cacheDir, options);
if (isSvelteMetadataChanged) {
// Force Vite to optimize again. Although we mutate the config here, it works because
// Vite's optimizer runs after `buildStart()`.
viteConfig.optimizeDeps.force = true;
}
},
configureServer(server) {
options.server = server;
setupWatchers(options, cache, requestParser);
},
async load(id, opts) {
const ssr = !!opts?.ssr;
const svelteRequest = requestParser(id, !!ssr);
if (svelteRequest) {
const { filename, query, raw } = svelteRequest;
if (raw) {
return loadRaw(svelteRequest, compileSvelte, options);
} else {
if (query.svelte && query.type === 'style') {
const css = cache.getCSS(svelteRequest);
if (css) {
log.debug(`load returns css for ${filename}`);
return css;
}
}
// prevent vite asset plugin from loading files as url that should be compiled in transform
if (viteConfig.assetsInclude(filename)) {
log.debug(`load returns raw content for ${filename}`);
return fs.readFileSync(filename, 'utf-8');
}
}
}
},
async resolveId(importee, importer, opts) {
const ssr = !!opts?.ssr;
const svelteRequest = requestParser(importee, ssr);
if (svelteRequest?.query.svelte) {
if (svelteRequest.query.type === 'style' && !svelteRequest.raw) {
// return cssId with root prefix so postcss pipeline of vite finds the directory correctly
// see https://github.com/sveltejs/vite-plugin-svelte/issues/14
log.debug(`resolveId resolved virtual css module ${svelteRequest.cssId}`);
return svelteRequest.cssId;
}
}
// TODO: remove this after bumping peerDep on Vite to 4.1+ or Svelte to 4.0+
if (isVite4_0 && isSvelte3 && ssr && importee === 'svelte') {
if (!resolvedSvelteSSR) {
resolvedSvelteSSR = this.resolve('svelte/ssr', undefined, { skipSelf: true }).then(
(svelteSSR) => {
log.debug('resolved svelte to svelte/ssr');
return svelteSSR;
},
(err) => {
log.debug(
'failed to resolve svelte to svelte/ssr. Update svelte to a version that exports it',
err
);
return null; // returning null here leads to svelte getting resolved regularly
}
);
}
return resolvedSvelteSSR;
}
//@ts-expect-error scan
const scan = !!opts?.scan; // scanner phase of optimizeDeps
const isPrebundled =
options.prebundleSvelteLibraries &&
viteConfig.optimizeDeps?.disabled !== true &&
viteConfig.optimizeDeps?.disabled !== (options.isBuild ? 'build' : 'dev') &&
!isDepExcluded(importee, viteConfig.optimizeDeps?.exclude ?? []);
// for prebundled libraries we let vite resolve the prebundling result
// for ssr, during scanning and non-prebundled, we do it
if (ssr || scan || !isPrebundled) {
try {
const isFirstResolve = !cache.hasResolvedSvelteField(importee, importer);
const resolved = await resolveViaPackageJsonSvelte(importee, importer, cache);
if (isFirstResolve && resolved) {
const packageInfo = await cache.getPackageInfo(resolved);
const packageVersion = `${packageInfo.name}@${packageInfo.version}`;
log.debug.once(
`resolveId resolved ${importee} to ${resolved} via package.json svelte field of ${packageVersion}`
);
try {
const viteResolved = (
await this.resolve(importee, importer, { ...opts, skipSelf: true })
)?.id;
if (resolved !== viteResolved) {
packagesWithResolveWarnings.add(packageVersion);
log.debug.enabled &&
log.debug.once(
`resolve difference for ${packageVersion} ${importee} - svelte: "${resolved}", vite: "${viteResolved}"`
);
}
} catch (e) {
packagesWithResolveWarnings.add(packageVersion);
log.debug.enabled &&
log.debug.once(
`resolve error for ${packageVersion} ${importee} - svelte: "${resolved}", vite: ERROR`,
e
);
}
}
return resolved;
} catch (e) {
log.debug.once(
`error trying to resolve ${importee} from ${importer} via package.json svelte field `,
e
);
// this error most likely happens due to non-svelte related importee/importers so swallow it here
// in case it really way a svelte library, users will notice anyway. (lib not working due to failed resolve)
}
}
},
async transform(code, id, opts) {
const ssr = !!opts?.ssr;
const svelteRequest = requestParser(id, ssr);
if (!svelteRequest || svelteRequest.query.type === 'style' || svelteRequest.raw) {
return;
}
let compileData;
try {
compileData = await compileSvelte(svelteRequest, code, options);
} catch (e) {
cache.setError(svelteRequest, e);
throw toRollupError(e, options);
}
logCompilerWarnings(svelteRequest, compileData.compiled.warnings, options);
cache.update(compileData);
if (compileData.dependencies?.length) {
if (options.server) {
for (const dep of compileData.dependencies) {
ensureWatchedFile(options.server.watcher, dep, options.root);
}
} else if (options.isBuild && viteConfig.build.watch) {
for (const dep of compileData.dependencies) {
this.addWatchFile(dep);
}
}
}
log.debug(`transform returns compiled js for ${svelteRequest.filename}`);
return {
...compileData.compiled.js,
meta: {
vite: {
lang: compileData.lang
}
}
};
},
handleHotUpdate(ctx) {
if (!options.hot || !options.emitCss) {
return;
}
const svelteRequest = requestParser(ctx.file, false, ctx.timestamp);
if (svelteRequest) {
return handleHotUpdate(compileSvelte, ctx, svelteRequest, cache, options);
}
},
async buildEnd() {
await options.stats?.finishAll();
if (
!options.experimental?.disableSvelteResolveWarnings &&
packagesWithResolveWarnings?.size > 0
) {
log.warn(
`WARNING: The following packages use a svelte resolve configuration in package.json that has conflicting results and is going to cause problems future.\n\n${[
...packagesWithResolveWarnings
].join('\n')}\n\nPlease see ${FAQ_LINK_CONFLICTS_IN_SVELTE_RESOLVE} for details.`
);
}
}
}
];
if (!isSvelte5) {
plugins.push(svelteInspector()); // TODO reenable once svelte5 has support
}
if (isSvelte5) {
log.warn(
'svelte 5 support in v-p-s is experimental, breaking changes can occur in any release until this notice is removed'
);
log.warn('svelte 5 does not support svelte-inspector yet, disabling it');
// TODO move to separate file
plugins.push({
name: 'vite-plugin-svelte-module',
enforce: 'post',
async configResolved() {
moduleRequestParser = buildModuleIdParser(options);
},
async transform(code, id, opts) {
const ssr = !!opts?.ssr;
const moduleRequest = moduleRequestParser(id, ssr);
if (!moduleRequest) {
return;
}
try {
// @ts-ignore doesn't exist in Svelte 4
const compileResult = await svelteCompiler.compileModule(code, {
generate: ssr ? 'server' : 'client',
filename: moduleRequest.filename
});
logCompilerWarnings(moduleRequest, compileResult.warnings, options);
return compileResult.js;
} catch (e) {
throw toRollupError(e, options);
}
}
});
}
return plugins;
}
export { vitePreprocess } from './preprocess.js';
export { loadSvelteConfig } from './utils/load-svelte-config.js';

View File

@ -0,0 +1,118 @@
import { preprocessCSS, resolveConfig, transformWithEsbuild } from 'vite';
import { mapToRelative, removeLangSuffix } from './utils/sourcemaps.js';
/**
* @typedef {(code: string, filename: string) => Promise<{ code: string; map?: any; deps?: Set<string> }>} CssTransform
*/
const supportedStyleLangs = ['css', 'less', 'sass', 'scss', 'styl', 'stylus', 'postcss', 'sss'];
const supportedScriptLangs = ['ts'];
export const lang_sep = '.vite-preprocess.';
/** @type {import('./index.d.ts').vitePreprocess} */
export function vitePreprocess(opts) {
/** @type {import('svelte/types/compiler/preprocess').PreprocessorGroup} */
const preprocessor = {};
if (opts?.script !== false) {
preprocessor.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/types/compiler/preprocess').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
};
}
};
}
/**
* @param {import('vite').ResolvedConfig | import('vite').InlineConfig} config
* @returns {{ style: import('svelte/types/compiler/preprocess').Preprocessor }}
*/
function viteStyle(config = {}) {
/** @type {CssTransform} */
let transform;
/** @type {import('svelte/types/compiler/preprocess').Preprocessor} */
const style = async ({ attributes, content, filename = '' }) => {
const lang = /** @type {string} */ (attributes.lang);
if (!supportedStyleLangs.includes(lang)) return;
if (!transform) {
/** @type {import('vite').ResolvedConfig} */
let resolvedConfig;
// @ts-expect-error special prop added if running in v-p-s
if (style.__resolvedConfig) {
// @ts-expect-error
resolvedConfig = style.__resolvedConfig;
} else if (isResolvedConfig(config)) {
resolvedConfig = config;
} else {
resolvedConfig = await resolveConfig(
config,
process.env.NODE_ENV === 'production' ? 'build' : 'serve'
);
}
transform = getCssTransformFn(resolvedConfig);
}
const suffix = `${lang_sep}${lang}`;
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('vite').ResolvedConfig} config
* @returns {CssTransform}
*/
function getCssTransformFn(config) {
return async (code, filename) => {
return preprocessCSS(code, filename, config);
};
}
/**
* @param {any} config
* @returns {config is import('vite').ResolvedConfig}
*/
function isResolvedConfig(config) {
return !!config.inlineConfig;
}

View File

@ -0,0 +1,48 @@
import type { Processed } from 'svelte/types/compiler/preprocess';
import type { SvelteRequest } from './id.d.ts';
import type { ResolvedOptions } from './options.d.ts';
export type CompileSvelte = (
svelteRequest: SvelteRequest,
code: string,
options: Partial<ResolvedOptions>
) => Promise<CompileData>;
export interface Code {
code: string;
map?: any;
dependencies?: any[];
}
export interface Compiled {
js: Code;
css: Code;
ast: any; // TODO type
warnings: any[]; // TODO type
vars: Array<{
name: string;
export_name: string;
injected: boolean;
module: boolean;
mutated: boolean;
reassigned: boolean;
referenced: boolean;
writable: boolean;
referenced_from_script: boolean;
}>;
stats: {
timings: {
total: number;
};
};
}
export interface CompileData {
filename: string;
normalizedFilename: string;
lang: string;
compiled: Compiled;
ssr: boolean | undefined;
dependencies: string[];
preprocessed: Processed;
}

View File

@ -0,0 +1,46 @@
import type { CompileOptions } from 'svelte/types/compiler/interfaces';
export type SvelteQueryTypes = 'style' | 'script' | 'preprocessed' | 'all';
export interface RequestQuery {
// our own
svelte?: boolean;
type?: SvelteQueryTypes;
sourcemap?: boolean;
compilerOptions?: Pick<
CompileOptions,
'generate' | 'dev' | 'css' | 'hydratable' | 'customElement' | 'immutable' | 'enableSourcemap'
>;
// vite specific
url?: boolean;
raw?: boolean;
direct?: 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 ModuleIdParser = (
id: string,
ssr: boolean,
timestamp?: number
) => SvelteModuleRequest | undefined;

View File

@ -0,0 +1,24 @@
import type { Warning } from '../index.d.ts';
export interface LogFn extends SimpleLogFn {
(message: string, payload?: any, namespace?: string): void;
enabled: boolean;
once: SimpleLogFn;
}
export interface SimpleLogFn {
(message: string, payload?: any, 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';

View File

@ -0,0 +1,20 @@
import type { CompileOptions } from 'svelte/types/compiler/interfaces';
import type { ViteDevServer } from 'vite';
import { VitePluginSvelteStats } from '../utils/vite-plugin-svelte-stats.js';
import type { Options } from '../index.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;
}

View File

@ -0,0 +1,11 @@
import type { ResolvedOptions } from './options.d.ts';
export interface PluginAPI {
/**
* must not be modified, should not be used outside of vite-plugin-svelte repo
* @internal
* @experimental
*/
options?: ResolvedOptions;
// TODO expose compile cache here so other utility plugins can use it
}

View File

@ -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;
}

View File

@ -0,0 +1,207 @@
import * as svelte from 'svelte/compiler';
// @ts-ignore
import { createMakeHot } from 'svelte-hmr';
import { safeBase64Hash } from './hash.js';
import { log } from './log.js';
import { createInjectScopeEverythingRulePreprocessorGroup } from './preprocess.js';
import { mapToRelative } from './sourcemaps.js';
const scriptLangRE = /<script [^>]*lang=["']?([^"' >]+)["']?[^>]*>/;
import { isSvelte3, isSvelte5 } from './svelte-version.js';
/**
* @param {Function} [makeHot]
* @returns {import('../types/compile.d.ts').CompileSvelte}
*/
export const _createCompileSvelte = (makeHot) => {
/** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */
let stats;
const devStylePreprocessor = createInjectScopeEverythingRulePreprocessorGroup();
/** @type {import('../types/compile.d.ts').CompileSvelte} */
return async function compileSvelte(svelteRequest, code, options) {
const { filename, normalizedFilename, cssId, ssr, raw } = svelteRequest;
const { emitCss = true } = options;
const dependencies = [];
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('../index.d.ts').CompileOptions} */
const compileOptions = {
...options.compilerOptions,
filename,
// @ts-expect-error generate type is different for svelte5
generate: isSvelte5 ? (ssr ? 'server' : 'client') : ssr ? 'ssr' : 'dom'
};
if (isSvelte3) {
// @ts-ignore
compileOptions.format = 'esm';
}
if (options.hot && options.emitCss) {
const hash = `s-${safeBase64Hash(normalizedFilename)}`;
log.debug(`setting cssHash ${hash} for ${normalizedFilename}`);
compileOptions.cssHash = () => hash;
}
if (ssr && compileOptions.enableSourcemap !== false) {
if (typeof compileOptions.enableSourcemap === 'object') {
compileOptions.enableSourcemap.css = false;
} else {
compileOptions.enableSourcemap = { js: true, css: false };
}
}
let preprocessed;
let preprocessors = options.preprocess;
if (!options.isBuild && options.emitCss && options.hot) {
// inject preprocessor that ensures css hmr works better
if (!Array.isArray(preprocessors)) {
preprocessors = preprocessors
? [preprocessors, devStylePreprocessor]
: [devStylePreprocessor];
} else {
preprocessors = preprocessors.concat(devStylePreprocessor);
}
}
if (preprocessors) {
try {
preprocessed = await svelte.preprocess(code, preprocessors, { filename }); // full filename here so postcss works
} catch (e) {
e.message = `Error while preprocessing ${filename}${e.message ? ` - ${e.message}` : ''}`;
throw e;
}
if (preprocessed.dependencies) dependencies.push(...preprocessed.dependencies);
if (preprocessed.map) compileOptions.sourcemap = preprocessed.map;
}
if (typeof preprocessed?.map === 'object') {
mapToRelative(preprocessed?.map, filename);
}
if (raw && svelteRequest.query.type === 'preprocessed') {
// @ts-expect-error shortcut
return /** @type {import('../types/compile.d.ts').CompileData} */ {
preprocessed: preprocessed ?? { code }
};
}
const finalCode = preprocessed ? preprocessed.code : code;
const dynamicCompileOptions = await options.experimental?.dynamicCompileOptions?.({
filename,
code: finalCode,
compileOptions
});
if (dynamicCompileOptions && log.debug.enabled) {
log.debug(
`dynamic compile options for ${filename}: ${JSON.stringify(dynamicCompileOptions)}`
);
}
const finalCompileOptions = dynamicCompileOptions
? {
...compileOptions,
...dynamicCompileOptions
}
: compileOptions;
const endStat = stats?.start(filename);
const compiled = svelte.compile(finalCode, finalCompileOptions);
if (isSvelte3) {
// prevent dangling pure comments
// see https://github.com/sveltejs/kit/issues/9492#issuecomment-1487704985
// uses regex replace with whitespace to keep sourcemap/character count unmodified
compiled.js.code = compiled.js.code.replace(
/\/\* [@#]__PURE__ \*\/(\s*)$/gm,
' $1'
);
}
if (endStat) {
endStat();
}
mapToRelative(compiled.js?.map, filename);
mapToRelative(compiled.css?.map, filename);
if (!raw) {
// wire css import and code for hmr
const hasCss = compiled.css?.code?.trim().length > 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`;
}
// only apply hmr when not in ssr context and hot options are set
if (!ssr && makeHot) {
compiled.js.code = makeHot({
id: filename,
compiledCode: compiled.js.code,
//@ts-expect-error hot isn't a boolean at this point
hotOptions: { ...options.hot, injectCss: options.hot?.injectCss === true && hasCss },
compiled,
originalCode: code,
compileOptions: finalCompileOptions
});
}
}
return {
filename,
normalizedFilename,
lang: code.match(scriptLangRE)?.[1] || 'js',
// @ts-ignore
compiled,
ssr,
dependencies,
preprocessed: preprocessed ?? { code }
};
};
};
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {Function | undefined}
*/
function buildMakeHot(options) {
const needsMakeHot = options.hot !== false && options.isServe && !options.isProduction;
if (needsMakeHot) {
// @ts-ignore
const hotApi = options?.hot?.hotApi;
// @ts-ignore
const adapter = options?.hot?.adapter;
return createMakeHot({
// TODO Svelte 5 doesn't expose walk anymore. If we decide to make v-p-s 2 work with Svelte 5 HMR, we need to import walk from estree-walker
walk: svelte.walk,
hotApi,
adapter,
hotOptions: { noOverlay: true, .../** @type {object} */ (options.hot) }
});
}
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {import('../types/compile.d.ts').CompileSvelte}
*/
export function createCompileSvelte(options) {
const makeHot = buildMakeHot(options);
return _createCompileSvelte(makeHot);
}

View File

@ -0,0 +1,31 @@
import { isSvelte3 } from './svelte-version.js';
export const VITE_RESOLVE_MAIN_FIELDS = ['module', 'jsnext:main', 'jsnext'];
export const SVELTE_RESOLVE_MAIN_FIELDS = ['svelte'];
export const SVELTE_IMPORTS = [
'svelte/animate',
'svelte/easing',
'svelte/internal',
'svelte/motion',
'svelte/ssr',
'svelte/store',
'svelte/transition',
'svelte'
];
// TODO add to global list after dropping svelte 3
if (!isSvelte3) {
SVELTE_IMPORTS.push('svelte/internal/disclose-version');
}
export const SVELTE_HMR_IMPORTS = [
'svelte-hmr/runtime/hot-api-esm.js',
'svelte-hmr/runtime/proxy-adapter-dom.js',
'svelte-hmr'
];
export const SVELTE_EXPORT_CONDITIONS = ['svelte'];
export const FAQ_LINK_CONFLICTS_IN_SVELTE_RESOLVE =
'https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/faq.md#conflicts-in-svelte-resolve';

View File

@ -0,0 +1,90 @@
import path from 'node:path';
import fs from 'node:fs/promises';
import { findDepPkgJsonPath } from 'vitefu';
/**
* @typedef {{
* dir: string;
* pkg: Record<string, any>;
* }} DependencyData
*/
/**
* @param {string} dep
* @param {string} parent
* @returns {Promise<DependencyData | undefined>}
*/
export async function resolveDependencyData(dep, parent) {
const depDataPath = await findDepPkgJsonPath(dep, parent);
if (!depDataPath) return undefined;
try {
return {
dir: path.dirname(depDataPath),
pkg: JSON.parse(await fs.readFile(depDataPath, 'utf-8'))
};
} catch {
return undefined;
}
}
const COMMON_DEPENDENCIES_WITHOUT_SVELTE_FIELD = [
'@lukeed/uuid',
'@playwright/test',
'@sveltejs/kit',
'@sveltejs/package',
'@sveltejs/vite-plugin-svelte',
'autoprefixer',
'cookie',
'dotenv',
'esbuild',
'eslint',
'jest',
'mdsvex',
'playwright',
'postcss',
'prettier',
'svelte',
'svelte2tsx',
'svelte-check',
'svelte-hmr',
'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/
)
);
}

View File

@ -0,0 +1,102 @@
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/types/compiler/interfaces').Warning & Error} 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/types/compiler/interfaces').Warning & Error} error a svelte compiler error, which is a mix of Warning and an error
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {import('esbuild').PartialMessage} the converted error
*/
export function toESBuildError(error, options) {
const { filename, frame, start, stack } = error;
/** @type {import('esbuild').PartialMessage} */
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');
}

View File

@ -0,0 +1,133 @@
import { readFileSync } from 'node:fs';
import * as svelte from 'svelte/compiler';
import { log } from './log.js';
import { toESBuildError } from './error.js';
import { isSvelte3, isSvelte5 } from './svelte-version.js';
/**
* @typedef {NonNullable<import('vite').DepOptimizationOptions['esbuildOptions']>} EsbuildOptions
* @typedef {NonNullable<EsbuildOptions['plugins']>[number]} EsbuildPlugin
*/
export const facadeEsbuildSveltePluginName = 'vite-plugin-svelte:facade';
const svelteModuleExtension = '.svelte.js';
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {EsbuildPlugin}
*/
export function esbuildSveltePlugin(options) {
return {
name: 'vite-plugin-svelte:optimize-svelte',
setup(build) {
// Skip in scanning phase as Vite already handles scanning Svelte files.
// Otherwise this would heavily slow down the scanning phase.
if (build.initialOptions.plugins?.some((v) => v.name === 'vite:dep-scan')) return;
const svelteExtensions = (options.extensions ?? ['.svelte']).map((ext) => ext.slice(1));
if (isSvelte5) {
svelteExtensions.push(svelteModuleExtension.slice(1));
}
const svelteFilter = new RegExp('\\.(' + svelteExtensions.join('|') + ')(\\?.*)?$');
/** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */
let statsCollection;
build.onStart(() => {
statsCollection = options.stats?.startCollection('prebundle libraries', {
logResult: (c) => c.stats.length > 1
});
});
build.onLoad({ filter: svelteFilter }, async ({ path: filename }) => {
const code = readFileSync(filename, 'utf8');
try {
const contents = await compileSvelte(options, { filename, code }, statsCollection);
return { contents };
} catch (e) {
return { errors: [toESBuildError(e, options)] };
}
});
build.onEnd(() => {
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<string>}
*/
async function compileSvelte(options, { filename, code }, statsCollection) {
if (isSvelte5 && filename.endsWith(svelteModuleExtension)) {
const endStat = statsCollection?.start(filename);
// @ts-ignore doesn't exist in Svelte 4
const compiled = svelte.compileModule(code, {
filename,
generate: 'client'
});
if (endStat) {
endStat();
}
return compiled.js.map
? compiled.js.code + '//# sourceMappingURL=' + compiled.js.map.toUrl()
: compiled.js.code;
}
let css = options.compilerOptions.css;
if (css !== 'none') {
// TODO ideally we'd be able to externalize prebundled styles too, but for now always put them in the js
css = 'injected';
}
/** @type {import('../index.d.ts').CompileOptions} */
const compileOptions = {
...options.compilerOptions,
css,
filename,
// @ts-expect-error generate type is different for svelte5
generate: isSvelte5 ? 'client' : 'dom'
};
if (isSvelte3) {
// @ts-ignore
compileOptions.format = 'esm';
}
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.experimental?.dynamicCompileOptions?.({
filename,
code: finalCode,
compileOptions
});
if (dynamicCompileOptions && log.debug.enabled) {
log.debug(`dynamic compile options for ${filename}: ${JSON.stringify(dynamicCompileOptions)}`);
}
const finalCompileOptions = dynamicCompileOptions
? {
...compileOptions,
...dynamicCompileOptions
}
: compileOptions;
const endStat = statsCollection?.start(filename);
const compiled = svelte.compile(finalCode, finalCompileOptions);
if (endStat) {
endStat();
}
return compiled.js.map
? compiled.js.code + '//# sourceMappingURL=' + compiled.js.map.toUrl()
: compiled.js.code;
}

View File

@ -0,0 +1,43 @@
import * as 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]);
}

View File

@ -0,0 +1,231 @@
import { createFilter, normalizePath } from 'vite';
import * as fs from 'node:fs';
import { log } from './log.js';
const VITE_FS_PREFIX = '/@fs/';
const IS_WINDOWS = process.platform === 'win32';
const SUPPORTED_COMPILER_OPTIONS = [
'generate',
'dev',
'css',
'hydratable',
'customElement',
'immutable',
'enableSourcemap'
];
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-ignore
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":"ssr","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}
*/
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 {import('../index.d.ts').Arrayable<string> | undefined} include
* @param {import('../index.d.ts').Arrayable<string> | undefined} exclude
* @param {string[]} extensions
* @returns {(filename: string) => boolean}
*/
function buildFilter(include, exclude, extensions) {
const rollupFilter = createFilter(include, exclude);
return (filename) => rollupFilter(filename) && extensions.some((ext) => filename.endsWith(ext));
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {import('../types/id.d.ts').IdParser}
*/
export function buildIdParser(options) {
const { include, exclude, extensions, root } = options;
const normalizedRoot = normalizePath(root);
const filter = buildFilter(include, exclude, extensions ?? []);
return (id, ssr, timestamp = Date.now()) => {
const { filename, rawQuery } = splitId(id);
if (filter(filename)) {
return parseToSvelteRequest(id, filename, rawQuery, normalizedRoot, timestamp, ssr);
}
};
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {import('../types/id.d.ts').ModuleIdParser}
*/
export function buildModuleIdParser(options) {
const { include, exclude, extensions } = options?.experimental?.compileModule ?? {};
const root = options.root;
const normalizedRoot = normalizePath(root);
const filter = buildFilter(include, exclude, extensions ?? ['.svelte.js', '.svelte.ts']);
return (id, ssr, timestamp = Date.now()) => {
const { filename, rawQuery } = splitId(id);
if (filter(filename)) {
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
};
}

View File

@ -0,0 +1,132 @@
import fs from 'node:fs';
import { toRollupError } from './error.js';
import { log } from './log.js';
/**
* utility function to compile ?raw and ?direct requests in load hook
*
* @param {import('../types/id.d.ts').SvelteRequest} svelteRequest
* @param {import('../types/compile.d.ts').CompileSvelte} compileSvelte
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {Promise<string>}
*/
export async function loadRaw(svelteRequest, compileSvelte, options) {
const { id, filename, query } = svelteRequest;
// raw svelte subrequest, compile on the fly and return requested subpart
let compileData;
const source = fs.readFileSync(filename, 'utf-8');
try {
//avoid compileSvelte doing extra ssr stuff unless requested
svelteRequest.ssr = query.compilerOptions?.generate === 'ssr';
const type = query.type;
compileData = await compileSvelte(svelteRequest, source, {
...options,
// don't use dynamic vite-plugin-svelte defaults here to ensure stable result between ssr,dev and build
compilerOptions: {
dev: false,
css: false,
hydratable: false,
enableSourcemap: query.sourcemap
? {
js: type === 'script' || type === 'all',
css: type === 'style' || type === 'all'
}
: false,
...svelteRequest.query.compilerOptions
},
hot: false,
emitCss: true
});
} catch (e) {
throw toRollupError(e, options);
}
let result;
if (query.type === 'style') {
result = compileData.compiled.css;
} else if (query.type === 'script') {
result = compileData.compiled.js;
} else if (query.type === 'preprocessed') {
result = compileData.preprocessed;
} else if (query.type === 'all' && query.raw) {
return allToRawExports(compileData, source);
} else {
throw new Error(
`invalid "type=${query.type}" in ${id}. supported are script, style, preprocessed, all`
);
}
if (query.direct) {
const supportedDirectTypes = ['script', 'style'];
if (!supportedDirectTypes.includes(query.type)) {
throw new Error(
`invalid "type=${
query.type
}" combined with direct in ${id}. supported are: ${supportedDirectTypes.join(', ')}`
);
}
log.debug(`load returns direct result for ${id}`);
let directOutput = result.code;
if (query.sourcemap && result.map?.toUrl) {
const map = `sourceMappingURL=${result.map.toUrl()}`;
if (query.type === 'style') {
directOutput += `\n\n/*# ${map} */\n`;
} else if (query.type === 'script') {
directOutput += `\n\n//# ${map}\n`;
}
}
return directOutput;
} else if (query.raw) {
log.debug(`load returns raw result for ${id}`);
return toRawExports(result);
} else {
throw new Error(`invalid raw mode in ${id}, supported are raw, direct`);
}
}
/**
* turn compileData and source into a flat list of raw exports
*
* @param {import('../types/compile.d.ts').CompileData} compileData
* @param {string} source
*/
function allToRawExports(compileData, source) {
// flatten CompileData
/** @type {Partial<import('../types/compile.d.ts').CompileData & { source: string }>} */
const exports = {
...compileData,
...compileData.compiled,
source
};
delete exports.compiled;
delete exports.filename; // absolute path, remove to avoid it in output
return toRawExports(exports);
}
/**
* turn object into raw exports.
*
* every prop is returned as a const export, and if prop 'code' exists it is additionally added as default export
*
* eg {'foo':'bar','code':'baz'} results in
*
* ```js
* export const code='baz'
* export const foo='bar'
* export default code
* ```
* @param {object} object
* @returns {string}
*/
function toRawExports(object) {
let exports =
Object.entries(object)
//eslint-disable-next-line no-unused-vars
.filter(([key, value]) => typeof value !== 'function') // preprocess output has a toString function that's enumerable
.sort(([a], [b]) => (a < b ? -1 : a === b ? 0 : 1))
.map(([key, value]) => `export const ${key}=${JSON.stringify(value)}`)
.join('\n') + '\n';
if (Object.prototype.hasOwnProperty.call(object, 'code')) {
exports += 'export default code\n';
}
return exports;
}

View File

@ -0,0 +1,117 @@
import { createRequire } from 'node:module';
import path from 'node:path';
import fs from 'node:fs';
import { pathToFileURL } from 'node:url';
import { log } from './log.js';
// used to require cjs config in esm.
// NOTE dynamic import() cjs technically works, but timestamp query cache bust
// have no effect, likely because it has another internal cache?
/** @type {NodeRequire}*/
let esmRequire;
export const knownSvelteConfigNames = [
'svelte.config.js',
'svelte.config.cjs',
'svelte.config.mjs'
];
// hide dynamic import from ts transform to prevent it turning into a require
// see https://github.com/microsoft/TypeScript/issues/43329#issuecomment-811606238
// also use timestamp query to avoid caching on reload
const dynamicImportDefault = new Function(
'path',
'timestamp',
'return import(path + "?t=" + timestamp).then(m => m.default)'
);
/** @type {import('../index.d.ts').loadSvelteConfig} */
export async function loadSvelteConfig(viteConfig, inlineOptions) {
if (inlineOptions?.configFile === false) {
return;
}
const configFile = findConfigToLoad(viteConfig, inlineOptions);
if (configFile) {
let err;
// try to use dynamic import for svelte.config.js first
if (configFile.endsWith('.js') || configFile.endsWith('.mjs')) {
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);
err = e;
}
}
// cjs or error with dynamic import
if (!configFile.endsWith('.mjs')) {
try {
// identify which require function to use (esm and cjs mode)
const _require = import.meta.url
? esmRequire ?? (esmRequire = createRequire(import.meta.url))
: require;
// avoid loading cached version on reload
delete _require.cache[_require.resolve(configFile)];
const result = _require(configFile);
if (result != null) {
return {
...result,
configFile
};
} else {
throw new Error(`invalid export in ${configFile}`);
}
} catch (e) {
log.error(`failed to require config ${configFile}`, e);
if (!err) {
err = e;
}
}
}
// failed to load existing config file
throw err;
}
}
/**
* @param {import('vite').UserConfig | undefined} viteConfig
* @param {Partial<import('../index.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.debug(`no svelte config found at ${root}`);
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];
}
}

View File

@ -0,0 +1,255 @@
/* eslint-disable no-console */
import { cyan, red, yellow } from 'kleur/colors';
import debug from 'debug';
/** @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: debug(`vite:${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) {
const log = namespace ? logger.log.extend(namespace) : logger.log;
payload !== undefined ? log(message, payload) : 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/types/compiler/interfaces').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/types/compiler/interfaces').Warning[]} */
const handledByDefaultWarn = [];
const notIgnored = warnings?.filter((w) => !ignoreCompilerWarning(w, isBuild, emitCss));
const extra = buildExtraWarnings(warnings, isBuild);
const allWarnings = [...notIgnored, ...extra];
if (sendViaWS) {
const _warn = warn;
/** @type {(w: import('svelte/types/compiler/interfaces').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/types/compiler/interfaces').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/types/compiler/interfaces').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/types/compiler/interfaces').Warning[]} warnings
* @param {boolean} isBuild
* @returns {import('svelte/types/compiler/interfaces').Warning[]}
*/
function buildExtraWarnings(warnings, isBuild) {
const extraWarnings = [];
if (!isBuild) {
const noScopableElementWarnings = warnings.filter((w) => isNoScopableElementWarning(w));
if (noScopableElementWarnings.length > 0) {
// in case there are multiple, use last one as that is the one caused by our *{} rule
const noScopableElementWarning =
noScopableElementWarnings[noScopableElementWarnings.length - 1];
extraWarnings.push({
...noScopableElementWarning,
code: 'vite-plugin-svelte-css-no-scopable-elements',
message:
"No scopable elements found in template. If you're using global styles in the style tag, you should move it into an external stylesheet file and import it in JS. See https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/faq.md#where-should-i-put-my-global-styles."
});
}
}
return extraWarnings;
}
/**
* @param {import('svelte/types/compiler/interfaces').Warning} w
*/
function warnDev(w) {
log.info.enabled && log.info(buildExtendedLogMessage(w));
}
/**
* @param {import('svelte/types/compiler/interfaces').Warning} w
*/
function warnBuild(w) {
log.warn.enabled && log.warn(buildExtendedLogMessage(w), w.frame);
}
/**
* @param {import('svelte/types/compiler/interfaces').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 debug.enabled(`vite:${prefix}:${namespace}`);
}

View File

@ -0,0 +1,53 @@
import { promises as fs } from 'node:fs';
import path from 'node:path';
// List of options that changes the prebundling result
/** @type {(keyof import('../types/options.d.ts').ResolvedOptions)[]} */
const PREBUNDLE_SENSITIVE_OPTIONS = [
'compilerOptions',
'configFile',
'experimental',
'extensions',
'ignorePluginPreprocessors',
'preprocess'
];
/**
* @param {string} cacheDir
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {Promise<boolean>} Whether the Svelte metadata has changed
*/
export async function saveSvelteMetadata(cacheDir, options) {
const svelteMetadata = generateSvelteMetadata(options);
const svelteMetadataPath = path.resolve(cacheDir, '_svelte_metadata.json');
const currentSvelteMetadata = JSON.stringify(svelteMetadata, (_, value) => {
// Handle preprocessors
return typeof value === 'function' ? value.toString() : value;
});
/** @type {string | undefined} */
let existingSvelteMetadata;
try {
existingSvelteMetadata = await fs.readFile(svelteMetadataPath, 'utf8');
} catch {
// ignore
}
await fs.mkdir(cacheDir, { recursive: true });
await fs.writeFile(svelteMetadataPath, currentSvelteMetadata);
return currentSvelteMetadata !== existingSvelteMetadata;
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {Partial<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;
}

View File

@ -0,0 +1,617 @@
/* eslint-disable no-unused-vars */
import { normalizePath } from 'vite';
import { isDebugNamespaceEnabled, log } from './log.js';
import { loadSvelteConfig } from './load-svelte-config.js';
import {
SVELTE_EXPORT_CONDITIONS,
SVELTE_HMR_IMPORTS,
SVELTE_IMPORTS,
SVELTE_RESOLVE_MAIN_FIELDS,
VITE_RESOLVE_MAIN_FIELDS
} from './constants.js';
import path from 'node:path';
import { esbuildSveltePlugin, facadeEsbuildSveltePluginName } from './esbuild.js';
import { addExtraPreprocessors } from './preprocess.js';
import deepmerge from 'deepmerge';
import {
crawlFrameworkPkgs,
isDepExcluded,
isDepExternaled,
isDepIncluded,
isDepNoExternaled
} from 'vitefu';
import { isCommonDepWithoutSvelteField } from './dependencies.js';
import { VitePluginSvelteStats } from './vite-plugin-svelte-stats.js';
import { VitePluginSvelteCache } from './vite-plugin-svelte-cache.js';
import { isSvelte5 } from './svelte-version.js';
const allowedPluginOptions = new Set([
'include',
'exclude',
'emitCss',
'hot',
'ignorePluginPreprocessors',
'disableDependencyReinclusion',
'prebundleSvelteLibraries',
'inspector',
'experimental'
]);
const knownRootOptions = new Set(['extensions', 'compilerOptions', 'preprocess', 'onwarn']);
const allowedInlineOptions = new Set(['configFile', ...allowedPluginOptions, ...knownRootOptions]);
/**
* @param {Partial<import('../index.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('../index.d.ts').SvelteOptions>} [config]
* @returns {Partial<import('../index.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-ignore
delete pluginOptions[unkownOption];
});
}
/** @type {import('../index.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('../index.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: ['.svelte'],
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
* @param {VitePluginSvelteCache} cache
* @returns {import('../types/options.d.ts').ResolvedOptions}
*/
export function resolveOptions(preResolveOptions, viteConfig, cache) {
const css = preResolveOptions.emitCss ? 'external' : 'injected';
/** @type {Partial<import('../index.d.ts').Options>} */
const defaultOptions = {
hot: viteConfig.isProduction
? false
: {
injectCss: css === 'injected',
partialAccept: !!viteConfig.experimental?.hmrPartialAccept
},
compilerOptions: {
css,
dev: !viteConfig.isProduction
}
};
/** @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);
enforceOptionsForProduction(merged);
// mergeConfigs would mangle functions on the stats class, so do this afterwards
if (log.debug.enabled && isDebugNamespaceEnabled('stats')) {
merged.stats = new VitePluginSvelteStats(cache);
}
return merged;
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
*/
function enforceOptionsForHmr(options) {
if (isSvelte5) {
log.warn('svelte 5 does not support hmr api yet, disabling it for now');
options.hot = false;
}
if (options.hot) {
if (!options.compilerOptions.dev) {
log.warn('hmr is enabled but compilerOptions.dev is false, forcing it to true');
options.compilerOptions.dev = true;
}
if (options.emitCss) {
if (options.hot !== true && options.hot.injectCss) {
log.warn('hmr and emitCss are enabled but hot.injectCss is true, forcing it to false');
options.hot.injectCss = false;
}
const css = options.compilerOptions.css;
if (css === true || css === 'injected') {
const forcedCss = 'external';
log.warn(
`hmr and emitCss are enabled but compilerOptions.css is ${css}, forcing it to ${forcedCss}`
);
options.compilerOptions.css = forcedCss;
}
} else {
if (options.hot === true || !options.hot.injectCss) {
log.warn(
'hmr with emitCss disabled requires option hot.injectCss to be enabled, forcing it to true'
);
if (options.hot === true) {
options.hot = { injectCss: true };
} else {
options.hot.injectCss = true;
}
}
const css = options.compilerOptions.css;
if (!(css === true || css === 'injected')) {
const forcedCss = 'injected';
log.warn(
`hmr with emitCss disabled requires compilerOptions.css to be enabled, forcing it to ${forcedCss}`
);
options.compilerOptions.css = forcedCss;
}
}
}
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
*/
function enforceOptionsForProduction(options) {
if (options.isProduction) {
if (options.hot) {
log.warn('options.hot is enabled but does not work on production build, forcing it to false');
options.hot = 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.hot && 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']) {
if (experimental[promoted]) {
//@ts-expect-error untyped assign
options[promoted] = experimental[promoted];
delete experimental[promoted];
log.warn(
`Option "vitePlugin.experimental.${promoted}" is no longer experimental and has moved to "vitePlugin.${promoted}". Please update your svelte 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) {
// make sure we only readd vite default mainFields when no other plugin has changed the config already
// see https://github.com/sveltejs/vite-plugin-svelte/issues/581
if (!config.resolve) {
config.resolve = {};
}
config.resolve.mainFields = [
...SVELTE_RESOLVE_MAIN_FIELDS,
...(config.resolve.mainFields ?? VITE_RESOLVE_MAIN_FIELDS)
];
/** @type {Partial<import('vite').UserConfig>} */
const extraViteConfig = {
resolve: {
dedupe: [...SVELTE_IMPORTS, ...SVELTE_HMR_IMPORTS],
conditions: [...SVELTE_EXPORT_CONDITIONS]
}
// this option is still awaiting a PR in vite to be supported
// see https://github.com/sveltejs/vite-plugin-svelte/issues/60
// @ts-ignore
// 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)
)
]
};
// handle prebundling for svelte files
if (options.prebundleSvelteLibraries) {
extraViteConfig.optimizeDeps = {
...extraViteConfig.optimizeDeps,
// Experimental Vite API to allow these extensions to be scanned and prebundled
// @ts-ignore
extensions: options.extensions ?? ['.svelte'],
// Add esbuild plugin to prebundle Svelte files.
// Currently a placeholder as more information is needed after Vite config is resolved,
// the real Svelte plugin is added in `patchResolvedViteConfig()`
esbuildOptions: {
plugins: [{ name: facadeEsbuildSveltePluginName, setup: () => {} }]
}
};
}
// enable hmrPartialAccept if not explicitly disabled
if (
(options.hot == null ||
options.hot === true ||
(options.hot && options.hot.partialAccept !== false)) && // deviate from svelte-hmr, default to true
config.experimental?.hmrPartialAccept !== false
) {
log.debug('enabling "experimental.hmrPartialAccept" in vite 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.'
);
}
}
}
/**
* @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 depsConfig = await crawlFrameworkPkgs({
root: 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;
});
}
return hasSvelteCondition || !!pkgJson.svelte;
},
isSemiFrameworkPkgByJson(pkgJson) {
return !!pkgJson.dependencies?.svelte || !!pkgJson.peerDependencies?.svelte;
},
isFrameworkPkgByName(pkgName) {
const isNotSveltePackage = isCommonDepWithoutSvelteField(pkgName);
if (isNotSveltePackage) {
return false;
} else {
return undefined;
}
}
});
log.debug('extra config for dependencies generated by vitefu', depsConfig);
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);
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 = [];
const exclude = ['svelte-hmr'];
if (!isDepExcluded('svelte', config.optimizeDeps?.exclude ?? [])) {
const svelteImportsToInclude = SVELTE_IMPORTS.filter((x) => x !== 'svelte/ssr'); // not used on clientside
log.debug(
`adding bare svelte packages to optimizeDeps.include: ${svelteImportsToInclude.join(', ')} `
);
include.push(...svelteImportsToInclude);
} else {
log.debug('"svelte" is excluded in optimizeDeps.exclude, skipped adding it to include.');
}
/** @type {(string | RegExp)[]} */
const noExternal = [];
/** @type {string[]} */
const external = [];
// add svelte to ssr.noExternal unless it is present in ssr.external
// so we can resolve it with svelte/ssr
if (!isDepExternaled('svelte', config.ssr?.external ?? [])) {
noExternal.push('svelte', /^svelte\//);
}
return { optimizeDeps: { include, exclude }, ssr: { noExternal, external } };
}
/**
* @param {import('vite').ResolvedConfig} viteConfig
* @param {import('../types/options.d.ts').ResolvedOptions} options
*/
export function patchResolvedViteConfig(viteConfig, options) {
if (options.preprocess) {
for (const preprocessor of arraify(options.preprocess)) {
if (preprocessor.style && '__resolvedConfig' in preprocessor.style) {
preprocessor.style.__resolvedConfig = viteConfig;
}
}
}
// replace facade esbuild plugin with a real one
const facadeEsbuildSveltePlugin = viteConfig.optimizeDeps.esbuildOptions?.plugins?.find(
(plugin) => plugin.name === facadeEsbuildSveltePluginName
);
if (facadeEsbuildSveltePlugin) {
Object.assign(facadeEsbuildSveltePlugin, esbuildSveltePlugin(options));
}
}
/**
* @template T
* @param {T | T[]} value
* @returns {T[]}
*/
function arraify(value) {
return Array.isArray(value) ? value : [value];
}

View File

@ -0,0 +1,118 @@
import MagicString from 'magic-string';
import { log } from './log.js';
import path from 'node:path';
/**
* this appends a *{} rule to component styles to force the svelte compiler to add style classes to all nodes
* That means adding/removing class rules from <style> node won't trigger js updates as the scope classes are not changed
*
* only used during dev with enabled css hmr
*
* @returns {import('svelte/types/compiler/preprocess').PreprocessorGroup}
*/
export function createInjectScopeEverythingRulePreprocessorGroup() {
return {
style({ content, filename }) {
const s = new MagicString(content);
s.append(' *{}');
return {
code: s.toString(),
map: s.generateDecodedMap({
source: filename ? path.basename(filename) : undefined,
hires: true
})
};
}
};
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @param {import('vite').ResolvedConfig} config
* @returns {{
* prependPreprocessors: import('svelte/types/compiler/preprocess').PreprocessorGroup[],
* appendPreprocessors: import('svelte/types/compiler/preprocess').PreprocessorGroup[]
* }}
*/
function buildExtraPreprocessors(options, config) {
/** @type {import('svelte/types/compiler/preprocess').PreprocessorGroup[]} */
const prependPreprocessors = [];
/** @type {import('svelte/types/compiler/preprocess').PreprocessorGroup[]} */
const appendPreprocessors = [];
// @ts-ignore
const pluginsWithPreprocessorsDeprecated = config.plugins.filter((p) => p?.sveltePreprocess);
if (pluginsWithPreprocessorsDeprecated.length > 0) {
log.warn(
`The following plugins use the deprecated 'plugin.sveltePreprocess' field. Please contact their maintainers and ask them to move it to 'plugin.api.sveltePreprocess': ${pluginsWithPreprocessorsDeprecated
.map((p) => p.name)
.join(', ')}`
);
// patch plugin to avoid breaking
pluginsWithPreprocessorsDeprecated.forEach((p) => {
if (!p.api) {
p.api = {};
}
if (p.api.sveltePreprocess === undefined) {
// @ts-ignore
p.api.sveltePreprocess = p.sveltePreprocess;
} else {
log.error(
`ignoring plugin.sveltePreprocess of ${p.name} because it already defined plugin.api.sveltePreprocess.`
);
}
});
}
/** @type {import('vite').Plugin[]} */
const pluginsWithPreprocessors = config.plugins.filter((p) => p?.api?.sveltePreprocess);
/** @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(', ')}`
);
}
if (included.length > 0) {
log.debug(
`Adding svelte preprocessors defined by these vite plugins: ${included
.map((p) => p.name)
.join(', ')}`
);
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];
}
}
}

View File

@ -0,0 +1,66 @@
import path from 'node:path';
import { builtinModules } from 'node:module';
import { resolveDependencyData, isCommonDepWithoutSvelteField } from './dependencies.js';
import { normalizePath } from 'vite';
/**
* @param {string} importee
* @param {string | undefined} importer
* @param {import('./vite-plugin-svelte-cache').VitePluginSvelteCache} cache
* @returns {Promise<string | void>}
*/
export async function resolveViaPackageJsonSvelte(importee, importer, cache) {
if (
importer &&
isBareImport(importee) &&
!isNodeInternal(importee) &&
!isCommonDepWithoutSvelteField(importee)
) {
const cached = cache.getResolvedSvelteField(importee, importer);
if (cached) {
return cached;
}
const pkgData = await resolveDependencyData(importee, importer);
if (pkgData) {
const { pkg, dir } = pkgData;
if (pkg.svelte) {
const result = normalizePath(path.resolve(dir, pkg.svelte));
cache.setResolvedSvelteField(importee, importer, result);
return result;
}
}
}
}
/**
* @param {string} importee
* @returns {boolean}
*/
function isNodeInternal(importee) {
return importee.startsWith('node:') || builtinModules.includes(importee);
}
/**
* @param {string} importee
* @returns {boolean}
*/
function isBareImport(importee) {
if (
!importee ||
importee[0] === '.' ||
importee[0] === '\0' ||
importee.includes(':') ||
path.isAbsolute(importee)
) {
return false;
}
const parts = importee.split('/');
switch (parts.length) {
case 1:
return true;
case 2:
return parts[0].startsWith('@');
default:
return false;
}
}

View File

@ -0,0 +1,81 @@
import path from 'node:path';
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);
}
}

View File

@ -0,0 +1,11 @@
import { VERSION } from 'svelte/compiler';
/**
* @type {boolean}
*/
export const isSvelte3 = VERSION.startsWith('3.');
/**
* @type {boolean}
*/
export const isSvelte5 = VERSION.startsWith('5.');

View File

@ -0,0 +1,253 @@
import { readFileSync } from 'node:fs';
import { dirname } from 'node:path';
import { findClosestPkgJsonPath } from 'vitefu';
import { normalizePath } from 'vite';
/**
* @typedef {{
* name: string;
* version: string;
* svelte?: string;
* path: string;
* }} PackageInfo
*/
/**
* @class
*/
export class VitePluginSvelteCache {
/** @type {Map<string, import('../types/compile.d.ts').Code>} */
#css = new Map();
/** @type {Map<string, import('../types/compile.d.ts').Code>} */
#js = new Map();
/** @type {Map<string, string[]>} */
#dependencies = new Map();
/** @type {Map<string, Set<string>>} */
#dependants = new Map();
/** @type {Map<string, string>} */
#resolvedSvelteFields = new Map();
/** @type {Map<string, any>} */
#errors = new Map();
/** @type {PackageInfo[]} */
#packageInfos = [];
/**
* @param {import('../types/compile.d.ts').CompileData} compileData
*/
update(compileData) {
this.#errors.delete(compileData.normalizedFilename);
this.#updateCSS(compileData);
this.#updateJS(compileData);
this.#updateDependencies(compileData);
}
/**
* @param {import('../types/id.d.ts').SvelteRequest} svelteRequest
* @returns {boolean}
*/
has(svelteRequest) {
const id = svelteRequest.normalizedFilename;
return this.#errors.has(id) || this.#js.has(id) || this.#css.has(id);
}
/**
* @param {import('../types/id.d.ts').SvelteRequest} svelteRequest
* @param {any} error
*/
setError(svelteRequest, error) {
// keep dependency info, otherwise errors in dependants would not trigger an update after fixing
// because they are no longer watched
this.remove(svelteRequest, true);
this.#errors.set(svelteRequest.normalizedFilename, error);
}
/**
* @param {import('../types/compile.d.ts').CompileData} compileData
*/
#updateCSS(compileData) {
this.#css.set(compileData.normalizedFilename, compileData.compiled.css);
}
/**
* @param {import('../types/compile.d.ts').CompileData} compileData
*/
#updateJS(compileData) {
if (!compileData.ssr) {
// do not cache SSR js
this.#js.set(compileData.normalizedFilename, compileData.compiled.js);
}
}
/**
* @param {import('../types/compile.d.ts').CompileData} compileData
*/
#updateDependencies(compileData) {
const id = compileData.normalizedFilename;
const prevDependencies = this.#dependencies.get(id) || [];
const dependencies = compileData.dependencies;
this.#dependencies.set(id, dependencies);
const removed = prevDependencies.filter((d) => !dependencies.includes(d));
const added = dependencies.filter((d) => !prevDependencies.includes(d));
added.forEach((d) => {
if (!this.#dependants.has(d)) {
this.#dependants.set(d, new Set());
}
/** @type {Set<string>} */ (this.#dependants.get(d)).add(compileData.filename);
});
removed.forEach((d) => {
/** @type {Set<string>} */ (this.#dependants.get(d)).delete(compileData.filename);
});
}
/**
* @param {import('../types/id.d.ts').SvelteRequest} svelteRequest
* @param {boolean} [keepDependencies]
* @returns {boolean}
*/
remove(svelteRequest, keepDependencies = false) {
const id = svelteRequest.normalizedFilename;
let removed = false;
if (this.#errors.delete(id)) {
removed = true;
}
if (this.#js.delete(id)) {
removed = true;
}
if (this.#css.delete(id)) {
removed = true;
}
if (!keepDependencies) {
const dependencies = this.#dependencies.get(id);
if (dependencies) {
removed = true;
dependencies.forEach((d) => {
const dependants = this.#dependants.get(d);
if (dependants && dependants.has(svelteRequest.filename)) {
dependants.delete(svelteRequest.filename);
}
});
this.#dependencies.delete(id);
}
}
return removed;
}
/**
* @param {import('../types/id.d.ts').SvelteRequest} svelteRequest
* @returns {import('../types/compile.d.ts').Code | undefined}
*/
getCSS(svelteRequest) {
return this.#css.get(svelteRequest.normalizedFilename);
}
/**
* @param {import('../types/id.d.ts').SvelteRequest} svelteRequest
* @returns {import('../types/compile.d.ts').Code | undefined}
*/
getJS(svelteRequest) {
if (!svelteRequest.ssr) {
// SSR js isn't cached
return this.#js.get(svelteRequest.normalizedFilename);
}
}
/**
* @param {import('../types/id.d.ts').SvelteRequest} svelteRequest
* @returns {any}
*/
getError(svelteRequest) {
return this.#errors.get(svelteRequest.normalizedFilename);
}
/**
* @param {string} path
* @returns {string[]}
*/
getDependants(path) {
const dependants = this.#dependants.get(path);
return dependants ? [...dependants] : [];
}
/**
* @param {string} name
* @param {string} [importer]
* @returns {string|void}
*/
getResolvedSvelteField(name, importer) {
return this.#resolvedSvelteFields.get(this.#getResolvedSvelteFieldKey(name, importer));
}
/**
* @param {string} name
* @param {string} [importer]
* @returns {boolean}
*/
hasResolvedSvelteField(name, importer) {
return this.#resolvedSvelteFields.has(this.#getResolvedSvelteFieldKey(name, importer));
}
/**
*
* @param {string} importee
* @param {string | undefined} importer
* @param {string} resolvedSvelte
*/
setResolvedSvelteField(importee, importer, resolvedSvelte) {
this.#resolvedSvelteFields.set(
this.#getResolvedSvelteFieldKey(importee, importer),
resolvedSvelte
);
}
/**
* @param {string} importee
* @param {string | undefined} importer
* @returns {string}
*/
#getResolvedSvelteFieldKey(importee, importer) {
return importer ? `${importer} > ${importee}` : importee;
}
/**
* @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;
}
}
/**
* 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;
}

View File

@ -0,0 +1,199 @@
import { log } from './log.js';
import { performance } from 'node:perf_hooks';
import { normalizePath } from 'vite';
/** @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 {
// package directory -> package name
/** @type {import('./vite-plugin-svelte-cache.js').VitePluginSvelteCache} */
#cache;
/** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection[]} */
#collections = [];
/**
* @param {import('./vite-plugin-svelte-cache.js').VitePluginSvelteCache} cache
*/
constructor(cache) {
this.#cache = cache;
}
/**
* @param {string} name
* @param {Partial<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.#cache.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;
}
}

View File

@ -0,0 +1,118 @@
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
* @param {import('./vite-plugin-svelte-cache').VitePluginSvelteCache} cache
* @param {import('../types/id.d.ts').IdParser} requestParser
* @returns {void}
*/
export function setupWatchers(options, cache, requestParser) {
const { server, configFile: svelteConfigFile } = options;
if (!server) {
return;
}
const { watcher, ws } = server;
const { root, server: serverConfig } = server.config;
/** @type {(filename: string) => void} */
const emitChangeEventOnDependants = (filename) => {
const dependants = cache.getDependants(filename);
dependants.forEach((dependant) => {
if (fs.existsSync(dependant)) {
log.debug(
`emitting virtual change event for "${dependant}" because depdendency "${filename}" changed`
);
watcher.emit('change', dependant);
}
});
};
/** @type {(filename: string) => void} */
const removeUnlinkedFromCache = (filename) => {
const svelteRequest = requestParser(filename, false);
if (svelteRequest) {
const removedFromCache = cache.remove(svelteRequest);
if (removedFromCache) {
log.debug(`cleared VitePluginSvelteCache for deleted file ${filename}`);
}
}
};
/** @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: [emitChangeEventOnDependants],
unlink: [removeUnlinkedFromCache, emitChangeEventOnDependants]
};
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));
}
}