Skip to content

Commit a494b03

Browse files
authored
refactor: caching (#1154)
1 parent 37019aa commit a494b03

File tree

12 files changed

+149
-338
lines changed

12 files changed

+149
-338
lines changed

.changeset/easy-dancers-smile.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/vite-plugin-svelte': patch
3+
---
4+
5+
refactor internal caching to reduce code, memory use and avoid perEnvironmentCache

packages/vite-plugin-svelte/src/plugins/compile.js

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { toRollupError } from '../utils/error.js';
22
import { logCompilerWarnings } from '../utils/log.js';
3-
import { ensureWatchedFile } from '../utils/watch.js';
43

54
/**
65
* @param {import('../types/plugin-api.d.ts').PluginAPI} api
@@ -32,37 +31,30 @@ export function compile(api) {
3231
if (!svelteRequest || svelteRequest.raw) {
3332
return;
3433
}
35-
const cache = api.getEnvironmentCache(this);
3634
let compileData;
3735
try {
38-
const svelteMeta = this.getModuleInfo(id)?.meta?.svelte;
39-
compileData = await compileSvelte(svelteRequest, code, options, svelteMeta?.preprocessed);
36+
compileData = await compileSvelte(
37+
svelteRequest,
38+
code,
39+
options,
40+
this.getCombinedSourcemap()
41+
);
4042
} catch (e) {
41-
cache.setError(svelteRequest, e);
4243
throw toRollupError(e, options);
4344
}
4445
if (compileData.compiled?.warnings) {
4546
logCompilerWarnings(svelteRequest, compileData.compiled.warnings, options);
4647
}
4748

48-
cache.update(compileData);
49-
if (compileData.dependencies?.length) {
50-
if (options.server) {
51-
for (const dep of compileData.dependencies) {
52-
ensureWatchedFile(options.server.watcher, dep, options.root);
53-
}
54-
} else if (options.isBuild && this.environment.config.build.watch) {
55-
for (const dep of compileData.dependencies) {
56-
this.addWatchFile(dep);
57-
}
58-
}
59-
}
6049
return {
6150
...compileData.compiled.js,
6251
moduleType: 'js',
6352
meta: {
6453
vite: {
6554
lang: compileData.lang
55+
},
56+
svelte: {
57+
css: compileData.compiled.css
6658
}
6759
}
6860
};

packages/vite-plugin-svelte/src/plugins/configure.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import process from 'node:process';
22
import { isDebugNamespaceEnabled, log } from '../utils/log.js';
33
import * as vite from 'vite';
4-
import { VitePluginSvelteCache } from '../utils/vite-plugin-svelte-cache.js';
54
import { VitePluginSvelteStats } from '../utils/vite-plugin-svelte-stats.js';
65
import {
76
buildExtraViteConfig,
@@ -15,7 +14,7 @@ import { buildIdFilter, buildIdParser } from '../utils/id.js';
1514
import { createCompileSvelte } from '../utils/compile.js';
1615

1716
// @ts-expect-error rolldownVersion
18-
const { version: viteVersion, rolldownVersion, perEnvironmentState } = vite;
17+
const { version: viteVersion, rolldownVersion } = vite;
1918

2019
/**
2120
* @param {Partial<import('../public.d.ts').Options>} [inlineOptions]
@@ -71,8 +70,7 @@ export function configure(api, inlineOptions) {
7170
if (isDebugNamespaceEnabled('stats')) {
7271
api.options.stats = new VitePluginSvelteStats();
7372
}
74-
//@ts-expect-error perEnvironmentState uses a wider type for PluginContext
75-
api.getEnvironmentCache = perEnvironmentState((_env) => new VitePluginSvelteCache());
73+
7674
api.idFilter = buildIdFilter(options);
7775

7876
api.idParser = buildIdParser(options);

packages/vite-plugin-svelte/src/plugins/hot-update.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export function hotUpdate(api) {
5757
(e) => e.config.consumer === 'client'
5858
);
5959
if (clientEnvironment) {
60-
setupWatchers(options, api.getEnvironmentCache({ environment: clientEnvironment }));
60+
setupWatchers(options);
6161
} else {
6262
log.warn(
6363
'No client environment found, not adding watchers for svelte config and preprocessor dependencies'

packages/vite-plugin-svelte/src/plugins/load-compiled-css.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,14 @@ export function loadCompiledCss(api) {
2626
if (!svelteRequest) {
2727
return;
2828
}
29-
const cache = api.getEnvironmentCache(this);
30-
const cachedCss = cache.getCSS(svelteRequest);
29+
const cachedCss = this.getModuleInfo(svelteRequest.filename)?.meta.svelte?.css;
3130
if (cachedCss) {
3231
const { hasGlobal, ...css } = cachedCss;
3332
if (hasGlobal === false) {
3433
// hasGlobal was added in svelte 5.26.0, so make sure it is boolean false
3534
css.meta ??= {};
3635
css.meta.vite ??= {};
37-
// TODO is that slice the best way to get the filename without parsing the id?
38-
css.meta.vite.cssScopeTo = [id.slice(0, id.lastIndexOf('?')), 'default'];
36+
css.meta.vite.cssScopeTo = [svelteRequest.filename, 'default'];
3937
}
4038
css.moduleType = 'css';
4139
return css;

packages/vite-plugin-svelte/src/plugins/preprocess.js

Lines changed: 120 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { mapToRelative } from '../utils/sourcemaps.js';
33
import * as svelte from 'svelte/compiler';
44
import { log } from '../utils/log.js';
55
import { arraify } from '../utils/options.js';
6+
import fs from 'node:fs';
7+
import path from 'node:path';
68

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

19+
/**
20+
* @type {DependenciesCache}
21+
*/
22+
let dependenciesCache;
23+
1724
/**
1825
* @type {import("../types/compile.d.ts").PreprocessSvelte}
1926
*/
2027
let preprocessSvelte;
28+
2129
/** @type {import('vite').Plugin} */
2230
const plugin = {
2331
name: 'vite-plugin-svelte:preprocess',
@@ -37,19 +45,39 @@ export function preprocess(api) {
3745
delete plugin.transform;
3846
}
3947
},
40-
48+
configureServer(server) {
49+
dependenciesCache = new DependenciesCache(server);
50+
},
51+
buildStart() {
52+
dependenciesCache?.clear();
53+
},
4154
transform: {
4255
async handler(code, id) {
43-
const cache = api.getEnvironmentCache(this);
4456
const ssr = this.environment.config.consumer === 'server';
4557
const svelteRequest = api.idParser(id, ssr);
4658
if (!svelteRequest) {
4759
return;
4860
}
4961
try {
50-
return await preprocessSvelte(svelteRequest, code, options);
62+
const preprocessed = await preprocessSvelte(svelteRequest, code, options);
63+
dependenciesCache?.update(svelteRequest, preprocessed?.dependencies ?? []);
64+
if (!preprocessed) {
65+
return;
66+
}
67+
if (options.isBuild && this.environment.config.build.watch && preprocessed.dependencies) {
68+
for (const dep of preprocessed.dependencies) {
69+
this.addWatchFile(dep);
70+
}
71+
}
72+
73+
/** @type {import('vite').Rollup.SourceDescription}*/
74+
const result = { code: preprocessed.code };
75+
if (preprocessed.map) {
76+
// @ts-expect-error type differs but should work
77+
result.map = preprocessed.map;
78+
}
79+
return result;
5180
} catch (e) {
52-
cache.setError(svelteRequest, e);
5381
throw toRollupError(e, options);
5482
}
5583
}
@@ -63,8 +91,6 @@ export function preprocess(api) {
6391
* @returns {import('../types/compile.d.ts').PreprocessSvelte}
6492
*/
6593
function createPreprocessSvelte(options, resolvedConfig) {
66-
/** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */
67-
let stats;
6894
/** @type {Array<import('svelte/compiler').PreprocessorGroup>} */
6995
const preprocessors = arraify(options.preprocess);
7096

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

77103
/** @type {import('../types/compile.d.ts').PreprocessSvelte} */
78-
return async function preprocessSvelte(svelteRequest, code, options) {
79-
const { filename, ssr } = svelteRequest;
80-
81-
if (options.stats) {
82-
if (options.isBuild) {
83-
if (!stats) {
84-
// build is either completely ssr or csr, create stats collector on first compile
85-
// it is then finished in the buildEnd hook.
86-
stats = options.stats.startCollection(`${ssr ? 'ssr' : 'dom'} preprocess`, {
87-
logInProgress: () => false
88-
});
89-
}
90-
} else {
91-
// dev time ssr, it's a ssr request and there are no stats, assume new page load and start collecting
92-
if (ssr && !stats) {
93-
stats = options.stats.startCollection('ssr preprocess');
94-
}
95-
// stats are being collected but this isn't an ssr request, assume page loaded and stop collecting
96-
if (!ssr && stats) {
97-
stats.finish();
98-
stats = undefined;
99-
}
100-
// TODO find a way to trace dom compile during dev
101-
// problem: we need to call finish at some point but have no way to tell if page load finished
102-
// also they for hmr updates too
103-
}
104-
}
105-
104+
return async function preprocessSvelte(svelteRequest, code) {
105+
const { filename } = svelteRequest;
106106
let preprocessed;
107-
108107
if (preprocessors && preprocessors.length > 0) {
109108
try {
110-
const endStat = stats?.start(filename);
111109
preprocessed = await svelte.preprocess(code, preprocessors, { filename }); // full filename here so postcss works
112-
endStat?.();
113110
} catch (e) {
114111
e.message = `Error while preprocessing ${filename}${e.message ? ` - ${e.message}` : ''}`;
115112
throw e;
116113
}
117-
118114
if (typeof preprocessed?.map === 'object') {
119115
mapToRelative(preprocessed?.map, filename);
120116
}
121-
return /** @type {import('../types/compile.d.ts').PreprocessTransformOutput} */ {
122-
code: preprocessed.code,
123-
// @ts-expect-error
124-
map: preprocessed.map,
125-
meta: {
126-
svelte: {
127-
preprocessed
128-
}
129-
}
130-
};
117+
return preprocessed;
131118
}
132119
};
133120
}
121+
122+
/**
123+
* @class
124+
*
125+
* caches dependencies of preprocessed files and emit change events on dependants
126+
*/
127+
class DependenciesCache {
128+
/** @type {Map<string, string[]>} */
129+
#dependencies = new Map();
130+
/** @type {Map<string, Set<string>>} */
131+
#dependants = new Map();
132+
133+
/** @type {import('vite').ViteDevServer} */
134+
#server;
135+
/**
136+
*
137+
* @param {import('vite').ViteDevServer} server
138+
*/
139+
constructor(server) {
140+
this.#server = server;
141+
/** @type {(filename: string) => void} */
142+
const emitChangeEventOnDependants = (filename) => {
143+
const dependants = this.#dependants.get(filename);
144+
dependants?.forEach((dependant) => {
145+
if (fs.existsSync(dependant)) {
146+
log.debug(
147+
`emitting virtual change event for "${dependant}" because dependency "${filename}" changed`,
148+
undefined,
149+
'hmr'
150+
);
151+
server.watcher.emit('change', dependant);
152+
}
153+
});
154+
};
155+
server.watcher.on('change', emitChangeEventOnDependants);
156+
server.watcher.on('unlink', emitChangeEventOnDependants);
157+
}
158+
159+
/**
160+
* @param {string} file
161+
*/
162+
#ensureWatchedFile(file) {
163+
const root = this.#server.config.root;
164+
if (
165+
file &&
166+
// only need to watch if out of root
167+
!file.startsWith(root + '/') &&
168+
// some rollup plugins use null bytes for private resolved Ids
169+
!file.includes('\0') &&
170+
fs.existsSync(file)
171+
) {
172+
// resolve file to normalized system path
173+
this.#server.watcher.add(path.resolve(file));
174+
}
175+
}
176+
177+
clear() {
178+
this.#dependencies.clear();
179+
this.#dependants.clear();
180+
}
181+
182+
/**
183+
*
184+
* @param {import('../types/id.d.ts').SvelteRequest} svelteRequest
185+
* @param {string[]} dependencies
186+
*/
187+
update(svelteRequest, dependencies) {
188+
const id = svelteRequest.normalizedFilename;
189+
const prevDependencies = this.#dependencies.get(id) || [];
190+
191+
this.#dependencies.set(id, dependencies);
192+
const removed = prevDependencies.filter((d) => !dependencies.includes(d));
193+
const added = dependencies.filter((d) => !prevDependencies.includes(d));
194+
added.forEach((d) => {
195+
this.#ensureWatchedFile(d);
196+
if (!this.#dependants.has(d)) {
197+
this.#dependants.set(d, new Set());
198+
}
199+
/** @type {Set<string>} */ (this.#dependants.get(d)).add(svelteRequest.filename);
200+
});
201+
removed.forEach((d) => {
202+
/** @type {Set<string>} */ (this.#dependants.get(d)).delete(svelteRequest.filename);
203+
});
204+
}
205+
}

packages/vite-plugin-svelte/src/types/compile.d.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,14 @@ export type CompileSvelte = (
77
svelteRequest: SvelteRequest,
88
code: string,
99
options: Partial<ResolvedOptions>,
10-
preprocessed?: Processed
10+
sourcemap?: Rollup.SourceMap
1111
) => Promise<CompileData>;
1212

1313
export type PreprocessSvelte = (
1414
svelteRequest: SvelteRequest,
1515
code: string,
1616
options: Partial<ResolvedOptions>
17-
) => Promise<PreprocessTransformOutput | undefined>;
18-
19-
export interface PreprocessTransformOutput {
20-
code: string;
21-
map: Rollup.SourceMapInput;
22-
meta: {
23-
svelte: {
24-
preprocessed: Processed;
25-
};
26-
};
27-
}
17+
) => Promise<Processed | undefined>;
2818

2919
export interface Code {
3020
code: string;
@@ -44,6 +34,4 @@ export interface CompileData {
4434
lang: string;
4535
compiled: CompileResult;
4636
ssr: boolean | undefined;
47-
dependencies: string[];
48-
preprocessed: Processed;
4937
}

packages/vite-plugin-svelte/src/types/plugin-api.d.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
11
import type { ResolvedOptions } from './options.d.ts';
22
import type { IdFilter, IdParser } from './id.d.ts';
33
import type { CompileSvelte } from './compile.d.ts';
4-
import type { Environment } from 'vite';
5-
// eslint-disable-next-line n/no-missing-import
6-
import { VitePluginSvelteCache } from '../utils/vite-plugin-svelte-cache.js';
7-
8-
interface EnvContext {
9-
environment: Environment;
10-
}
114

125
export interface PluginAPI {
136
options: ResolvedOptions;
14-
getEnvironmentCache: (arg: EnvContext) => VitePluginSvelteCache;
157
idFilter: IdFilter;
168
idParser: IdParser;
179
compileSvelte: CompileSvelte;

0 commit comments

Comments
 (0)