Skip to content

Commit 7af3cd2

Browse files
authored
refactor: use object hook filters (#1132)
* refactor: add object hook filters to main vite plugin * refactor: add object hook filter to compile module plugin * fix filters and add unit test * chore: cleanup * negative tests
1 parent 59e082e commit 7af3cd2

File tree

10 files changed

+257
-152
lines changed

10 files changed

+257
-152
lines changed

.changeset/tangy-cars-dress.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@sveltejs/vite-plugin-svelte': major
3+
---
4+
5+
define filters using object hook syntax and optimize the filter for resolveId
6+
7+
> [!NOTE]
8+
> include logic has changed to files matching `svelteConfig.include` **OR** `svelteConfig.extensions`. Previously only files matching both were loaded and transformed.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { buildIdFilter, buildModuleIdFilter } from '../src/utils/id.js';
3+
4+
function passes(filter, id) {
5+
const included = filter.id.include.some((includeRE) => includeRE.test(id));
6+
return included && !filter.id.exclude.some((excludeRE) => excludeRE.test(id));
7+
}
8+
9+
describe('buildIdFilter', () => {
10+
it('default filter matches .svelte files', () => {
11+
const filter = buildIdFilter({});
12+
expect(passes(filter, '/src/Foo.svelte')).toBe(true);
13+
expect(passes(filter, '/src/Foo.svelte?something')).toBe(true);
14+
});
15+
16+
it('default filter does not match .js files', () => {
17+
const filter = buildIdFilter({});
18+
expect(passes(filter, '/src/foo.js')).toBe(false);
19+
expect(passes(filter, '/src/foo.js?something')).toBe(false);
20+
});
21+
22+
it('custom filter matches .svx files', () => {
23+
const filter = buildIdFilter({ extensions: ['.svelte', '.svx'] });
24+
expect(passes(filter, '/src/Foo.svx')).toBe(true);
25+
expect(passes(filter, '/src/Foo.svx?something')).toBe(true);
26+
});
27+
});
28+
29+
describe('buildModuleIdFilter', () => {
30+
it('default filter matches .svelte.*.js/ts files', () => {
31+
const filter = buildModuleIdFilter({});
32+
expect(passes(filter, '/src/foo.svelte.js')).toBe(true);
33+
expect(passes(filter, '/src/foo.svelte.ts')).toBe(true);
34+
expect(passes(filter, '/src/foo.svelte.test.js')).toBe(true);
35+
expect(passes(filter, '/src/foo.svelte.test.ts')).toBe(true);
36+
});
37+
38+
it('default filter does not match files without .svelte.', () => {
39+
const filter = buildModuleIdFilter({});
40+
expect(passes(filter, '/src/foo.js')).toBe(false);
41+
expect(passes(filter, '/src/foo.ts')).toBe(false);
42+
expect(passes(filter, '/src/foo.test.js')).toBe(false);
43+
expect(passes(filter, '/src/foo.test.ts')).toBe(false);
44+
});
45+
46+
it('custom filter matches .svx. files', () => {
47+
const filter = buildModuleIdFilter({ experimental: { compileModule: { infixes: ['.svx.'] } } });
48+
expect(passes(filter, '/src/foo.svx.js')).toBe(true);
49+
expect(passes(filter, '/src/foo.svx.ts')).toBe(true);
50+
expect(passes(filter, '/src/foo.svx.test.js')).toBe(true);
51+
expect(passes(filter, '/src/foo.svx.test.ts')).toBe(true);
52+
});
53+
});

packages/vite-plugin-svelte/src/index.js

Lines changed: 114 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import { svelteInspector } from '@sveltejs/vite-plugin-svelte-inspector';
44
import { handleHotUpdate } from './handle-hot-update.js';
55
import { log, logCompilerWarnings } from './utils/log.js';
66
import { createCompileSvelte } from './utils/compile.js';
7-
import { buildIdParser, buildModuleIdParser } from './utils/id.js';
7+
import {
8+
buildIdFilter,
9+
buildIdParser,
10+
buildModuleIdFilter,
11+
buildModuleIdParser
12+
} from './utils/id.js';
813
import {
914
buildExtraViteConfig,
1015
validateInlineOptions,
@@ -20,6 +25,7 @@ import { saveSvelteMetadata } from './utils/optimizer.js';
2025
import { VitePluginSvelteCache } from './utils/vite-plugin-svelte-cache.js';
2126
import { loadRaw } from './utils/load-raw.js';
2227
import * as svelteCompiler from 'svelte/compiler';
28+
import { SVELTE_VIRTUAL_STYLE_ID_REGEX } from './utils/constants.js';
2329

2430
/**
2531
* @param {Partial<import('./public.d.ts').Options>} [inlineOptions]
@@ -42,67 +48,74 @@ export function svelte(inlineOptions) {
4248
let viteConfig;
4349
/** @type {import('./types/compile.d.ts').CompileSvelte} */
4450
let compileSvelte;
45-
/** @type {import('./types/plugin-api.d.ts').PluginAPI} */
46-
const api = {};
47-
/** @type {import('vite').Plugin[]} */
48-
const plugins = [
49-
{
50-
name: 'vite-plugin-svelte',
51-
// make sure our resolver runs before vite internal resolver to resolve svelte field correctly
52-
enforce: 'pre',
53-
api,
54-
async config(config, configEnv) {
55-
// setup logger
56-
if (process.env.DEBUG) {
57-
log.setLevel('debug');
58-
} else if (config.logLevel) {
59-
log.setLevel(config.logLevel);
60-
}
61-
// @ts-expect-error temporarily lend the options variable until fixed in configResolved
62-
options = await preResolveOptions(inlineOptions, config, configEnv);
63-
// extra vite config
64-
const extraViteConfig = await buildExtraViteConfig(options, config);
65-
log.debug('additional vite config', extraViteConfig, 'config');
66-
return extraViteConfig;
67-
},
6851

69-
configEnvironment(name, config, opts) {
70-
ensureConfigEnvironmentMainFields(name, config, opts);
71-
// @ts-expect-error the function above should make `resolve.mainFields` non-nullable
72-
config.resolve.mainFields.unshift('svelte');
52+
/** @type {import('vite').Plugin} */
53+
const compilePlugin = {
54+
name: 'vite-plugin-svelte',
55+
// make sure our resolver runs before vite internal resolver to resolve svelte field correctly
56+
enforce: 'pre',
57+
/** @type {import('./types/plugin-api.d.ts').PluginAPI} */
58+
api: {},
59+
async config(config, configEnv) {
60+
// setup logger
61+
if (process.env.DEBUG) {
62+
log.setLevel('debug');
63+
} else if (config.logLevel) {
64+
log.setLevel(config.logLevel);
65+
}
66+
// @ts-expect-error temporarily lend the options variable until fixed in configResolved
67+
options = await preResolveOptions(inlineOptions, config, configEnv);
68+
// extra vite config
69+
const extraViteConfig = await buildExtraViteConfig(options, config);
70+
log.debug('additional vite config', extraViteConfig, 'config');
71+
return extraViteConfig;
72+
},
73+
74+
configEnvironment(name, config, opts) {
75+
ensureConfigEnvironmentMainFields(name, config, opts);
76+
// @ts-expect-error the function above should make `resolve.mainFields` non-nullable
77+
config.resolve.mainFields.unshift('svelte');
7378

74-
ensureConfigEnvironmentConditions(name, config, opts);
75-
// @ts-expect-error the function above should make `resolve.conditions` non-nullable
76-
config.resolve.conditions.push('svelte');
77-
},
79+
ensureConfigEnvironmentConditions(name, config, opts);
80+
// @ts-expect-error the function above should make `resolve.conditions` non-nullable
81+
config.resolve.conditions.push('svelte');
82+
},
7883

79-
async configResolved(config) {
80-
options = resolveOptions(options, config, cache);
81-
patchResolvedViteConfig(config, options);
82-
requestParser = buildIdParser(options);
83-
compileSvelte = createCompileSvelte();
84-
viteConfig = config;
85-
// TODO deep clone to avoid mutability from outside?
86-
api.options = options;
87-
log.debug('resolved options', options, 'config');
88-
},
84+
async configResolved(config) {
85+
options = resolveOptions(options, config, cache);
86+
patchResolvedViteConfig(config, options);
87+
const filter = buildIdFilter(options);
88+
//@ts-expect-error transform defined below but filter not in type
89+
compilePlugin.transform.filter = filter;
90+
//@ts-expect-error load defined below but filter not in type
91+
compilePlugin.load.filter = filter;
8992

90-
async buildStart() {
91-
if (!options.prebundleSvelteLibraries) return;
92-
const isSvelteMetadataChanged = await saveSvelteMetadata(viteConfig.cacheDir, options);
93-
if (isSvelteMetadataChanged) {
94-
// Force Vite to optimize again. Although we mutate the config here, it works because
95-
// Vite's optimizer runs after `buildStart()`.
96-
viteConfig.optimizeDeps.force = true;
97-
}
98-
},
93+
requestParser = buildIdParser(options);
94+
compileSvelte = createCompileSvelte();
95+
viteConfig = config;
96+
// TODO deep clone to avoid mutability from outside?
97+
compilePlugin.api.options = options;
98+
log.debug('resolved options', options, 'config');
99+
log.debug('filters', filter, 'config');
100+
},
101+
102+
async buildStart() {
103+
if (!options.prebundleSvelteLibraries) return;
104+
const isSvelteMetadataChanged = await saveSvelteMetadata(viteConfig.cacheDir, options);
105+
if (isSvelteMetadataChanged) {
106+
// Force Vite to optimize again. Although we mutate the config here, it works because
107+
// Vite's optimizer runs after `buildStart()`.
108+
viteConfig.optimizeDeps.force = true;
109+
}
110+
},
99111

100-
configureServer(server) {
101-
options.server = server;
102-
setupWatchers(options, cache, requestParser);
103-
},
112+
configureServer(server) {
113+
options.server = server;
114+
setupWatchers(options, cache, requestParser);
115+
},
104116

105-
async load(id, opts) {
117+
load: {
118+
async handler(id, opts) {
106119
const ssr = !!opts?.ssr;
107120
const svelteRequest = requestParser(id, !!ssr);
108121
if (svelteRequest) {
@@ -137,30 +150,23 @@ export function svelte(inlineOptions) {
137150
}
138151
}
139152
}
140-
},
153+
}
154+
},
141155

142-
async resolveId(importee, importer, opts) {
143-
const ssr = !!opts?.ssr;
144-
const svelteRequest = requestParser(importee, ssr);
145-
if (svelteRequest?.query.svelte) {
146-
if (
147-
svelteRequest.query.type === 'style' &&
148-
!svelteRequest.raw &&
149-
!svelteRequest.query.inline
150-
) {
151-
// return cssId with root prefix so postcss pipeline of vite finds the directory correctly
152-
// see https://github.com/sveltejs/vite-plugin-svelte/issues/14
153-
log.debug(
154-
`resolveId resolved virtual css module ${svelteRequest.cssId}`,
155-
undefined,
156-
'resolve'
157-
);
158-
return svelteRequest.cssId;
159-
}
160-
}
161-
},
156+
resolveId: {
157+
// we don't use our generic filter here but a reduced one that only matches our virtual css
158+
filter: { id: SVELTE_VIRTUAL_STYLE_ID_REGEX },
159+
handler(id) {
160+
// return cssId with root prefix so postcss pipeline of vite finds the directory correctly
161+
// see https://github.com/sveltejs/vite-plugin-svelte/issues/14
162+
log.debug(`resolveId resolved virtual css module ${id}`, undefined, 'resolve');
163+
// TODO: do we have to repeat the dance for constructing the virtual id here? our transform added it that way
164+
return id;
165+
}
166+
},
162167

163-
async transform(code, id, opts) {
168+
transform: {
169+
async handler(code, id, opts) {
164170
const ssr = !!opts?.ssr;
165171
const svelteRequest = requestParser(id, ssr);
166172
if (!svelteRequest || svelteRequest.query.type === 'style' || svelteRequest.raw) {
@@ -194,28 +200,34 @@ export function svelte(inlineOptions) {
194200
}
195201
}
196202
};
197-
},
203+
}
204+
},
198205

199-
handleHotUpdate(ctx) {
200-
if (!options.compilerOptions.hmr || !options.emitCss) {
201-
return;
202-
}
203-
const svelteRequest = requestParser(ctx.file, false, ctx.timestamp);
204-
if (svelteRequest) {
205-
return handleHotUpdate(compileSvelte, ctx, svelteRequest, cache, options);
206-
}
207-
},
208-
async buildEnd() {
209-
await options.stats?.finishAll();
206+
handleHotUpdate(ctx) {
207+
if (!options.compilerOptions.hmr || !options.emitCss) {
208+
return;
210209
}
210+
const svelteRequest = requestParser(ctx.file, false, ctx.timestamp);
211+
if (svelteRequest) {
212+
return handleHotUpdate(compileSvelte, ctx, svelteRequest, cache, options);
213+
}
214+
},
215+
async buildEnd() {
216+
await options.stats?.finishAll();
217+
}
218+
};
219+
220+
/** @type {import('vite').Plugin} */
221+
const moduleCompilePlugin = {
222+
name: 'vite-plugin-svelte-module',
223+
enforce: 'post',
224+
async configResolved() {
225+
//@ts-expect-error transform defined below but filter not in type
226+
moduleCompilePlugin.transform.filter = buildModuleIdFilter(options);
227+
moduleRequestParser = buildModuleIdParser(options);
211228
},
212-
{
213-
name: 'vite-plugin-svelte-module',
214-
enforce: 'post',
215-
async configResolved() {
216-
moduleRequestParser = buildModuleIdParser(options);
217-
},
218-
async transform(code, id, opts) {
229+
transform: {
230+
async handler(code, id, opts) {
219231
const ssr = !!opts?.ssr;
220232
const moduleRequest = moduleRequestParser(id, ssr);
221233
if (!moduleRequest) {
@@ -233,9 +245,11 @@ export function svelte(inlineOptions) {
233245
throw toRollupError(e, options);
234246
}
235247
}
236-
},
237-
svelteInspector()
238-
];
248+
}
249+
};
250+
251+
/** @type {import('vite').Plugin[]} */
252+
const plugins = [compilePlugin, moduleCompilePlugin, svelteInspector()];
239253
return plugins;
240254
}
241255

packages/vite-plugin-svelte/src/public.d.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ export interface PluginOptions {
2222
*
2323
* @see https://github.com/micromatch/picomatch
2424
*/
25-
include?: Arrayable<string>;
25+
include?: Arrayable<string | RegExp>;
2626
/**
2727
* A `picomatch` pattern, or array of patterns, which specifies the files to be ignored by the
2828
* plugin. By default, no files are ignored.
2929
*
3030
* @see https://github.com/micromatch/picomatch
3131
*/
32-
exclude?: Arrayable<string>;
32+
exclude?: Arrayable<string | RegExp>;
3333
/**
3434
* Emit Svelte styles as virtual CSS files for Vite and other plugins to process
3535
*
@@ -187,8 +187,8 @@ interface CompileModuleOptions {
187187
* @default ['.ts','.js']
188188
*/
189189
extensions?: string[];
190-
include?: Arrayable<string>;
191-
exclude?: Arrayable<string>;
190+
include?: Arrayable<string | RegExp>;
191+
exclude?: Arrayable<string | RegExp>;
192192
}
193193

194194
type Arrayable<T> = T | T[];

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ export interface SvelteModuleRequest {
3939
}
4040

4141
export type IdParser = (id: string, ssr: boolean, timestamp?: number) => SvelteRequest | undefined;
42+
43+
export type IdFilter = {
44+
id: {
45+
include: Array<string | RegExp>;
46+
exclude: Array<string | RegExp>;
47+
};
48+
};
4249
export type ModuleIdParser = (
4350
id: string,
4451
ssr: boolean,

packages/vite-plugin-svelte/src/utils/constants.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,8 @@ export const FAQ_LINK_MISSING_EXPORTS_CONDITION =
2929
export const DEFAULT_SVELTE_EXT = ['.svelte'];
3030
export const DEFAULT_SVELTE_MODULE_INFIX = ['.svelte.'];
3131
export const DEFAULT_SVELTE_MODULE_EXT = ['.js', '.ts'];
32+
33+
export const SVELTE_VIRTUAL_STYLE_SUFFIX = '?svelte&type=style&lang.css';
34+
export const SVELTE_VIRTUAL_STYLE_ID_REGEX = new RegExp(
35+
`${SVELTE_VIRTUAL_STYLE_SUFFIX.replace(/[?.]/g, '\\$&')}$`
36+
);

0 commit comments

Comments
 (0)