first commit

This commit is contained in:
becarta
2025-05-16 00:17:42 +02:00
parent ea5c866137
commit bacf566ec9
6020 changed files with 1715262 additions and 0 deletions

View File

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

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

@@ -0,0 +1,236 @@
import fs from 'node:fs';
import process from 'node:process';
import { svelteInspector } from '@sveltejs/vite-plugin-svelte-inspector';
import { handleHotUpdate } from './handle-hot-update.js';
import { log, logCompilerWarnings } from './utils/log.js';
import { createCompileSvelte } from './utils/compile.js';
import { buildIdParser, buildModuleIdParser } from './utils/id.js';
import {
buildExtraViteConfig,
validateInlineOptions,
resolveOptions,
patchResolvedViteConfig,
preResolveOptions,
ensureConfigEnvironmentMainFields,
ensureConfigEnvironmentConditions
} from './utils/options.js';
import { ensureWatchedFile, setupWatchers } from './utils/watch.js';
import { toRollupError } from './utils/error.js';
import { saveSvelteMetadata } from './utils/optimizer.js';
import { VitePluginSvelteCache } from './utils/vite-plugin-svelte-cache.js';
import { loadRaw } from './utils/load-raw.js';
import * as svelteCompiler from 'svelte/compiler';
/**
* @param {Partial<import('./public.d.ts').Options>} [inlineOptions]
* @returns {import('vite').Plugin[]}
*/
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;
/** @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, 'config');
return extraViteConfig;
},
configEnvironment(name, config, opts) {
ensureConfigEnvironmentMainFields(name, config, opts);
// @ts-expect-error the function above should make `resolve.mainFields` non-nullable
config.resolve.mainFields.unshift('svelte');
ensureConfigEnvironmentConditions(name, config, opts);
// @ts-expect-error the function above should make `resolve.conditions` non-nullable
config.resolve.conditions.push('svelte');
},
async configResolved(config) {
options = resolveOptions(options, config, cache);
patchResolvedViteConfig(config, options);
requestParser = buildIdParser(options);
compileSvelte = createCompileSvelte();
viteConfig = config;
// TODO deep clone to avoid mutability from outside?
api.options = options;
log.debug('resolved options', options, 'config');
},
async buildStart() {
if (!options.prebundleSvelteLibraries) return;
const isSvelteMetadataChanged = await saveSvelteMetadata(viteConfig.cacheDir, options);
if (isSvelteMetadataChanged) {
// Force Vite to optimize again. Although we mutate the config here, it works because
// Vite's optimizer runs after `buildStart()`.
viteConfig.optimizeDeps.force = true;
}
},
configureServer(server) {
options.server = server;
setupWatchers(options, cache, requestParser);
},
async load(id, opts) {
const ssr = !!opts?.ssr;
const svelteRequest = requestParser(id, !!ssr);
if (svelteRequest) {
const { filename, query, raw } = svelteRequest;
if (raw) {
const code = await loadRaw(svelteRequest, compileSvelte, options);
// prevent vite from injecting sourcemaps in the results.
return {
code,
map: {
mappings: ''
}
};
} else {
if (query.svelte && query.type === 'style') {
const css = cache.getCSS(svelteRequest);
if (css) {
return css;
}
}
// prevent vite asset plugin from loading files as url that should be compiled in transform
if (viteConfig.assetsInclude(filename)) {
log.debug(`load returns raw content for ${filename}`, undefined, 'load');
return fs.readFileSync(filename, 'utf-8');
}
}
}
},
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 &&
!svelteRequest.query.inline
) {
// 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}`,
undefined,
'resolve'
);
return svelteRequest.cssId;
}
}
},
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);
}
}
}
return {
...compileData.compiled.js,
meta: {
vite: {
lang: compileData.lang
}
}
};
},
handleHotUpdate(ctx) {
if (!options.compilerOptions.hmr || !options.emitCss) {
return;
}
const svelteRequest = requestParser(ctx.file, false, ctx.timestamp);
if (svelteRequest) {
return handleHotUpdate(compileSvelte, ctx, svelteRequest, cache, options);
}
},
async buildEnd() {
await options.stats?.finishAll();
}
},
{
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 {
const compileResult = svelteCompiler.compileModule(code, {
dev: !viteConfig.isProduction,
generate: ssr ? 'server' : 'client',
filename: moduleRequest.filename
});
logCompilerWarnings(moduleRequest, compileResult.warnings, options);
return compileResult.js;
} catch (e) {
throw toRollupError(e, options);
}
}
},
svelteInspector()
];
return plugins;
}
export { vitePreprocess } from './preprocess.js';
export { loadSvelteConfig } from './utils/load-svelte-config.js';

View File

@@ -0,0 +1,124 @@
import process from 'node:process';
import { isCSSRequest, 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 supportedScriptLangs = ['ts'];
export const lang_sep = '.vite-preprocess';
/**
* @param {import('./public.d.ts').VitePreprocessOptions} [opts]
* @returns {import('svelte/compiler').PreprocessorGroup}
*/
export function vitePreprocess(opts) {
/** @type {import('svelte/compiler').PreprocessorGroup} */
const preprocessor = { name: 'vite-preprocess' };
if (opts?.script === true) {
preprocessor.script = viteScript().script;
}
if (opts?.style !== false) {
const styleOpts = typeof opts?.style == 'object' ? opts?.style : undefined;
preprocessor.style = viteStyle(styleOpts).style;
}
return preprocessor;
}
/**
* @returns {{ script: import('svelte/compiler').Preprocessor }}
*/
function viteScript() {
return {
async script({ attributes, content, filename = '' }) {
const lang = /** @type {string} */ (attributes.lang);
if (!supportedScriptLangs.includes(lang)) return;
const { code, map } = await transformWithEsbuild(content, filename, {
loader: /** @type {import('vite').ESBuildOptions['loader']} */ (lang),
target: 'esnext',
tsconfigRaw: {
compilerOptions: {
// svelte typescript needs this flag to work with type imports
importsNotUsedAsValues: 'preserve',
preserveValueImports: true
}
}
});
mapToRelative(map, filename);
return {
code,
map
};
}
};
}
/**
* @param {import('vite').ResolvedConfig | import('vite').InlineConfig} config
* @returns {{ style: import('svelte/compiler').Preprocessor }}
*/
function viteStyle(config = {}) {
/** @type {Promise<CssTransform> | CssTransform} */
let cssTransform;
/** @type {import('svelte/compiler').Preprocessor} */
const style = async ({ attributes, content, filename = '' }) => {
const ext = attributes.lang ? `.${attributes.lang}` : '.css';
if (attributes.lang && !isCSSRequest(ext)) return;
if (!cssTransform) {
cssTransform = createCssTransform(style, config).then((t) => (cssTransform = t));
}
const transform = await cssTransform;
const suffix = `${lang_sep}${ext}`;
const moduleId = `${filename}${suffix}`;
const { code, map, deps } = await transform(content, moduleId);
removeLangSuffix(map, suffix);
mapToRelative(map, filename);
const dependencies = deps ? Array.from(deps).filter((d) => !d.endsWith(suffix)) : undefined;
return {
code,
map: map ?? undefined,
dependencies
};
};
// @ts-expect-error tag so can be found by v-p-s
style.__resolvedConfig = null;
return { style };
}
/**
* @param {import('svelte/compiler').Preprocessor} style
* @param {import('vite').ResolvedConfig | import('vite').InlineConfig} config
* @returns {Promise<CssTransform>}
*/
async function createCssTransform(style, config) {
/** @type {import('vite').ResolvedConfig} */
let resolvedConfig;
// @ts-expect-error special prop added if running in v-p-s
if (style.__resolvedConfig) {
// @ts-expect-error not typed
resolvedConfig = style.__resolvedConfig;
} else if (isResolvedConfig(config)) {
resolvedConfig = config;
} else {
// default to "build" if no NODE_ENV is set to avoid running in dev mode for svelte-check etc.
const useBuild = !process.env.NODE_ENV || process.env.NODE_ENV === 'production';
const command = useBuild ? 'build' : 'serve';
const defaultMode = useBuild ? 'production' : 'development';
resolvedConfig = await resolveConfig(config, command, defaultMode, defaultMode, false);
}
return async (code, filename) => {
return preprocessCSS(code, filename, resolvedConfig);
};
}
/**
* @param {any} config
* @returns {config is import('vite').ResolvedConfig}
*/
function isResolvedConfig(config) {
return !!config.inlineConfig;
}

View File

@@ -0,0 +1,210 @@
import type { InlineConfig, ResolvedConfig } from 'vite';
import type { CompileOptions, Warning, PreprocessorGroup } from 'svelte/compiler';
import type { Options as InspectorOptions } from '@sveltejs/vite-plugin-svelte-inspector';
export type Options = Omit<SvelteConfig, 'vitePlugin'> & PluginOptionsInline;
interface PluginOptionsInline extends PluginOptions {
/**
* Path to a svelte config file, either absolute or relative to Vite root
*
* set to `false` to ignore the svelte config file
*
* @see https://vitejs.dev/config/#root
*/
configFile?: string | false;
}
export interface PluginOptions {
/**
* A `picomatch` pattern, or array of patterns, which specifies the files the plugin should
* operate on. By default, all svelte files are included.
*
* @see https://github.com/micromatch/picomatch
*/
include?: Arrayable<string>;
/**
* 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.
* Deprecated, use compilerOptions.hmr instead!
*
* @deprecated
* @default true for development, always false for production
*/
hot?: boolean;
/**
* Some Vite plugins can contribute additional preprocessors by defining `api.sveltePreprocess`.
* If you don't want to use them, set this to true to ignore them all or use an array of strings
* with plugin names to specify which.
*
* @default false
*/
ignorePluginPreprocessors?: boolean | string[];
/**
* vite-plugin-svelte automatically handles excluding svelte libraries and reinclusion of their dependencies
* in vite.optimizeDeps.
*
* `disableDependencyReinclusion: true` disables all reinclusions
* `disableDependencyReinclusion: ['foo','bar']` disables reinclusions for dependencies of foo and bar
*
* This should be used for hybrid packages that contain both node and browser dependencies, eg Routify
*
* @default false
*/
disableDependencyReinclusion?: boolean | string[];
/**
* Enable support for Vite's dependency optimization to prebundle Svelte libraries.
*
* To disable prebundling for a specific library, add it to `optimizeDeps.exclude`.
*
* @default true for dev, false for build
*/
prebundleSvelteLibraries?: boolean;
/**
* toggle/configure Svelte Inspector
*
* @default unset for dev, always false for build
*/
inspector?: InspectorOptions | boolean;
/**
* A function to update `compilerOptions` before compilation
*
* `data.filename` - The file to be compiled
* `data.code` - The preprocessed Svelte code
* `data.compileOptions` - The current compiler options
*
* To change part of the compiler options, return an object with the changes you need.
*
* @example
* ```
* ({ filename, compileOptions }) => {
* // Dynamically set runes mode per Svelte file
* if (forceRunesMode(filename) && !compileOptions.runes) {
* return { runes: true };
* }
* }
* ```
*/
dynamicCompileOptions?: (data: {
filename: string;
code: string;
compileOptions: Partial<CompileOptions>;
}) => Promise<Partial<CompileOptions> | void> | Partial<CompileOptions> | void;
/**
* These options are considered experimental and breaking changes to them can occur in any release
*/
experimental?: ExperimentalOptions;
}
export interface SvelteConfig {
/**
* A list of file extensions to be compiled by Svelte
*
* @default ['.svelte']
*/
extensions?: string[];
/**
* An array of preprocessors to transform the Svelte source code before compilation
*
* @see https://svelte.dev/docs#svelte_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
*
* warnings emitted for files in node_modules are logged at the debug level, to see them run
* `DEBUG=vite-plugin-svelte:node-modules-onwarn pnpm build`
*
* @example
* ```
* (warning, defaultHandler) => {
* // ignore some warnings
* if (!['foo','bar'].includes(warning.code)) {
* defaultHandler(warning);
* }
* }
* ```
*
*/
onwarn?: (warning: Warning, defaultHandler: (warning: Warning) => void) => void;
/**
* Options for vite-plugin-svelte
*/
vitePlugin?: PluginOptions;
}
/**
* These options are considered experimental and breaking changes to them can occur in any release
*/
interface ExperimentalOptions {
/**
* send a websocket message with svelte compiler warnings during dev
*
*/
sendWarningsToBrowser?: boolean;
/**
* disable svelte field resolve warnings
*
* @default false
*/
disableSvelteResolveWarnings?: boolean;
compileModule?: CompileModuleOptions;
}
interface CompileModuleOptions {
/**
* infix that must be present in filename
* @default ['.svelte.']
*/
infixes?: string[];
/**
* module extensions
* @default ['.ts','.js']
*/
extensions?: string[];
include?: Arrayable<string>;
exclude?: Arrayable<string>;
}
type Arrayable<T> = T | T[];
export interface VitePreprocessOptions {
/**
* preprocess script block with vite pipeline.
* Since svelte5 this is not needed for typescript anymore
*
* @default false
*/
script?: boolean;
/**
* preprocess style blocks with vite pipeline
*/
style?: boolean | InlineConfig | ResolvedConfig;
}
// eslint-disable-next-line n/no-missing-import
export * from './index.js';

View File

@@ -0,0 +1,25 @@
import type { Processed, CompileResult } from 'svelte/compiler';
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 CompileData {
filename: string;
normalizedFilename: string;
lang: string;
compiled: CompileResult;
ssr: boolean | undefined;
dependencies: string[];
preprocessed: Processed;
}

View File

@@ -0,0 +1,46 @@
import type { CompileOptions } from 'svelte/compiler';
export type SvelteQueryTypes = 'style' | 'script' | 'preprocessed' | 'all';
export interface RequestQuery {
// our own
svelte?: boolean;
type?: SvelteQueryTypes;
sourcemap?: boolean;
compilerOptions?: Pick<
CompileOptions,
'generate' | 'dev' | 'css' | 'customElement' | 'immutable'
>;
// vite specific
url?: boolean;
raw?: boolean;
direct?: boolean;
inline?: boolean;
}
export interface SvelteRequest {
id: string;
cssId: string;
filename: string;
normalizedFilename: string;
query: RequestQuery;
timestamp: number;
ssr: boolean;
raw: boolean;
}
export interface SvelteModuleRequest {
id: string;
filename: string;
normalizedFilename: string;
query: RequestQuery;
timestamp: number;
ssr: boolean;
}
export type IdParser = (id: string, ssr: boolean, timestamp?: number) => SvelteRequest | undefined;
export type ModuleIdParser = (
id: string,
ssr: boolean,
timestamp?: number
) => SvelteModuleRequest | undefined;

View File

@@ -0,0 +1,24 @@
import type { Warning } from 'svelte/compiler';
export interface LogFn extends SimpleLogFn {
(message: string, payload?: unknown, namespace?: string): void;
enabled: boolean;
once: SimpleLogFn;
}
export interface SimpleLogFn {
(message: string, payload?: unknown, namespace?: string): void;
}
export type SvelteWarningsMessage = {
id: string;
filename: string;
normalizedFilename: string;
timestamp: number;
warnings: Warning[]; // allWarnings filtered by warnings where onwarn did not call the default handler
allWarnings: Warning[]; // includes warnings filtered by onwarn and our extra vite plugin svelte warnings
rawWarnings: Warning[]; // raw compiler output
};
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';

View File

@@ -0,0 +1,21 @@
import type { CompileOptions } from 'svelte/compiler';
import type { ViteDevServer } from 'vite';
// eslint-disable-next-line n/no-missing-import
import { VitePluginSvelteStats } from '../utils/vite-plugin-svelte-stats.js';
import type { Options } from '../public.d.ts';
export interface PreResolvedOptions extends Options {
// these options are non-nullable after resolve
compilerOptions: CompileOptions;
// extra options
root: string;
isBuild: boolean;
isServe: boolean;
isDebug: boolean;
}
export interface ResolvedOptions extends PreResolvedOptions {
isProduction: boolean;
server?: ViteDevServer;
stats?: VitePluginSvelteStats;
}

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,191 @@
import * as svelte from 'svelte/compiler';
import { safeBase64Hash } from './hash.js';
import { log } from './log.js';
import {
checkPreprocessDependencies,
createInjectScopeEverythingRulePreprocessorGroup
} from './preprocess.js';
import { mapToRelative } from './sourcemaps.js';
import { enhanceCompileError } from './error.js';
// TODO this is a patched version of https://github.com/sveltejs/vite-plugin-svelte/pull/796/files#diff-3bce0b33034aad4b35ca094893671f7e7ddf4d27254ae7b9b0f912027a001b15R10
// which is closer to the other regexes in at least not falling into commented script
// but ideally would be shared exactly with svelte and other tools that use it
const scriptLangRE =
/<!--[^]*?-->|<script\s+(?:[^>]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/g;
/**
* @returns {import('../types/compile.d.ts').CompileSvelte}
*/
export function createCompileSvelte() {
/** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */
let stats;
const devStylePreprocessor = createInjectScopeEverythingRulePreprocessorGroup();
/** @type {import('../types/compile.d.ts').CompileSvelte} */
return async function compileSvelte(svelteRequest, code, options) {
const { filename, normalizedFilename, cssId, ssr, raw } = svelteRequest;
const { emitCss = true } = options;
/** @type {string[]} */
const dependencies = [];
/** @type {import('svelte/compiler').Warning[]} */
const warnings = [];
if (options.stats) {
if (options.isBuild) {
if (!stats) {
// build is either completely ssr or csr, create stats collector on first compile
// it is then finished in the buildEnd hook.
stats = options.stats.startCollection(`${ssr ? 'ssr' : 'dom'} compile`, {
logInProgress: () => false
});
}
} else {
// dev time ssr, it's a ssr request and there are no stats, assume new page load and start collecting
if (ssr && !stats) {
stats = options.stats.startCollection('ssr compile');
}
// stats are being collected but this isn't an ssr request, assume page loaded and stop collecting
if (!ssr && stats) {
stats.finish();
stats = undefined;
}
// TODO find a way to trace dom compile during dev
// problem: we need to call finish at some point but have no way to tell if page load finished
// also they for hmr updates too
}
}
/** @type {import('svelte/compiler').CompileOptions} */
const compileOptions = {
...options.compilerOptions,
filename,
generate: ssr ? 'server' : 'client'
};
if (compileOptions.hmr && options.emitCss) {
const hash = `s-${safeBase64Hash(normalizedFilename)}`;
compileOptions.cssHash = () => hash;
}
let preprocessed;
let preprocessors = options.preprocess;
if (!options.isBuild && options.emitCss && compileOptions.hmr) {
// inject preprocessor that ensures css hmr works better
if (!Array.isArray(preprocessors)) {
preprocessors = preprocessors
? [preprocessors, devStylePreprocessor]
: [devStylePreprocessor];
} else {
preprocessors = preprocessors.concat(devStylePreprocessor);
}
}
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?.length) {
const checked = checkPreprocessDependencies(filename, preprocessed.dependencies);
if (checked.warnings.length) {
warnings.push(...checked.warnings);
}
if (checked.dependencies.length) {
dependencies.push(...checked.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?.dynamicCompileOptions?.({
filename,
code: finalCode,
compileOptions
});
if (dynamicCompileOptions && log.debug.enabled) {
log.debug(
`dynamic compile options for ${filename}: ${JSON.stringify(dynamicCompileOptions)}`,
undefined,
'compile'
);
}
const finalCompileOptions = dynamicCompileOptions
? {
...compileOptions,
...dynamicCompileOptions
}
: compileOptions;
const endStat = stats?.start(filename);
/** @type {import('svelte/compiler').CompileResult} */
let compiled;
try {
compiled = svelte.compile(finalCode, { ...finalCompileOptions, filename });
// patch output with partial accept until svelte does it
// TODO remove later
if (
options.server?.config.experimental.hmrPartialAccept &&
compiled.js.code.includes('import.meta.hot.accept(')
) {
compiled.js.code = compiled.js.code.replaceAll(
'import.meta.hot.accept(',
'import.meta.hot.acceptExports(["default"],'
);
}
} catch (e) {
enhanceCompileError(e, code, preprocessors);
throw e;
}
if (endStat) {
endStat();
}
mapToRelative(compiled.js?.map, filename);
mapToRelative(compiled.css?.map, filename);
if (warnings.length) {
if (!compiled.warnings) {
compiled.warnings = [];
}
compiled.warnings.push(...warnings);
}
if (!raw) {
// wire css import and code for hmr
const hasCss = compiled.css?.code?.trim()?.length ?? 0 > 0;
// compiler might not emit css with mode none or it may be empty
if (emitCss && hasCss) {
// TODO properly update sourcemap?
compiled.js.code += `\nimport ${JSON.stringify(cssId)};\n`;
}
}
let lang = 'js';
for (const match of code.matchAll(scriptLangRE)) {
if (match[2]) {
lang = match[2];
break;
}
}
return {
filename,
normalizedFilename,
lang,
compiled,
ssr,
dependencies,
preprocessed: preprocessed ?? { code }
};
};
}

View File

@@ -0,0 +1,26 @@
import { createRequire } from 'node:module';
export const SVELTE_IMPORTS = Object.entries(
createRequire(import.meta.url)('svelte/package.json').exports
)
.map(([name, config]) => {
// ignore type only
if (typeof config === 'object' && Object.keys(config).length === 1 && config.types) {
return '';
}
// ignore names
if (name === './package.json' || name === './compiler') {
return '';
}
return name.replace(/^\./, 'svelte');
})
.filter((s) => s.length > 0);
export const SVELTE_EXPORT_CONDITIONS = ['svelte'];
export const FAQ_LINK_MISSING_EXPORTS_CONDITION =
'https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/faq.md#missing-exports-condition';
export const DEFAULT_SVELTE_EXT = ['.svelte'];
export const DEFAULT_SVELTE_MODULE_INFIX = ['.svelte.'];
export const DEFAULT_SVELTE_MODULE_EXT = ['.js', '.ts'];

View File

@@ -0,0 +1,89 @@
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-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,162 @@
import { buildExtendedLogMessage } from './log.js';
/**
* convert an error thrown by svelte.compile to a RollupError so that vite displays it in a user friendly way
* @param {import('svelte/compiler').Warning & Error & {frame?: string}} error a svelte compiler error, which is a mix of Warning and an error
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {import('vite').Rollup.RollupError} the converted error
*/
export function toRollupError(error, options) {
const { filename, frame, start, code, name, stack } = error;
/** @type {import('vite').Rollup.RollupError} */
const rollupError = {
name, // needed otherwise sveltekit coalesce_to_error turns it into a string
id: filename,
message: buildExtendedLogMessage(error), // include filename:line:column so that it's clickable
frame: formatFrameForVite(frame),
code,
stack: options.isBuild || options.isDebug || !frame ? stack : ''
};
if (start) {
rollupError.loc = {
line: start.line,
column: start.column,
file: filename
};
}
return rollupError;
}
/**
* convert an error thrown by svelte.compile to an esbuild PartialMessage
* @param {import('svelte/compiler').Warning & Error & {frame?: string}} error a svelte compiler error, which is a mix of Warning and an error
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {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');
}
/**
*
* @param {string} code the svelte error code
* @see https://github.com/sveltejs/svelte/blob/main/packages/svelte/src/compiler/errors.js
* @returns {boolean}
*/
function couldBeFixedByCssPreprocessor(code) {
return code === 'expected_token' || code === 'unexpected_eof' || code?.startsWith('css_');
}
/**
* @param {import('svelte/compiler').Warning & Error} err a svelte compiler error, which is a mix of Warning and an error
* @param {string} originalCode
* @param {import('../public.d.ts').Options['preprocess']} [preprocessors]
*/
export function enhanceCompileError(err, originalCode, preprocessors) {
preprocessors = arraify(preprocessors ?? []);
/** @type {string[]} */
const additionalMessages = [];
// Handle incorrect CSS preprocessor usage
if (couldBeFixedByCssPreprocessor(err.code)) {
// Reference from Svelte: https://github.com/sveltejs/svelte/blob/9926347ad9dbdd0f3324d5538e25dcb7f5e442f8/packages/svelte/src/compiler/preprocess/index.js#L257
const styleRe =
/<!--[^]*?-->|<style((?:\s+[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)|\s+[^=>'"/]+)*\s*)(?:\/>|>([\S\s]*?)<\/style>)/g;
let m;
while ((m = styleRe.exec(originalCode))) {
// Warn missing lang attribute
if (!m[1]?.includes('lang=')) {
additionalMessages.push('Did you forget to add a lang attribute to your style tag?');
}
// Warn missing style preprocessor
if (
preprocessors.every((p) => p.style == null || p.name === 'inject-scope-everything-rule')
) {
const preprocessorType = m[1]?.match(/lang="(.+?)"/)?.[1] ?? 'style';
additionalMessages.push(
`Did you forget to add a ${preprocessorType} preprocessor? See https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/preprocess.md for more information.`
);
}
}
}
if (additionalMessages.length) {
err.message += '\n\n- ' + additionalMessages.join('\n- ');
}
return err;
}
/**
* @param {T | T[]} value
* @template T
*/
function arraify(value) {
return Array.isArray(value) ? value : [value];
}

View File

@@ -0,0 +1,177 @@
import { readFileSync } from 'node:fs';
import * as svelte from 'svelte/compiler';
import { log } from './log.js';
import { toESBuildError } from './error.js';
import { safeBase64Hash } from './hash.js';
import { normalize } from './id.js';
/**
* @typedef {NonNullable<import('vite').DepOptimizationOptions['esbuildOptions']>} EsbuildOptions
* @typedef {NonNullable<EsbuildOptions['plugins']>[number]} EsbuildPlugin
*/
export const facadeEsbuildSveltePluginName = 'vite-plugin-svelte:facade';
export const facadeEsbuildSvelteModulePluginName = 'vite-plugin-svelte-module:facade';
/**
* @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 filter = /\.svelte(?:\?.*)?$/;
/** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */
let statsCollection;
build.onStart(() => {
statsCollection = options.stats?.startCollection('prebundle library components', {
logResult: (c) => c.stats.length > 1
});
});
build.onLoad({ filter }, 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) {
let css = options.compilerOptions.css;
if (css !== 'injected') {
// TODO ideally we'd be able to externalize prebundled styles too, but for now always put them in the js
css = 'injected';
}
/** @type {import('svelte/compiler').CompileOptions} */
const compileOptions = {
dev: true, // default to dev: true because prebundling is only used in dev
...options.compilerOptions,
css,
filename,
generate: 'client'
};
if (compileOptions.hmr && options.emitCss) {
const hash = `s-${safeBase64Hash(normalize(filename, options.root))}`;
compileOptions.cssHash = () => hash;
}
let preprocessed;
if (options.preprocess) {
try {
preprocessed = await svelte.preprocess(code, options.preprocess, { filename });
} catch (e) {
e.message = `Error while preprocessing ${filename}${e.message ? ` - ${e.message}` : ''}`;
throw e;
}
if (preprocessed.map) compileOptions.sourcemap = preprocessed.map;
}
const finalCode = preprocessed ? preprocessed.code : code;
const dynamicCompileOptions = await options?.dynamicCompileOptions?.({
filename,
code: finalCode,
compileOptions
});
if (dynamicCompileOptions && log.debug.enabled) {
log.debug(
`dynamic compile options for ${filename}: ${JSON.stringify(dynamicCompileOptions)}`,
undefined,
'compile'
);
}
const finalCompileOptions = dynamicCompileOptions
? {
...compileOptions,
...dynamicCompileOptions
}
: compileOptions;
const endStat = statsCollection?.start(filename);
const compiled = svelte.compile(finalCode, finalCompileOptions);
if (endStat) {
endStat();
}
return compiled.js.map
? compiled.js.code + '//# sourceMappingURL=' + compiled.js.map.toUrl()
: compiled.js.code;
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {EsbuildPlugin}
*/
export function esbuildSvelteModulePlugin(options) {
return {
name: 'vite-plugin-svelte-module: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 filter = /\.svelte\.[jt]s(?:\?.*)?$/;
/** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */
let statsCollection;
build.onStart(() => {
statsCollection = options.stats?.startCollection('prebundle library modules', {
logResult: (c) => c.stats.length > 1
});
});
build.onLoad({ filter }, async ({ path: filename }) => {
const code = readFileSync(filename, 'utf8');
try {
const contents = await compileSvelteModule(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 compileSvelteModule(options, { filename, code }, statsCollection) {
const endStat = statsCollection?.start(filename);
const compiled = svelte.compileModule(code, {
dev: options.compilerOptions?.dev ?? true, // default to dev: true because prebundling is only used in dev
filename,
generate: 'client'
});
if (endStat) {
endStat();
}
return compiled.js.map
? compiled.js.code + '//# sourceMappingURL=' + compiled.js.map.toUrl()
: compiled.js.code;
}

View File

@@ -0,0 +1,43 @@
import crypto from 'node:crypto';
const hashes = Object.create(null);
//TODO shorter?
const hash_length = 12;
/**
* replaces +/= in base64 output so they don't interfere
*
* @param {string} input
* @returns {string} base64 hash safe to use in any context
*/
export function safeBase64Hash(input) {
if (hashes[input]) {
return hashes[input];
}
//TODO if performance really matters, use a faster one like xx-hash etc.
// should be evenly distributed because short input length and similarities in paths could cause collisions otherwise
// OR DON'T USE A HASH AT ALL, what about a simple counter?
const md5 = crypto.createHash('md5');
md5.update(input);
const hash = toSafe(md5.digest('base64')).slice(0, hash_length);
hashes[input] = hash;
return hash;
}
/** @type {Record<string, string>} */
const replacements = {
'+': '-',
'/': '_',
'=': ''
};
const replaceRE = new RegExp(`[${Object.keys(replacements).join('')}]`, 'g');
/**
* @param {string} base64
* @returns {string}
*/
function toSafe(base64) {
return base64.replace(replaceRE, (x) => replacements[x]);
}

View File

@@ -0,0 +1,252 @@
import { createFilter, normalizePath } from 'vite';
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { log } from './log.js';
import { DEFAULT_SVELTE_MODULE_EXT, DEFAULT_SVELTE_MODULE_INFIX } from './constants.js';
const VITE_FS_PREFIX = '/@fs/';
const IS_WINDOWS = process.platform === 'win32';
const SUPPORTED_COMPILER_OPTIONS = ['generate', 'dev', 'css', 'customElement', 'immutable'];
const TYPES_WITH_COMPILER_OPTIONS = ['style', 'script', 'all'];
/**
* @param {string} id
* @returns {{ filename: string, rawQuery: string }}
*/
function splitId(id) {
const parts = id.split('?', 2);
const filename = parts[0];
const rawQuery = parts[1];
return { filename, rawQuery };
}
/**
* @param {string} id
* @param {string} filename
* @param {string} rawQuery
* @param {string} root
* @param {number} timestamp
* @param {boolean} ssr
* @returns {import('../types/id.d.ts').SvelteRequest | undefined}
*/
function parseToSvelteRequest(id, filename, rawQuery, root, timestamp, ssr) {
const query = parseRequestQuery(rawQuery);
const rawOrDirect = !!(query.raw || query.direct);
if (query.url || (!query.svelte && rawOrDirect)) {
// skip requests with special vite tags
return;
}
const raw = rawOrDirect;
const normalizedFilename = normalize(filename, root);
const cssId = createVirtualImportId(filename, root, 'style');
return {
id,
filename,
normalizedFilename,
cssId,
query,
timestamp,
ssr,
raw
};
}
/**
* @param {string} filename
* @param {string} root
* @param {import('../types/id.d.ts').SvelteQueryTypes} type
* @returns {string}
*/
function createVirtualImportId(filename, root, type) {
const parts = ['svelte', `type=${type}`];
if (type === 'style') {
parts.push('lang.css');
}
if (existsInRoot(filename, root)) {
filename = root + filename;
} else if (filename.startsWith(VITE_FS_PREFIX)) {
filename = IS_WINDOWS
? filename.slice(VITE_FS_PREFIX.length) // remove /@fs/ from /@fs/C:/...
: filename.slice(VITE_FS_PREFIX.length - 1); // remove /@fs from /@fs/home/user
}
// return same virtual id format as vite-plugin-vue eg ...App.svelte?svelte&type=style&lang.css
return `${filename}?${parts.join('&')}`;
}
/**
* @param {string} rawQuery
* @returns {import('../types/id.d.ts').RequestQuery}
*/
function parseRequestQuery(rawQuery) {
const query = Object.fromEntries(new URLSearchParams(rawQuery));
for (const key in query) {
if (query[key] === '') {
// @ts-expect-error not boolean
query[key] = true;
}
}
const compilerOptions = query.compilerOptions;
if (compilerOptions) {
if (!((query.raw || query.direct) && TYPES_WITH_COMPILER_OPTIONS.includes(query.type))) {
throw new Error(
`Invalid compilerOptions in query ${rawQuery}. CompilerOptions are only supported for raw or direct queries with type in "${TYPES_WITH_COMPILER_OPTIONS.join(
', '
)}" e.g. '?svelte&raw&type=script&compilerOptions={"generate":"server","dev":false}`
);
}
try {
const parsed = JSON.parse(compilerOptions);
const invalid = Object.keys(parsed).filter(
(key) => !SUPPORTED_COMPILER_OPTIONS.includes(key)
);
if (invalid.length) {
throw new Error(
`Invalid compilerOptions in query ${rawQuery}: ${invalid.join(
', '
)}. Supported: ${SUPPORTED_COMPILER_OPTIONS.join(', ')}`
);
}
query.compilerOptions = parsed;
} catch (e) {
log.error('failed to parse request query compilerOptions', e);
throw e;
}
}
return /** @type {import('../types/id.d.ts').RequestQuery}*/ query;
}
/**
* posixify and remove root at start
*
* @param {string} filename
* @param {string} normalizedRoot
* @returns {string}
*/
export function normalize(filename, normalizedRoot) {
return stripRoot(normalizePath(filename), normalizedRoot);
}
/**
* @param {string} filename
* @param {string} root
* @returns {boolean}
*/
function existsInRoot(filename, root) {
if (filename.startsWith(VITE_FS_PREFIX)) {
return false; // vite already tagged it as out of root
}
return fs.existsSync(root + filename);
}
/**
* @param {string} normalizedFilename
* @param {string} normalizedRoot
* @returns {string}
*/
function stripRoot(normalizedFilename, normalizedRoot) {
return normalizedFilename.startsWith(normalizedRoot + '/')
? normalizedFilename.slice(normalizedRoot.length)
: normalizedFilename;
}
/**
* @param {import('../public.d.ts').Options['include'] | undefined} include
* @param {import('../public.d.ts').Options['exclude'] | 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('../public.d.ts').Options['include'] | undefined} include
* @param {import('../public.d.ts').Options['exclude'] | undefined} exclude
* @param {string[]} infixes
* @param {string[]} extensions
* @returns {(filename: string) => boolean}
*/
function buildModuleFilter(include, exclude, infixes, extensions) {
const rollupFilter = createFilter(include, exclude);
return (filename) => {
const basename = path.basename(filename);
return (
rollupFilter(filename) &&
infixes.some((infix) => basename.includes(infix)) &&
extensions.some((ext) => basename.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,
infixes = DEFAULT_SVELTE_MODULE_INFIX,
extensions = DEFAULT_SVELTE_MODULE_EXT
} = options?.experimental?.compileModule ?? {};
const root = options.root;
const normalizedRoot = normalizePath(root);
const filter = buildModuleFilter(include, exclude, infixes, extensions);
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,125 @@
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 === 'server';
compileData = await compileSvelte(svelteRequest, source, {
...options,
// don't use dynamic vite-plugin-svelte defaults here to ensure stable result between ssr,dev and build
compilerOptions: {
dev: false,
css: 'external',
hmr: false,
...svelteRequest.query.compilerOptions
},
emitCss: true
});
} catch (e) {
throw toRollupError(e, options);
}
let result;
if (query.type === 'style') {
result = compileData.compiled.css ?? { code: '', map: null };
} else if (query.type === 'script') {
result = compileData.compiled.js;
} else if (query.type === 'preprocessed') {
result = compileData.preprocessed;
} else if (query.type === 'all' && query.raw) {
return allToRawExports(compileData, source);
} else {
throw new Error(
`invalid "type=${query.type}" in ${id}. supported are script, style, preprocessed, all`
);
}
if (query.direct) {
const supportedDirectTypes = ['script', 'style'];
if (!supportedDirectTypes.includes(query.type)) {
throw new Error(
`invalid "type=${
query.type
}" combined with direct in ${id}. supported are: ${supportedDirectTypes.join(', ')}`
);
}
log.debug(`load returns direct result for ${id}`, undefined, 'load');
let directOutput = result.code;
// @ts-expect-error might not be SourceMap but toUrl check should suffice
if (query.sourcemap && result.map?.toUrl) {
// @ts-expect-error toUrl might not exist
const map = `sourceMappingURL=${result.map.toUrl()}`;
if (query.type === 'style') {
directOutput += `\n\n/*# ${map} */\n`;
} else if (query.type === 'script') {
directOutput += `\n\n//# ${map}\n`;
}
}
return directOutput;
} else if (query.raw) {
log.debug(`load returns raw result for ${id}`, undefined, 'load');
return toRawExports(result);
} else {
throw new Error(`invalid raw mode in ${id}, supported are raw, direct`);
}
}
/**
* turn compileData and source into a flat list of raw exports
*
* @param {import('../types/compile.d.ts').CompileData} compileData
* @param {string} source
*/
function allToRawExports(compileData, source) {
// flatten CompileData
/** @type {Partial<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)
.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,122 @@
import { createRequire } from 'node:module';
import path from 'node:path';
import process from 'node:process';
import fs from 'node:fs';
import { pathToFileURL } from 'node:url';
import { log } from './log.js';
// 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'
];
/**
* @param {string} filePath
* @param {number} timestamp
*/
async function dynamicImportDefault(filePath, timestamp) {
return await import(filePath + '?t=' + timestamp).then((m) => m.default);
}
/**
* @param {import('vite').UserConfig} [viteConfig]
* @param {Partial<import('../public.d.ts').Options>} [inlineOptions]
* @returns {Promise<Partial<import('../public.d.ts').SvelteConfig> | undefined>}
*/
export async function loadSvelteConfig(viteConfig, inlineOptions) {
if (inlineOptions?.configFile === false) {
return;
}
const configFile = findConfigToLoad(viteConfig, inlineOptions);
if (configFile) {
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)))
: // eslint-disable-next-line no-undef
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('../public.d.ts').Options> | undefined} inlineOptions
* @returns {string | undefined}
*/
function findConfigToLoad(viteConfig, inlineOptions) {
const root = viteConfig?.root || process.cwd();
if (inlineOptions?.configFile) {
const abolutePath = path.isAbsolute(inlineOptions.configFile)
? inlineOptions.configFile
: path.resolve(root, inlineOptions.configFile);
if (!fs.existsSync(abolutePath)) {
throw new Error(`failed to find svelte config file ${abolutePath}.`);
}
return abolutePath;
} else {
const existingKnownConfigFiles = knownSvelteConfigNames
.map((candidate) => path.resolve(root, candidate))
.filter((file) => fs.existsSync(file));
if (existingKnownConfigFiles.length === 0) {
log.debug(`no svelte config found at ${root}`, undefined, 'config');
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,277 @@
/* 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(`${prefix}`),
enabled: false,
isDebug: true
},
info: {
color: cyan,
log: console.log,
enabled: true
},
warn: {
color: yellow,
log: console.warn,
enabled: true
},
error: {
color: red,
log: console.error,
enabled: true
},
silent: {
enabled: false
}
};
/** @type {import('../types/log.d.ts').LogLevel} */
let _level = 'info';
/**
* @param {import('../types/log.d.ts').LogLevel} level
* @returns {void}
*/
function setLevel(level) {
if (level === _level) {
return;
}
const levelIndex = levels.indexOf(level);
if (levelIndex > -1) {
_level = level;
for (let i = 0; i < levels.length; i++) {
loggers[levels[i]].enabled = i >= levelIndex;
}
} else {
_log(loggers.error, `invalid log level: ${level} `);
}
}
/**
* @param {any} logger
* @param {string} message
* @param {any} [payload]
* @param {string} [namespace]
* @returns
*/
function _log(logger, message, payload, namespace) {
if (!logger.enabled) {
return;
}
if (logger.isDebug) {
let log = logger.log;
if (namespace) {
if (!isDebugNamespaceEnabled(namespace)) {
return;
}
log = logger.log.extend(namespace);
}
if (payload !== undefined) {
log(message, payload);
} else {
log(message);
}
} else {
logger.log(
logger.color(
`${new Date().toLocaleTimeString()} [${prefix}${
namespace ? `:${namespace}` : ''
}] ${message}`
)
);
if (payload) {
logger.log(payload);
}
}
}
/**
* @param {import('../types/log.d.ts').LogLevel} level
* @returns {import('../types/log.d.ts').LogFn}
*/
function createLogger(level) {
const logger = loggers[level];
const logFn = /** @type {import('../types/log.d.ts').LogFn} */ (_log.bind(null, logger));
/** @type {Set<string>} */
const logged = new Set();
/** @type {import('../types/log.d.ts').SimpleLogFn} */
const once = function (message, payload, namespace) {
if (!logger.enabled || logged.has(message)) {
return;
}
logged.add(message);
logFn.apply(null, [message, payload, namespace]);
};
Object.defineProperty(logFn, 'enabled', {
get() {
return logger.enabled;
}
});
Object.defineProperty(logFn, 'once', {
get() {
return once;
}
});
return logFn;
}
export const log = {
debug: createLogger('debug'),
info: createLogger('info'),
warn: createLogger('warn'),
error: createLogger('error'),
setLevel
};
/**
* @param {import('../types/id.d.ts').SvelteRequest | import('../types/id.d.ts').SvelteModuleRequest} svelteRequest
* @param {import('svelte/compiler').Warning[]} warnings
* @param {import('../types/options.d.ts').ResolvedOptions} options
*/
export function logCompilerWarnings(svelteRequest, warnings, options) {
const { emitCss, onwarn, isBuild } = options;
const sendViaWS = !isBuild && options.experimental?.sendWarningsToBrowser;
let warn = isBuild ? warnBuild : warnDev;
/** @type {import('svelte/compiler').Warning[]} */
const handledByDefaultWarn = [];
const 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/compiler').Warning) => void} */
warn = (w) => {
handledByDefaultWarn.push(w);
_warn(w);
};
}
allWarnings.forEach((warning) => {
if (onwarn) {
onwarn(warning, warn);
} else {
warn(warning);
}
});
if (sendViaWS) {
/** @type {import('../types/log.d.ts').SvelteWarningsMessage} */
const message = {
id: svelteRequest.id,
filename: svelteRequest.filename,
normalizedFilename: svelteRequest.normalizedFilename,
timestamp: svelteRequest.timestamp,
warnings: handledByDefaultWarn, // allWarnings filtered by warnings where onwarn did not call the default handler
allWarnings, // includes warnings filtered by onwarn and our extra vite plugin svelte warnings
rawWarnings: warnings // raw compiler output
};
log.debug(`sending svelte:warnings message for ${svelteRequest.normalizedFilename}`);
options.server?.ws?.send('svelte:warnings', message);
}
}
/**
* @param {import('svelte/compiler').Warning} warning
* @param {boolean} isBuild
* @param {boolean} [emitCss]
* @returns {boolean}
*/
function ignoreCompilerWarning(warning, isBuild, emitCss) {
return (
(!emitCss && warning.code === 'css_unused_selector') || // same as rollup-plugin-svelte
(!isBuild && isNoScopableElementWarning(warning))
);
}
/**
*
* @param {import('svelte/compiler').Warning} warning
* @returns {boolean}
*/
function isNoScopableElementWarning(warning) {
// see https://github.com/sveltejs/vite-plugin-svelte/issues/153
return warning.code === 'css_unused_selector' && warning.message.includes('"*"');
}
/**
*
* @param {import('svelte/compiler').Warning[]} warnings
* @param {boolean} isBuild
* @returns {import('svelte/compiler').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/compiler').Warning} w
*/
function warnDev(w) {
if (w.filename?.includes('node_modules')) {
if (isDebugNamespaceEnabled('node-modules-onwarn')) {
log.debug(buildExtendedLogMessage(w), undefined, 'node-modules-onwarn');
}
} else if (log.info.enabled) {
log.info(buildExtendedLogMessage(w));
}
}
/**
* @param {import('svelte/compiler').Warning & {frame?: string}} w
*/
function warnBuild(w) {
if (w.filename?.includes('node_modules')) {
if (isDebugNamespaceEnabled('node-modules-onwarn')) {
log.debug(buildExtendedLogMessage(w), w.frame, 'node-modules-onwarn');
}
} else if (log.warn.enabled) {
log.warn(buildExtendedLogMessage(w), w.frame);
}
}
/**
* @param {import('svelte/compiler').Warning} w
*/
export function buildExtendedLogMessage(w) {
const parts = [];
if (w.filename) {
parts.push(w.filename);
}
if (w.start) {
parts.push(':', w.start.line, ':', w.start.column);
}
if (w.message) {
if (parts.length > 0) {
parts.push(' ');
}
parts.push(w.message);
}
return parts.join('');
}
/**
* @param {string} namespace
* @returns {boolean}
*/
export function isDebugNamespaceEnabled(namespace) {
return debug.enabled(`${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,653 @@
import process from 'node:process';
import {
defaultClientMainFields,
defaultServerMainFields,
defaultClientConditions,
defaultServerConditions,
normalizePath
} from 'vite';
import { isDebugNamespaceEnabled, log } from './log.js';
import { loadSvelteConfig } from './load-svelte-config.js';
import {
DEFAULT_SVELTE_EXT,
FAQ_LINK_MISSING_EXPORTS_CONDITION,
SVELTE_EXPORT_CONDITIONS,
SVELTE_IMPORTS
} from './constants.js';
import path from 'node:path';
import {
esbuildSvelteModulePlugin,
esbuildSveltePlugin,
facadeEsbuildSvelteModulePluginName,
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';
const allowedPluginOptions = new Set([
'include',
'exclude',
'emitCss',
'hot',
'ignorePluginPreprocessors',
'disableDependencyReinclusion',
'prebundleSvelteLibraries',
'inspector',
'dynamicCompileOptions',
'experimental'
]);
const knownRootOptions = new Set(['extensions', 'compilerOptions', 'preprocess', 'onwarn']);
const allowedInlineOptions = new Set(['configFile', ...allowedPluginOptions, ...knownRootOptions]);
/**
* @param {Partial<import('../public.d.ts').Options>} [inlineOptions]
*/
export function validateInlineOptions(inlineOptions) {
const invalidKeys = Object.keys(inlineOptions || {}).filter(
(key) => !allowedInlineOptions.has(key)
);
if (invalidKeys.length) {
log.warn(`invalid plugin options "${invalidKeys.join(', ')}" in inline config`, inlineOptions);
}
}
/**
* @param {Partial<import('../public.d.ts').SvelteConfig>} [config]
* @returns {Partial<import('../public.d.ts').Options> | undefined}
*/
function convertPluginOptions(config) {
if (!config) {
return;
}
const invalidRootOptions = Object.keys(config).filter((key) => allowedPluginOptions.has(key));
if (invalidRootOptions.length > 0) {
throw new Error(
`Invalid options in svelte config. Move the following options into 'vitePlugin:{...}': ${invalidRootOptions.join(
', '
)}`
);
}
if (!config.vitePlugin) {
return config;
}
const pluginOptions = config.vitePlugin;
const pluginOptionKeys = Object.keys(pluginOptions);
const rootOptionsInPluginOptions = pluginOptionKeys.filter((key) => knownRootOptions.has(key));
if (rootOptionsInPluginOptions.length > 0) {
throw new Error(
`Invalid options in svelte config under vitePlugin:{...}', move them to the config root : ${rootOptionsInPluginOptions.join(
', '
)}`
);
}
const duplicateOptions = pluginOptionKeys.filter((key) =>
Object.prototype.hasOwnProperty.call(config, key)
);
if (duplicateOptions.length > 0) {
throw new Error(
`Invalid duplicate options in svelte config under vitePlugin:{...}', they are defined in root too and must only exist once: ${duplicateOptions.join(
', '
)}`
);
}
const unknownPluginOptions = pluginOptionKeys.filter((key) => !allowedPluginOptions.has(key));
if (unknownPluginOptions.length > 0) {
log.warn(
`ignoring unknown plugin options in svelte config under vitePlugin:{...}: ${unknownPluginOptions.join(
', '
)}`
);
unknownPluginOptions.forEach((unkownOption) => {
// @ts-expect-error not typed
delete pluginOptions[unkownOption];
});
}
/** @type {import('../public.d.ts').Options} */
const result = {
...config,
...pluginOptions
};
// @ts-expect-error it exists
delete result.vitePlugin;
return result;
}
/**
* used in config phase, merges the default options, svelte config, and inline options
* @param {Partial<import('../public.d.ts').Options> | undefined} inlineOptions
* @param {import('vite').UserConfig} viteUserConfig
* @param {import('vite').ConfigEnv} viteEnv
* @returns {Promise<import('../types/options.d.ts').PreResolvedOptions>}
*/
export async function preResolveOptions(inlineOptions, viteUserConfig, viteEnv) {
if (!inlineOptions) {
inlineOptions = {};
}
/** @type {import('vite').UserConfig} */
const viteConfigWithResolvedRoot = {
...viteUserConfig,
root: resolveViteRoot(viteUserConfig)
};
const isBuild = viteEnv.command === 'build';
/** @type {Partial<import('../types/options.d.ts').PreResolvedOptions>} */
const defaultOptions = {
extensions: DEFAULT_SVELTE_EXT,
emitCss: true,
prebundleSvelteLibraries: !isBuild
};
const svelteConfig = convertPluginOptions(
await loadSvelteConfig(viteConfigWithResolvedRoot, inlineOptions)
);
/** @type {Partial<import('../types/options.d.ts').PreResolvedOptions>} */
const extraOptions = {
root: viteConfigWithResolvedRoot.root,
isBuild,
isServe: viteEnv.command === 'serve',
isDebug: process.env.DEBUG != null
};
const merged = /** @type {import('../types/options.d.ts').PreResolvedOptions} */ (
mergeConfigs(defaultOptions, svelteConfig, inlineOptions, extraOptions)
);
// configFile of svelteConfig contains the absolute path it was loaded from,
// prefer it over the possibly relative inline path
if (svelteConfig?.configFile) {
merged.configFile = svelteConfig.configFile;
}
return merged;
}
/**
* @template T
* @param {(Partial<T> | undefined)[]} configs
* @returns T
*/
function mergeConfigs(...configs) {
/** @type {Partial<T>} */
let result = {};
for (const config of configs.filter((x) => x != null)) {
result = deepmerge(result, /** @type {Partial<T>} */ (config), {
// replace arrays
arrayMerge: (target, source) => source ?? target
});
}
return /** @type {T} */ result;
}
/**
* used in configResolved phase, merges a contextual default config, pre-resolved options, and some preprocessors. also validates the final config.
*
* @param {import('../types/options.d.ts').PreResolvedOptions} preResolveOptions
* @param {import('vite').ResolvedConfig} viteConfig
* @param {import('./vite-plugin-svelte-cache.js').VitePluginSvelteCache} cache
* @returns {import('../types/options.d.ts').ResolvedOptions}
*/
export function resolveOptions(preResolveOptions, viteConfig, cache) {
const css = preResolveOptions.emitCss ? 'external' : 'injected';
/** @type {Partial<import('../public.d.ts').Options>} */
const defaultOptions = {
compilerOptions: {
css,
dev: !viteConfig.isProduction,
hmr:
!viteConfig.isProduction &&
!preResolveOptions.isBuild &&
viteConfig.server &&
viteConfig.server.hmr !== false
}
};
/** @type {Partial<import('../types/options.d.ts').ResolvedOptions>} */
const extraOptions = {
root: viteConfig.root,
isProduction: viteConfig.isProduction
};
const merged = /** @type {import('../types/options.d.ts').ResolvedOptions}*/ (
mergeConfigs(defaultOptions, preResolveOptions, extraOptions)
);
removeIgnoredOptions(merged);
handleDeprecatedOptions(merged);
addExtraPreprocessors(merged, viteConfig);
enforceOptionsForHmr(merged, viteConfig);
enforceOptionsForProduction(merged);
// 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
* @param {import('vite').ResolvedConfig} viteConfig
*/
function enforceOptionsForHmr(options, viteConfig) {
if (options.hot) {
log.warn(
'svelte 5 has hmr integrated in core. Please remove the vitePlugin.hot option and use compilerOptions.hmr instead'
);
delete options.hot;
options.compilerOptions.hmr = true;
}
if (options.compilerOptions.hmr && viteConfig.server?.hmr === false) {
log.warn(
'vite config server.hmr is false but compilerOptions.hmr is true. Forcing compilerOptions.hmr to false as it would not work.'
);
options.compilerOptions.hmr = false;
}
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
*/
function enforceOptionsForProduction(options) {
if (options.isProduction) {
if (options.compilerOptions.hmr) {
log.warn(
'you are building for production but compilerOptions.hmr is true, forcing it to false'
);
options.compilerOptions.hmr = false;
}
if (options.compilerOptions.dev) {
log.warn(
'you are building for production but compilerOptions.dev is true, forcing it to false'
);
options.compilerOptions.dev = false;
}
}
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
*/
function removeIgnoredOptions(options) {
const ignoredCompilerOptions = ['generate', 'format', 'filename'];
if (options.compilerOptions.hmr && options.emitCss) {
ignoredCompilerOptions.push('cssHash');
}
const passedCompilerOptions = Object.keys(options.compilerOptions || {});
const passedIgnored = passedCompilerOptions.filter((o) => ignoredCompilerOptions.includes(o));
if (passedIgnored.length) {
log.warn(
`The following Svelte compilerOptions are controlled by vite-plugin-svelte and essential to its functionality. User-specified values are ignored. Please remove them from your configuration: ${passedIgnored.join(
', '
)}`
);
passedIgnored.forEach((ignored) => {
// @ts-expect-error string access
delete options.compilerOptions[ignored];
});
}
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
*/
function handleDeprecatedOptions(options) {
const experimental = /** @type {Record<string, any>} */ (options.experimental);
if (experimental) {
for (const promoted of ['prebundleSvelteLibraries', 'inspector', 'dynamicCompileOptions']) {
if (experimental[promoted]) {
//@ts-expect-error untyped assign
options[promoted] = experimental[promoted];
delete experimental[promoted];
log.warn(
`Option "experimental.${promoted}" is no longer experimental and has moved to "${promoted}". Please update your Svelte or Vite config.`
);
}
}
if (experimental.generateMissingPreprocessorSourcemaps) {
log.warn('experimental.generateMissingPreprocessorSourcemaps has been removed.');
}
}
}
/**
* vite passes unresolved `root`option to config hook but we need the resolved value, so do it here
*
* @see https://github.com/sveltejs/vite-plugin-svelte/issues/113
* @see https://github.com/vitejs/vite/blob/43c957de8a99bb326afd732c962f42127b0a4d1e/packages/vite/src/node/config.ts#L293
*
* @param {import('vite').UserConfig} viteConfig
* @returns {string | undefined}
*/
function resolveViteRoot(viteConfig) {
return normalizePath(viteConfig.root ? path.resolve(viteConfig.root) : process.cwd());
}
/**
* @param {import('../types/options.d.ts').PreResolvedOptions} options
* @param {import('vite').UserConfig} config
* @returns {Promise<Partial<import('vite').UserConfig>>}
*/
export async function buildExtraViteConfig(options, config) {
/** @type {Partial<import('vite').UserConfig>} */
const extraViteConfig = {
resolve: {
dedupe: [...SVELTE_IMPORTS]
}
// this option is still awaiting a PR in vite to be supported
// see https://github.com/sveltejs/vite-plugin-svelte/issues/60
// knownJsSrcExtensions: options.extensions
};
const extraSvelteConfig = buildExtraConfigForSvelte(config);
const extraDepsConfig = await buildExtraConfigForDependencies(options, config);
// merge extra svelte and deps config, but make sure dep values are not contradicting svelte
extraViteConfig.optimizeDeps = {
include: [
...extraSvelteConfig.optimizeDeps.include,
...extraDepsConfig.optimizeDeps.include.filter(
(dep) => !isDepExcluded(dep, extraSvelteConfig.optimizeDeps.exclude)
)
],
exclude: [
...extraSvelteConfig.optimizeDeps.exclude,
...extraDepsConfig.optimizeDeps.exclude.filter(
(dep) => !isDepIncluded(dep, extraSvelteConfig.optimizeDeps.include)
)
]
};
extraViteConfig.ssr = {
external: [
...extraSvelteConfig.ssr.external,
...extraDepsConfig.ssr.external.filter(
(dep) => !isDepNoExternaled(dep, extraSvelteConfig.ssr.noExternal)
)
],
noExternal: [
...extraSvelteConfig.ssr.noExternal,
...extraDepsConfig.ssr.noExternal.filter(
(dep) => !isDepExternaled(dep, extraSvelteConfig.ssr.external)
)
]
};
// handle prebundling for svelte files
if (options.prebundleSvelteLibraries) {
extraViteConfig.optimizeDeps = {
...extraViteConfig.optimizeDeps,
// Experimental Vite API to allow these extensions to be scanned and prebundled
extensions: options.extensions ?? ['.svelte'],
// Add 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: () => {} },
{ name: facadeEsbuildSvelteModulePluginName, setup: () => {} }
]
}
};
}
// enable hmrPartialAccept if not explicitly disabled
if (config.experimental?.hmrPartialAccept !== false) {
log.debug('enabling "experimental.hmrPartialAccept" in vite config', undefined, 'config');
extraViteConfig.experimental = { hmrPartialAccept: true };
}
validateViteConfig(extraViteConfig, config, options);
return extraViteConfig;
}
/**
* @param {Partial<import('vite').UserConfig>} extraViteConfig
* @param {import('vite').UserConfig} config
* @param {import('../types/options.d.ts').PreResolvedOptions} options
*/
function validateViteConfig(extraViteConfig, config, options) {
const { prebundleSvelteLibraries, isBuild } = options;
if (prebundleSvelteLibraries) {
/** @type {(option: 'dev' | 'build' | boolean)=> boolean} */
const isEnabled = (option) => option !== true && option !== (isBuild ? 'build' : 'dev');
/** @type {(name: string, value: 'dev' | 'build' | boolean, recommendation: string)=> void} */
const logWarning = (name, value, recommendation) =>
log.warn.once(
`Incompatible options: \`prebundleSvelteLibraries: true\` and vite \`${name}: ${JSON.stringify(
value
)}\` ${isBuild ? 'during build.' : '.'} ${recommendation}`
);
const viteOptimizeDepsDisabled = config.optimizeDeps?.disabled ?? 'build'; // fall back to vite default
const isOptimizeDepsEnabled = isEnabled(viteOptimizeDepsDisabled);
if (!isBuild && !isOptimizeDepsEnabled) {
logWarning(
'optimizeDeps.disabled',
viteOptimizeDepsDisabled,
'Forcing `optimizeDeps.disabled: "build"`. Disable prebundleSvelteLibraries or update your vite config to enable optimizeDeps during dev.'
);
if (!extraViteConfig.optimizeDeps) {
extraViteConfig.optimizeDeps = {};
}
extraViteConfig.optimizeDeps.disabled = 'build';
} else if (isBuild && isOptimizeDepsEnabled) {
logWarning(
'optimizeDeps.disabled',
viteOptimizeDepsDisabled,
'Disable optimizeDeps or prebundleSvelteLibraries for build if you experience errors.'
);
}
}
}
/**
* @param {import('../types/options.d.ts').PreResolvedOptions} options
* @param {import('vite').UserConfig} config
* @returns {Promise<import('vitefu').CrawlFrameworkPkgsResult>}
*/
async function buildExtraConfigForDependencies(options, config) {
// extra handling for svelte dependencies in the project
const packagesWithoutSvelteExportsCondition = new Set();
const depsConfig = await crawlFrameworkPkgs({
root: options.root,
isBuild: options.isBuild,
viteUserConfig: config,
isFrameworkPkgByJson(pkgJson) {
let hasSvelteCondition = false;
if (typeof pkgJson.exports === 'object') {
// use replacer as a simple way to iterate over nested keys
JSON.stringify(pkgJson.exports, (key, value) => {
if (SVELTE_EXPORT_CONDITIONS.includes(key)) {
hasSvelteCondition = true;
}
return value;
});
}
const hasSvelteField = !!pkgJson.svelte;
if (hasSvelteField && !hasSvelteCondition) {
packagesWithoutSvelteExportsCondition.add(`${pkgJson.name}@${pkgJson.version}`);
}
return hasSvelteCondition || hasSvelteField;
},
isSemiFrameworkPkgByJson(pkgJson) {
return !!pkgJson.dependencies?.svelte || !!pkgJson.peerDependencies?.svelte;
},
isFrameworkPkgByName(pkgName) {
const isNotSveltePackage = isCommonDepWithoutSvelteField(pkgName);
if (isNotSveltePackage) {
return false;
} else {
return undefined;
}
}
});
if (
!options.experimental?.disableSvelteResolveWarnings &&
packagesWithoutSvelteExportsCondition?.size > 0
) {
log.warn(
`WARNING: The following packages have a svelte field in their package.json but no exports condition for svelte.\n\n${[
...packagesWithoutSvelteExportsCondition
].join('\n')}\n\nPlease see ${FAQ_LINK_MISSING_EXPORTS_CONDITION} for details.`
);
}
log.debug('extra config for dependencies generated by vitefu', depsConfig, 'config');
if (options.prebundleSvelteLibraries) {
// prebundling enabled, so we don't need extra dependency excludes
depsConfig.optimizeDeps.exclude = [];
// but keep dependency reinclusions of explicit user excludes
const userExclude = config.optimizeDeps?.exclude;
depsConfig.optimizeDeps.include = !userExclude
? []
: depsConfig.optimizeDeps.include.filter((dep) => {
// reincludes look like this: foo > bar > baz
// in case foo or bar are excluded, we have to retain the reinclude even with prebundling
return (
dep.includes('>') &&
dep
.split('>')
.slice(0, -1)
.some((d) => isDepExcluded(d.trim(), userExclude))
);
});
}
if (options.disableDependencyReinclusion === true) {
depsConfig.optimizeDeps.include = depsConfig.optimizeDeps.include.filter(
(dep) => !dep.includes('>')
);
} else if (Array.isArray(options.disableDependencyReinclusion)) {
const disabledDeps = options.disableDependencyReinclusion;
depsConfig.optimizeDeps.include = depsConfig.optimizeDeps.include.filter((dep) => {
if (!dep.includes('>')) return true;
const trimDep = dep.replace(/\s+/g, '');
return disabledDeps.some((disabled) => trimDep.includes(`${disabled}>`));
});
}
log.debug('post-processed extra config for dependencies', depsConfig, 'config');
return depsConfig;
}
/**
* @param {import('vite').UserConfig} config
* @returns {import('vite').UserConfig & { optimizeDeps: { include: string[], exclude:string[] }, ssr: { noExternal:(string|RegExp)[], external: string[] } } }
*/
function buildExtraConfigForSvelte(config) {
// include svelte imports for optimization unless explicitly excluded
/** @type {string[]} */
const include = [];
/** @type {string[]} */
const exclude = [];
if (!isDepExcluded('svelte', config.optimizeDeps?.exclude ?? [])) {
const svelteImportsToInclude = SVELTE_IMPORTS.filter(
(si) => !(si.endsWith('/server') || si.includes('/server/'))
);
log.debug(
`adding bare svelte packages to optimizeDeps.include: ${svelteImportsToInclude.join(', ')} `,
undefined,
'config'
);
include.push(...svelteImportsToInclude);
} else {
log.debug(
'"svelte" is excluded in optimizeDeps.exclude, skipped adding it to include.',
undefined,
'config'
);
}
/** @type {(string | RegExp)[]} */
const noExternal = [];
/** @type {string[]} */
const external = [];
// add svelte to ssr.noExternal unless it is present in ssr.external
// so it is correctly resolving according to the conditions in sveltes exports map
if (!isDepExternaled('svelte', config.ssr?.external ?? [])) {
noExternal.push('svelte', /^svelte\//);
}
// esm-env needs to be bundled by default for the development/production condition
// be properly used by svelte
if (!isDepExternaled('esm-env', config.ssr?.external ?? [])) {
noExternal.push('esm-env');
}
return { optimizeDeps: { include, exclude }, ssr: { noExternal, external } };
}
/**
* @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));
}
const facadeEsbuildSvelteModulePlugin = viteConfig.optimizeDeps.esbuildOptions?.plugins?.find(
(plugin) => plugin.name === facadeEsbuildSvelteModulePluginName
);
if (facadeEsbuildSvelteModulePlugin) {
Object.assign(facadeEsbuildSvelteModulePlugin, esbuildSvelteModulePlugin(options));
}
}
/**
* Mutates `config` to ensure `resolve.mainFields` is set. If unset, it emulates Vite's default fallback.
* @param {string} name
* @param {import('vite').EnvironmentOptions} config
* @param {{ isSsrTargetWebworker?: boolean }} opts
*/
export function ensureConfigEnvironmentMainFields(name, config, opts) {
config.resolve ??= {};
if (config.resolve.mainFields == null) {
if (config.consumer === 'client' || name === 'client' || opts.isSsrTargetWebworker) {
config.resolve.mainFields = [...defaultClientMainFields];
} else {
config.resolve.mainFields = [...defaultServerMainFields];
}
}
return true;
}
/**
* Mutates `config` to ensure `resolve.conditions` is set. If unset, it emulates Vite's default fallback.
* @param {string} name
* @param {import('vite').EnvironmentOptions} config
* @param {{ isSsrTargetWebworker?: boolean }} opts
*/
export function ensureConfigEnvironmentConditions(name, config, opts) {
config.resolve ??= {};
if (config.resolve.conditions == null) {
if (config.consumer === 'client' || name === 'client' || opts.isSsrTargetWebworker) {
config.resolve.conditions = [...defaultClientConditions];
} else {
config.resolve.conditions = [...defaultServerConditions];
}
}
}
/**
* @template T
* @param {T | T[]} value
* @returns {T[]}
*/
function arraify(value) {
return Array.isArray(value) ? value : [value];
}

View File

@@ -0,0 +1,173 @@
import MagicString from 'magic-string';
import { log } from './log.js';
import path from 'node:path';
import { normalizePath } from 'vite';
/**
* this appends a *{} rule to component styles to force the svelte compiler to add style classes to all nodes
* That means adding/removing class rules from <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/compiler').PreprocessorGroup}
*/
export function createInjectScopeEverythingRulePreprocessorGroup() {
return {
name: 'inject-scope-everything-rule',
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/compiler').PreprocessorGroup[],
* appendPreprocessors: import('svelte/compiler').PreprocessorGroup[]
* }}
*/
function buildExtraPreprocessors(options, config) {
/** @type {import('svelte/compiler').PreprocessorGroup[]} */
const prependPreprocessors = [];
/** @type {import('svelte/compiler').PreprocessorGroup[]} */
const appendPreprocessors = [];
// @ts-expect-error not typed
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-expect-error not typed
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(', ')}`,
undefined,
'preprocess'
);
}
if (included.length > 0) {
log.debug(
`Adding svelte preprocessors defined by these vite plugins: ${included
.map((p) => p.name)
.join(', ')}`,
undefined,
'preprocess'
);
appendPreprocessors.push(...pluginsWithPreprocessors.map((p) => p.api.sveltePreprocess));
}
return { prependPreprocessors, appendPreprocessors };
}
/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @param {import('vite').ResolvedConfig} config
*/
export function addExtraPreprocessors(options, config) {
const { prependPreprocessors, appendPreprocessors } = buildExtraPreprocessors(options, config);
if (prependPreprocessors.length > 0 || appendPreprocessors.length > 0) {
if (!options.preprocess) {
options.preprocess = [...prependPreprocessors, ...appendPreprocessors];
} else if (Array.isArray(options.preprocess)) {
options.preprocess.unshift(...prependPreprocessors);
options.preprocess.push(...appendPreprocessors);
} else {
options.preprocess = [...prependPreprocessors, options.preprocess, ...appendPreprocessors];
}
}
}
/**
*
* @param filename {string}
* @param dependencies {string[]}
* @returns {({dependencies: string[], warnings:import('svelte/compiler').Warning[] })}
*/
export function checkPreprocessDependencies(filename, dependencies) {
/** @type {import('svelte/compiler').Warning[]} */
const warnings = [];
// to find self, we have to compare normalized filenames, but must keep the original values in `dependencies`
// because otherwise file watching on windows doesn't work
// so we track idx and filter by that in the end
/** @type {number[]} */
const selfIdx = [];
const normalizedFullFilename = normalizePath(filename);
const normalizedDeps = dependencies.map(normalizePath);
for (let i = 0; i < normalizedDeps.length; i++) {
if (normalizedDeps[i] === normalizedFullFilename) {
selfIdx.push(i);
}
}
const hasSelfDependency = selfIdx.length > 0;
if (hasSelfDependency) {
warnings.push({
code: 'vite-plugin-svelte-preprocess-depends-on-self',
message:
'svelte.preprocess returned this file as a dependency of itself. This can be caused by an invalid configuration or importing generated code that depends on .svelte files (eg. tailwind base css)',
filename
});
}
if (dependencies.length > 10) {
warnings.push({
code: 'vite-plugin-svelte-preprocess-many-dependencies',
message: `svelte.preprocess depends on more than 10 external files which can cause slow builds and poor DX, try to reduce them. Found: ${dependencies.join(
', '
)}`,
filename
});
}
return {
dependencies: hasSelfDependency
? dependencies.filter((_, i) => !selfIdx.includes(i)) // remove self dependency
: dependencies,
warnings
};
}

View File

@@ -0,0 +1,82 @@
import path from 'node:path';
import process from 'node:process';
const IS_WINDOWS = process.platform === 'win32';
/**
* @typedef {{
* file?: string;
* sources?: string[];
* sourceRoot?: string;
* }} SourceMapFileRefs
*/
/**
* convert absolute paths in sourcemap file refs to their relative equivalents to avoid leaking fs info
*
* map is modified in place.
*
* @param {SourceMapFileRefs | undefined} map sourcemap
* @param {string} filename absolute path to file the sourcemap is for
*/
export function mapToRelative(map, filename) {
if (!map) {
return;
}
const sourceRoot = map.sourceRoot;
const dirname = path.dirname(filename);
/** @type {(s: string) => string} */
const toRelative = (s) => {
if (!s) {
return s;
}
/** @type {string} */
let sourcePath;
if (s.startsWith('file:///')) {
// windows has file:///C:/foo and posix has file:///foo, so we have to remove one extra on windows
sourcePath = s.slice(IS_WINDOWS ? 8 : 7);
} else if (sourceRoot) {
const sep = sourceRoot[sourceRoot.length - 1] === '/' || s[0] === '/' ? '' : '/';
sourcePath = `${sourceRoot}${sep}${s}`;
} else {
sourcePath = s;
}
return path.isAbsolute(sourcePath) ? path.relative(dirname, sourcePath) : sourcePath;
};
if (map.file) {
map.file = path.basename(filename);
}
if (map.sources) {
map.sources = map.sources.map(toRelative);
}
if (map.sourceRoot) {
// we have prepended sourceRoot and computed relative paths from it
// remove it here to avoid downstream processing prepending it again
delete map.sourceRoot;
}
}
/**
* vitePreprocess uses an extra lang extension to tell vite about the type of preprocessor to use
* This function removes it afterwards to get back working file refs
*
* map is modified in place.
*
* @param {SourceMapFileRefs | undefined} map the output sourcemap
* @param {string} suffix the suffix to remove
*/
export function removeLangSuffix(map, suffix) {
if (!map) {
return;
}
/** @type {(s:string)=> string} */
const removeSuffix = (s) => (s?.endsWith(suffix) ? s.slice(0, -1 * suffix.length) : s);
if (map.file) {
map.file = removeSuffix(map.file);
}
if (map.sources) {
map.sources = map.sources.map(removeSuffix);
}
}

View File

@@ -0,0 +1,12 @@
import { VERSION } from 'svelte/compiler';
/**
* @type {boolean}
*/
export const isSvelte5 = VERSION.startsWith('5.');
/**
* @type {boolean}
*/
export const isSvelte5WithHMRSupport =
VERSION.startsWith('5.0.0-next.') && Number(VERSION.slice(11)) > 96;

View File

@@ -0,0 +1,212 @@
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 | null>} */
#css = new Map();
/** @type {Map<string, import('../types/compile.d.ts').Code | null>} */
#js = new Map();
/** @type {Map<string, string[]>} */
#dependencies = new Map();
/** @type {Map<string, Set<string>>} */
#dependants = 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 | null}
*/
getCSS(svelteRequest) {
return this.#css.get(svelteRequest.normalizedFilename);
}
/**
* @param {import('../types/id.d.ts').SvelteRequest} svelteRequest
* @returns {import('../types/compile.d.ts').Code | undefined | null}
*/
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} 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,200 @@
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,120 @@
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.js').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 dependency "${filename}" changed`,
undefined,
'hmr'
);
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}`, undefined, 'hmr');
}
}
};
/** @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));
}
}