Skip to content

refactor: use object hook filters #1132

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 5 commits into from
Jun 6, 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
8 changes: 8 additions & 0 deletions .changeset/tangy-cars-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@sveltejs/vite-plugin-svelte': major
---

define filters using object hook syntax and optimize the filter for resolveId

> [!NOTE]
> include logic has changed to files matching `svelteConfig.include` **OR** `svelteConfig.extensions`. Previously only files matching both were loaded and transformed.
53 changes: 53 additions & 0 deletions packages/vite-plugin-svelte/__tests__/id.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, it, expect } from 'vitest';
import { buildIdFilter, buildModuleIdFilter } from '../src/utils/id.js';

function passes(filter, id) {
const included = filter.id.include.some((includeRE) => includeRE.test(id));
return included && !filter.id.exclude.some((excludeRE) => excludeRE.test(id));
}

describe('buildIdFilter', () => {
it('default filter matches .svelte files', () => {
const filter = buildIdFilter({});
expect(passes(filter, '/src/Foo.svelte')).toBe(true);
expect(passes(filter, '/src/Foo.svelte?something')).toBe(true);
});

it('default filter does not match .js files', () => {
const filter = buildIdFilter({});
expect(passes(filter, '/src/foo.js')).toBe(false);
expect(passes(filter, '/src/foo.js?something')).toBe(false);
});

it('custom filter matches .svx files', () => {
const filter = buildIdFilter({ extensions: ['.svelte', '.svx'] });
expect(passes(filter, '/src/Foo.svx')).toBe(true);
expect(passes(filter, '/src/Foo.svx?something')).toBe(true);
});
});

describe('buildModuleIdFilter', () => {
it('default filter matches .svelte.*.js/ts files', () => {
const filter = buildModuleIdFilter({});
expect(passes(filter, '/src/foo.svelte.js')).toBe(true);
expect(passes(filter, '/src/foo.svelte.ts')).toBe(true);
expect(passes(filter, '/src/foo.svelte.test.js')).toBe(true);
expect(passes(filter, '/src/foo.svelte.test.ts')).toBe(true);
});

it('default filter does not match files without .svelte.', () => {
const filter = buildModuleIdFilter({});
expect(passes(filter, '/src/foo.js')).toBe(false);
expect(passes(filter, '/src/foo.ts')).toBe(false);
expect(passes(filter, '/src/foo.test.js')).toBe(false);
expect(passes(filter, '/src/foo.test.ts')).toBe(false);
});

it('custom filter matches .svx. files', () => {
const filter = buildModuleIdFilter({ experimental: { compileModule: { infixes: ['.svx.'] } } });
expect(passes(filter, '/src/foo.svx.js')).toBe(true);
expect(passes(filter, '/src/foo.svx.ts')).toBe(true);
expect(passes(filter, '/src/foo.svx.test.js')).toBe(true);
expect(passes(filter, '/src/foo.svx.test.ts')).toBe(true);
});
});
214 changes: 114 additions & 100 deletions packages/vite-plugin-svelte/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ 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 {
buildIdFilter,
buildIdParser,
buildModuleIdFilter,
buildModuleIdParser
} from './utils/id.js';
import {
buildExtraViteConfig,
validateInlineOptions,
Expand All @@ -20,6 +25,7 @@ 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';
import { SVELTE_VIRTUAL_STYLE_ID_REGEX } from './utils/constants.js';

/**
* @param {Partial<import('./public.d.ts').Options>} [inlineOptions]
Expand All @@ -42,67 +48,74 @@ export function svelte(inlineOptions) {
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');
/** @type {import('vite').Plugin} */
const compilePlugin = {
name: 'vite-plugin-svelte',
// make sure our resolver runs before vite internal resolver to resolve svelte field correctly
enforce: 'pre',
/** @type {import('./types/plugin-api.d.ts').PluginAPI} */
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');
},
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 configResolved(config) {
options = resolveOptions(options, config, cache);
patchResolvedViteConfig(config, options);
const filter = buildIdFilter(options);
//@ts-expect-error transform defined below but filter not in type
compilePlugin.transform.filter = filter;
//@ts-expect-error load defined below but filter not in type
compilePlugin.load.filter = filter;

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;
}
},
requestParser = buildIdParser(options);
compileSvelte = createCompileSvelte();
viteConfig = config;
// TODO deep clone to avoid mutability from outside?
compilePlugin.api.options = options;
log.debug('resolved options', options, 'config');
log.debug('filters', filter, '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);
},
configureServer(server) {
options.server = server;
setupWatchers(options, cache, requestParser);
},

async load(id, opts) {
load: {
async handler(id, opts) {
const ssr = !!opts?.ssr;
const svelteRequest = requestParser(id, !!ssr);
if (svelteRequest) {
Expand Down Expand Up @@ -137,30 +150,23 @@ export function svelte(inlineOptions) {
}
}
}
},
}
},

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;
}
}
},
resolveId: {
// we don't use our generic filter here but a reduced one that only matches our virtual css
filter: { id: SVELTE_VIRTUAL_STYLE_ID_REGEX },
handler(id) {
// 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 ${id}`, undefined, 'resolve');
// TODO: do we have to repeat the dance for constructing the virtual id here? our transform added it that way
return id;
}
},

async transform(code, id, opts) {
transform: {
async handler(code, id, opts) {
const ssr = !!opts?.ssr;
const svelteRequest = requestParser(id, ssr);
if (!svelteRequest || svelteRequest.query.type === 'style' || svelteRequest.raw) {
Expand Down Expand Up @@ -194,28 +200,34 @@ export function svelte(inlineOptions) {
}
}
};
},
}
},

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();
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();
}
};

/** @type {import('vite').Plugin} */
const moduleCompilePlugin = {
name: 'vite-plugin-svelte-module',
enforce: 'post',
async configResolved() {
//@ts-expect-error transform defined below but filter not in type
moduleCompilePlugin.transform.filter = buildModuleIdFilter(options);
moduleRequestParser = buildModuleIdParser(options);
},
{
name: 'vite-plugin-svelte-module',
enforce: 'post',
async configResolved() {
moduleRequestParser = buildModuleIdParser(options);
},
async transform(code, id, opts) {
transform: {
async handler(code, id, opts) {
const ssr = !!opts?.ssr;
const moduleRequest = moduleRequestParser(id, ssr);
if (!moduleRequest) {
Expand All @@ -233,9 +245,11 @@ export function svelte(inlineOptions) {
throw toRollupError(e, options);
}
}
},
svelteInspector()
];
}
};

/** @type {import('vite').Plugin[]} */
const plugins = [compilePlugin, moduleCompilePlugin, svelteInspector()];
return plugins;
}

Expand Down
8 changes: 4 additions & 4 deletions packages/vite-plugin-svelte/src/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ export interface PluginOptions {
*
* @see https://github.com/micromatch/picomatch
*/
include?: Arrayable<string>;
include?: Arrayable<string | RegExp>;
/**
* 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>;
exclude?: Arrayable<string | RegExp>;
/**
* Emit Svelte styles as virtual CSS files for Vite and other plugins to process
*
Expand Down Expand Up @@ -187,8 +187,8 @@ interface CompileModuleOptions {
* @default ['.ts','.js']
*/
extensions?: string[];
include?: Arrayable<string>;
exclude?: Arrayable<string>;
include?: Arrayable<string | RegExp>;
exclude?: Arrayable<string | RegExp>;
}

type Arrayable<T> = T | T[];
Expand Down
7 changes: 7 additions & 0 deletions packages/vite-plugin-svelte/src/types/id.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ export interface SvelteModuleRequest {
}

export type IdParser = (id: string, ssr: boolean, timestamp?: number) => SvelteRequest | undefined;

export type IdFilter = {
id: {
include: Array<string | RegExp>;
exclude: Array<string | RegExp>;
};
};
export type ModuleIdParser = (
id: string,
ssr: boolean,
Expand Down
5 changes: 5 additions & 0 deletions packages/vite-plugin-svelte/src/utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,8 @@ export const FAQ_LINK_MISSING_EXPORTS_CONDITION =
export const DEFAULT_SVELTE_EXT = ['.svelte'];
export const DEFAULT_SVELTE_MODULE_INFIX = ['.svelte.'];
export const DEFAULT_SVELTE_MODULE_EXT = ['.js', '.ts'];

export const SVELTE_VIRTUAL_STYLE_SUFFIX = '?svelte&type=style&lang.css';
export const SVELTE_VIRTUAL_STYLE_ID_REGEX = new RegExp(
`${SVELTE_VIRTUAL_STYLE_SUFFIX.replace(/[?.]/g, '\\$&')}$`
);
Loading