Skip to content

refactor: caching #1154

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/easy-dancers-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/vite-plugin-svelte': patch
---

refactor internal caching to reduce code, memory use and avoid perEnvironmentCache
26 changes: 9 additions & 17 deletions packages/vite-plugin-svelte/src/plugins/compile.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { toRollupError } from '../utils/error.js';
import { logCompilerWarnings } from '../utils/log.js';
import { ensureWatchedFile } from '../utils/watch.js';

/**
* @param {import('../types/plugin-api.d.ts').PluginAPI} api
Expand Down Expand Up @@ -32,37 +31,30 @@ export function compile(api) {
if (!svelteRequest || svelteRequest.raw) {
return;
}
const cache = api.getEnvironmentCache(this);
let compileData;
try {
const svelteMeta = this.getModuleInfo(id)?.meta?.svelte;
compileData = await compileSvelte(svelteRequest, code, options, svelteMeta?.preprocessed);
compileData = await compileSvelte(
svelteRequest,
code,
options,
this.getCombinedSourcemap()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of passing in sourcemap from preprocessed that could be outdated because another transform changed the code in between the 2 plugins, use getCombinedSourceMap() this has the added benefit that we don't have to pass preprocessed around in meta

);
} catch (e) {
cache.setError(svelteRequest, e);
throw toRollupError(e, options);
}
if (compileData.compiled?.warnings) {
logCompilerWarnings(svelteRequest, compileData.compiled.warnings, options);
}

cache.update(compileData);
if (compileData.dependencies?.length) {
if (options.server) {
for (const dep of compileData.dependencies) {
ensureWatchedFile(options.server.watcher, dep, options.root);
}
} else if (options.isBuild && this.environment.config.build.watch) {
for (const dep of compileData.dependencies) {
this.addWatchFile(dep);
}
}
}
return {
...compileData.compiled.js,
moduleType: 'js',
meta: {
vite: {
lang: compileData.lang
},
svelte: {
css: compileData.compiled.css
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of caching the css in a separate cache structure, attach it to the meta of the js module

}
}
};
Expand Down
6 changes: 2 additions & 4 deletions packages/vite-plugin-svelte/src/plugins/configure.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import process from 'node:process';
import { isDebugNamespaceEnabled, log } from '../utils/log.js';
import * as vite from 'vite';
import { VitePluginSvelteCache } from '../utils/vite-plugin-svelte-cache.js';
import { VitePluginSvelteStats } from '../utils/vite-plugin-svelte-stats.js';
import {
buildExtraViteConfig,
Expand All @@ -15,7 +14,7 @@ import { buildIdFilter, buildIdParser } from '../utils/id.js';
import { createCompileSvelte } from '../utils/compile.js';

// @ts-expect-error rolldownVersion
const { version: viteVersion, rolldownVersion, perEnvironmentState } = vite;
const { version: viteVersion, rolldownVersion } = vite;

/**
* @param {Partial<import('../public.d.ts').Options>} [inlineOptions]
Expand Down Expand Up @@ -71,8 +70,7 @@ export function configure(api, inlineOptions) {
if (isDebugNamespaceEnabled('stats')) {
api.options.stats = new VitePluginSvelteStats();
}
//@ts-expect-error perEnvironmentState uses a wider type for PluginContext
api.getEnvironmentCache = perEnvironmentState((_env) => new VitePluginSvelteCache());

api.idFilter = buildIdFilter(options);

api.idParser = buildIdParser(options);
Expand Down
2 changes: 1 addition & 1 deletion packages/vite-plugin-svelte/src/plugins/hot-update.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function hotUpdate(api) {
(e) => e.config.consumer === 'client'
);
if (clientEnvironment) {
setupWatchers(options, api.getEnvironmentCache({ environment: clientEnvironment }));
setupWatchers(options);
} else {
log.warn(
'No client environment found, not adding watchers for svelte config and preprocessor dependencies'
Expand Down
6 changes: 2 additions & 4 deletions packages/vite-plugin-svelte/src/plugins/load-compiled-css.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,14 @@ export function loadCompiledCss(api) {
if (!svelteRequest) {
return;
}
const cache = api.getEnvironmentCache(this);
const cachedCss = cache.getCSS(svelteRequest);
const cachedCss = this.getModuleInfo(svelteRequest.filename)?.meta.svelte?.css;
if (cachedCss) {
const { hasGlobal, ...css } = cachedCss;
if (hasGlobal === false) {
// hasGlobal was added in svelte 5.26.0, so make sure it is boolean false
css.meta ??= {};
css.meta.vite ??= {};
// TODO is that slice the best way to get the filename without parsing the id?
css.meta.vite.cssScopeTo = [id.slice(0, id.lastIndexOf('?')), 'default'];
css.meta.vite.cssScopeTo = [svelteRequest.filename, 'default'];
}
css.moduleType = 'css';
return css;
Expand Down
168 changes: 120 additions & 48 deletions packages/vite-plugin-svelte/src/plugins/preprocess.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { mapToRelative } from '../utils/sourcemaps.js';
import * as svelte from 'svelte/compiler';
import { log } from '../utils/log.js';
import { arraify } from '../utils/options.js';
import fs from 'node:fs';
import path from 'node:path';

/**
* @param {import('../types/plugin-api.d.ts').PluginAPI} api
Expand All @@ -14,10 +16,16 @@ export function preprocess(api) {
*/
let options;

/**
* @type {DependenciesCache}
*/
let dependenciesCache;

/**
* @type {import("../types/compile.d.ts").PreprocessSvelte}
*/
let preprocessSvelte;

/** @type {import('vite').Plugin} */
const plugin = {
name: 'vite-plugin-svelte:preprocess',
Expand All @@ -37,19 +45,39 @@ export function preprocess(api) {
delete plugin.transform;
}
},

configureServer(server) {
dependenciesCache = new DependenciesCache(server);
},
buildStart() {
dependenciesCache?.clear();
},
transform: {
async handler(code, id) {
const cache = api.getEnvironmentCache(this);
const ssr = this.environment.config.consumer === 'server';
const svelteRequest = api.idParser(id, ssr);
if (!svelteRequest) {
return;
}
try {
return await preprocessSvelte(svelteRequest, code, options);
const preprocessed = await preprocessSvelte(svelteRequest, code, options);
dependenciesCache?.update(svelteRequest, preprocessed?.dependencies ?? []);
if (!preprocessed) {
return;
}
if (options.isBuild && this.environment.config.build.watch && preprocessed.dependencies) {
for (const dep of preprocessed.dependencies) {
this.addWatchFile(dep);
}
}

/** @type {import('vite').Rollup.SourceDescription}*/
const result = { code: preprocessed.code };
if (preprocessed.map) {
// @ts-expect-error type differs but should work
result.map = preprocessed.map;
}
return result;
} catch (e) {
cache.setError(svelteRequest, e);
throw toRollupError(e, options);
}
}
Expand All @@ -63,8 +91,6 @@ export function preprocess(api) {
* @returns {import('../types/compile.d.ts').PreprocessSvelte}
*/
function createPreprocessSvelte(options, resolvedConfig) {
/** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */
let stats;
/** @type {Array<import('svelte/compiler').PreprocessorGroup>} */
const preprocessors = arraify(options.preprocess);

Expand All @@ -75,59 +101,105 @@ function createPreprocessSvelte(options, resolvedConfig) {
}

/** @type {import('../types/compile.d.ts').PreprocessSvelte} */
return async function preprocessSvelte(svelteRequest, code, options) {
const { filename, ssr } = svelteRequest;

if (options.stats) {
if (options.isBuild) {
if (!stats) {
// build is either completely ssr or csr, create stats collector on first compile
// it is then finished in the buildEnd hook.
stats = options.stats.startCollection(`${ssr ? 'ssr' : 'dom'} preprocess`, {
logInProgress: () => false
});
}
} else {
// dev time ssr, it's a ssr request and there are no stats, assume new page load and start collecting
if (ssr && !stats) {
stats = options.stats.startCollection('ssr preprocess');
}
// 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
}
}

return async function preprocessSvelte(svelteRequest, code) {
const { filename } = svelteRequest;
let preprocessed;

if (preprocessors && preprocessors.length > 0) {
try {
const endStat = stats?.start(filename);
preprocessed = await svelte.preprocess(code, preprocessors, { filename }); // full filename here so postcss works
endStat?.();
} catch (e) {
e.message = `Error while preprocessing ${filename}${e.message ? ` - ${e.message}` : ''}`;
throw e;
}

if (typeof preprocessed?.map === 'object') {
mapToRelative(preprocessed?.map, filename);
}
return /** @type {import('../types/compile.d.ts').PreprocessTransformOutput} */ {
code: preprocessed.code,
// @ts-expect-error
map: preprocessed.map,
meta: {
svelte: {
preprocessed
}
}
};
return preprocessed;
}
};
}

/**
* @class
*
* caches dependencies of preprocessed files and emit change events on dependants
*/
class DependenciesCache {
/** @type {Map<string, string[]>} */
#dependencies = new Map();
/** @type {Map<string, Set<string>>} */
#dependants = new Map();

/** @type {import('vite').ViteDevServer} */
#server;
/**
*
* @param {import('vite').ViteDevServer} server
*/
constructor(server) {
this.#server = server;
/** @type {(filename: string) => void} */
const emitChangeEventOnDependants = (filename) => {
const dependants = this.#dependants.get(filename);
dependants?.forEach((dependant) => {
if (fs.existsSync(dependant)) {
log.debug(
`emitting virtual change event for "${dependant}" because dependency "${filename}" changed`,
undefined,
'hmr'
);
server.watcher.emit('change', dependant);
}
});
};
server.watcher.on('change', emitChangeEventOnDependants);
server.watcher.on('unlink', emitChangeEventOnDependants);
}

/**
* @param {string} file
*/
#ensureWatchedFile(file) {
const root = this.#server.config.root;
if (
file &&
// only need to watch if out of root
!file.startsWith(root + '/') &&
// some rollup plugins use null bytes for private resolved Ids
!file.includes('\0') &&
fs.existsSync(file)
) {
// resolve file to normalized system path
this.#server.watcher.add(path.resolve(file));
}
}

clear() {
this.#dependencies.clear();
this.#dependants.clear();
}

/**
*
* @param {import('../types/id.d.ts').SvelteRequest} svelteRequest
* @param {string[]} dependencies
*/
update(svelteRequest, dependencies) {
const id = svelteRequest.normalizedFilename;
const prevDependencies = this.#dependencies.get(id) || [];

this.#dependencies.set(id, dependencies);
const removed = prevDependencies.filter((d) => !dependencies.includes(d));
const added = dependencies.filter((d) => !prevDependencies.includes(d));
added.forEach((d) => {
this.#ensureWatchedFile(d);
if (!this.#dependants.has(d)) {
this.#dependants.set(d, new Set());
}
/** @type {Set<string>} */ (this.#dependants.get(d)).add(svelteRequest.filename);
});
removed.forEach((d) => {
/** @type {Set<string>} */ (this.#dependants.get(d)).delete(svelteRequest.filename);
});
}
}
16 changes: 2 additions & 14 deletions packages/vite-plugin-svelte/src/types/compile.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,14 @@ export type CompileSvelte = (
svelteRequest: SvelteRequest,
code: string,
options: Partial<ResolvedOptions>,
preprocessed?: Processed
sourcemap?: Rollup.SourceMap
) => Promise<CompileData>;

export type PreprocessSvelte = (
svelteRequest: SvelteRequest,
code: string,
options: Partial<ResolvedOptions>
) => Promise<PreprocessTransformOutput | undefined>;

export interface PreprocessTransformOutput {
code: string;
map: Rollup.SourceMapInput;
meta: {
svelte: {
preprocessed: Processed;
};
};
}
) => Promise<Processed | undefined>;

export interface Code {
code: string;
Expand All @@ -44,6 +34,4 @@ export interface CompileData {
lang: string;
compiled: CompileResult;
ssr: boolean | undefined;
dependencies: string[];
preprocessed: Processed;
}
8 changes: 0 additions & 8 deletions packages/vite-plugin-svelte/src/types/plugin-api.d.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import type { ResolvedOptions } from './options.d.ts';
import type { IdFilter, IdParser } from './id.d.ts';
import type { CompileSvelte } from './compile.d.ts';
import type { Environment } from 'vite';
// eslint-disable-next-line n/no-missing-import
import { VitePluginSvelteCache } from '../utils/vite-plugin-svelte-cache.js';

interface EnvContext {
environment: Environment;
}

export interface PluginAPI {
options: ResolvedOptions;
getEnvironmentCache: (arg: EnvContext) => VitePluginSvelteCache;
idFilter: IdFilter;
idParser: IdParser;
compileSvelte: CompileSvelte;
Expand Down
Loading