Skip to content

Commit dfd4cc6

Browse files
authored
refactor: allow infix notation for svelte modules (#901)
* refactor: allow infix notation for svelte modules and prevent custom extension prebundling * docs: regenerate types * refactor: use 2 esbuild plugins to prebundle .svelte and .svelte.js (simplifies regex and separates logic at cost of some code duplication) * fix: regex escape . * fix: prebundle with dev: true by default
1 parent 85acc9f commit dfd4cc6

File tree

15 files changed

+161
-30
lines changed

15 files changed

+161
-30
lines changed

.changeset/angry-pumpkins-deny.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/vite-plugin-svelte': major
3+
---
4+
5+
only prebundle files with default filenames (.svelte for components, .svelte.(js|ts) for modules)

.changeset/friendly-wombats-listen.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/vite-plugin-svelte': patch
3+
---
4+
5+
prebundle with dev: true by default

.changeset/lazy-bats-warn.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@sveltejs/vite-plugin-svelte': minor
3+
---
4+
5+
allow infix notation for svelte modules
6+
7+
Previously, only suffix notation `.svelte.js` was allowed, now you can also use `.svelte.test.js` or `.svelte.stories.js`.
8+
This helps when writing testcases or other auxillary code where you may want to use runes too.

packages/e2e-tests/prebundle-svelte-deps/__tests__/prebundle-svelte-deps.spec.ts

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ async function expectPageToWork() {
1717
expect(await getText('#api-only')).toBe('api loaded: true');
1818
expect(await getText('#simple .label')).toBe('dependency-import');
1919
expect(await getText('#exports-simple .label')).toBe('dependency-import');
20+
expect(await getText('#module button:first-child')).toBe('count is 0');
2021
}
2122

2223
if (!isBuild) {
@@ -31,6 +32,7 @@ if (!isBuild) {
3132
expect(optimizedPaths).toContain('e2e-test-dep-svelte-exports-simple');
3233
expect(optimizedPaths).toContain('e2e-test-dep-svelte-api-only');
3334
expect(optimizedPaths).toContain('e2e-test-dep-svelte-nested');
35+
expect(optimizedPaths).toContain('e2e-test-dep-svelte-module');
3436
});
3537

3638
test('should not optimize excluded svelte dependencies', () => {

packages/e2e-tests/prebundle-svelte-deps/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"e2e-test-dep-svelte-exports-simple": "file:../_test_dependencies/svelte-exports-simple",
1414
"e2e-test-dep-svelte-hybrid": "file:../_test_dependencies/svelte-hybrid",
1515
"e2e-test-dep-svelte-nested": "file:../_test_dependencies/svelte-nested",
16-
"e2e-test-dep-svelte-simple": "file:../_test_dependencies/svelte-simple"
16+
"e2e-test-dep-svelte-simple": "file:../_test_dependencies/svelte-simple",
17+
"e2e-test-dep-svelte-module": "file:../_test_dependencies/svelte-module"
1718
},
1819
"devDependencies": {
1920
"@sveltejs/vite-plugin-svelte": "workspace:^",

packages/e2e-tests/prebundle-svelte-deps/src/App.svelte

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { Message as Nested } from 'e2e-test-dep-svelte-nested';
66
import { setSomeContext } from 'e2e-test-dep-svelte-api-only';
77
import { getContext } from 'svelte';
8+
import { Counter } from 'e2e-test-dep-svelte-module';
89
setSomeContext();
910
const apiOnlyLoaded = !!getContext('svelte-api-only');
1011
</script>
@@ -23,4 +24,7 @@
2324
<div id="exports-simple">
2425
<Dependency />
2526
</div>
27+
<div id="module">
28+
<Counter />
29+
</div>
2630
</main>

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

+9
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,15 @@ interface ExperimentalOptions {
175175
}
176176

177177
interface CompileModuleOptions {
178+
/**
179+
* infix that must be present in filename
180+
* @default ['.svelte.']
181+
*/
182+
infixes?: string[];
183+
/**
184+
* module extensions
185+
* @default ['.ts','.js']
186+
*/
178187
extensions?: string[];
179188
include?: Arrayable<string>;
180189
exclude?: Arrayable<string>;

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,11 @@ export function createCompileSvelte() {
128128
...dynamicCompileOptions
129129
}
130130
: compileOptions;
131-
132131
const endStat = stats?.start(filename);
133132
/** @type {import('svelte/compiler').CompileResult} */
134133
let compiled;
135134
try {
136-
compiled = svelte.compile(finalCode, finalCompileOptions);
135+
compiled = svelte.compile(finalCode, { ...finalCompileOptions, filename: filename });
137136
// patch output with partial accept until svelte does it
138137
// TODO remove later
139138
if (

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

+4
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,7 @@ export const SVELTE_EXPORT_CONDITIONS = ['svelte'];
2020

2121
export const FAQ_LINK_MISSING_EXPORTS_CONDITION =
2222
'https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/faq.md#missing-exports-condition';
23+
24+
export const DEFAULT_SVELTE_EXT = ['.svelte'];
25+
export const DEFAULT_SVELTE_MODULE_INFIX = ['.svelte.'];
26+
export const DEFAULT_SVELTE_MODULE_EXT = ['.js', '.ts'];

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

+62-21
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ import { toESBuildError } from './error.js';
99
*/
1010

1111
export const facadeEsbuildSveltePluginName = 'vite-plugin-svelte:facade';
12-
13-
const svelteModuleExtension = '.svelte.js';
12+
export const facadeEsbuildSvelteModulePluginName = 'vite-plugin-svelte-module:facade';
1413

1514
/**
1615
* @param {import('../types/options.d.ts').ResolvedOptions} options
@@ -24,18 +23,15 @@ export function esbuildSveltePlugin(options) {
2423
// Otherwise this would heavily slow down the scanning phase.
2524
if (build.initialOptions.plugins?.some((v) => v.name === 'vite:dep-scan')) return;
2625

27-
const svelteExtensions = (options.extensions ?? ['.svelte']).map((ext) => ext.slice(1));
28-
svelteExtensions.push(svelteModuleExtension.slice(1));
29-
30-
const svelteFilter = new RegExp('\\.(' + svelteExtensions.join('|') + ')(\\?.*)?$');
26+
const filter = /\.svelte(?:\?.*)?$/;
3127
/** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */
3228
let statsCollection;
3329
build.onStart(() => {
34-
statsCollection = options.stats?.startCollection('prebundle libraries', {
30+
statsCollection = options.stats?.startCollection('prebundle library components', {
3531
logResult: (c) => c.stats.length > 1
3632
});
3733
});
38-
build.onLoad({ filter: svelteFilter }, async ({ path: filename }) => {
34+
build.onLoad({ filter }, async ({ path: filename }) => {
3935
const code = readFileSync(filename, 'utf8');
4036
try {
4137
const contents = await compileSvelte(options, { filename, code }, statsCollection);
@@ -58,26 +54,14 @@ export function esbuildSveltePlugin(options) {
5854
* @returns {Promise<string>}
5955
*/
6056
async function compileSvelte(options, { filename, code }, statsCollection) {
61-
if (filename.endsWith(svelteModuleExtension)) {
62-
const endStat = statsCollection?.start(filename);
63-
const compiled = svelte.compileModule(code, {
64-
filename,
65-
generate: 'client'
66-
});
67-
if (endStat) {
68-
endStat();
69-
}
70-
return compiled.js.map
71-
? compiled.js.code + '//# sourceMappingURL=' + compiled.js.map.toUrl()
72-
: compiled.js.code;
73-
}
7457
let css = options.compilerOptions.css;
7558
if (css !== 'injected') {
7659
// TODO ideally we'd be able to externalize prebundled styles too, but for now always put them in the js
7760
css = 'injected';
7861
}
7962
/** @type {import('svelte/compiler').CompileOptions} */
8063
const compileOptions = {
64+
dev: true, // default to dev: true because prebundling is only used in dev
8165
...options.compilerOptions,
8266
css,
8367
filename,
@@ -127,3 +111,60 @@ async function compileSvelte(options, { filename, code }, statsCollection) {
127111
? compiled.js.code + '//# sourceMappingURL=' + compiled.js.map.toUrl()
128112
: compiled.js.code;
129113
}
114+
115+
/**
116+
* @param {import('../types/options.d.ts').ResolvedOptions} options
117+
* @returns {EsbuildPlugin}
118+
*/
119+
export function esbuildSvelteModulePlugin(options) {
120+
return {
121+
name: 'vite-plugin-svelte-module:optimize-svelte',
122+
setup(build) {
123+
// Skip in scanning phase as Vite already handles scanning Svelte files.
124+
// Otherwise this would heavily slow down the scanning phase.
125+
if (build.initialOptions.plugins?.some((v) => v.name === 'vite:dep-scan')) return;
126+
127+
const filter = /\.svelte\.[jt]s(?:\?.*)?$/;
128+
/** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */
129+
let statsCollection;
130+
build.onStart(() => {
131+
statsCollection = options.stats?.startCollection('prebundle library modules', {
132+
logResult: (c) => c.stats.length > 1
133+
});
134+
});
135+
build.onLoad({ filter }, async ({ path: filename }) => {
136+
const code = readFileSync(filename, 'utf8');
137+
try {
138+
const contents = await compileSvelteModule(options, { filename, code }, statsCollection);
139+
return { contents };
140+
} catch (e) {
141+
return { errors: [toESBuildError(e, options)] };
142+
}
143+
});
144+
build.onEnd(() => {
145+
statsCollection?.finish();
146+
});
147+
}
148+
};
149+
}
150+
151+
/**
152+
* @param {import('../types/options.d.ts').ResolvedOptions} options
153+
* @param {{ filename: string; code: string }} input
154+
* @param {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection} [statsCollection]
155+
* @returns {Promise<string>}
156+
*/
157+
async function compileSvelteModule(options, { filename, code }, statsCollection) {
158+
const endStat = statsCollection?.start(filename);
159+
const compiled = svelte.compileModule(code, {
160+
dev: options.compilerOptions?.dev ?? true, // default to dev: true because prebundling is only used in dev
161+
filename,
162+
generate: 'client'
163+
});
164+
if (endStat) {
165+
endStat();
166+
}
167+
return compiled.js.map
168+
? compiled.js.code + '//# sourceMappingURL=' + compiled.js.map.toUrl()
169+
: compiled.js.code;
170+
}

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

+23-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createFilter, normalizePath } from 'vite';
22
import * as fs from 'node:fs';
33
import { log } from './log.js';
4+
import { DEFAULT_SVELTE_MODULE_EXT, DEFAULT_SVELTE_MODULE_INFIX } from './constants.js';
45

56
const VITE_FS_PREFIX = '/@fs/';
67
const IS_WINDOWS = process.platform === 'win32';
@@ -169,6 +170,21 @@ function buildFilter(include, exclude, extensions) {
169170
return (filename) => rollupFilter(filename) && extensions.some((ext) => filename.endsWith(ext));
170171
}
171172

173+
/**
174+
* @param {import('../public.d.ts').Options['include'] | undefined} include
175+
* @param {import('../public.d.ts').Options['exclude'] | undefined} exclude
176+
* @param {string[]} infixes
177+
* @param {string[]} extensions
178+
* @returns {(filename: string) => boolean}
179+
*/
180+
function buildModuleFilter(include, exclude, infixes, extensions) {
181+
const rollupFilter = createFilter(include, exclude);
182+
return (filename) =>
183+
rollupFilter(filename) &&
184+
infixes.some((infix) => filename.includes(infix)) &&
185+
extensions.some((ext) => filename.endsWith(ext));
186+
}
187+
172188
/**
173189
* @param {import('../types/options.d.ts').ResolvedOptions} options
174190
* @returns {import('../types/id.d.ts').IdParser}
@@ -190,10 +206,15 @@ export function buildIdParser(options) {
190206
* @returns {import('../types/id.d.ts').ModuleIdParser}
191207
*/
192208
export function buildModuleIdParser(options) {
193-
const { include, exclude, extensions } = options?.experimental?.compileModule ?? {};
209+
const {
210+
include,
211+
exclude,
212+
infixes = DEFAULT_SVELTE_MODULE_INFIX,
213+
extensions = DEFAULT_SVELTE_MODULE_EXT
214+
} = options?.experimental?.compileModule ?? {};
194215
const root = options.root;
195216
const normalizedRoot = normalizePath(root);
196-
const filter = buildFilter(include, exclude, extensions ?? ['.svelte.js', '.svelte.ts']);
217+
const filter = buildModuleFilter(include, exclude, infixes, extensions);
197218
return (id, ssr, timestamp = Date.now()) => {
198219
const { filename, rawQuery } = splitId(id);
199220
if (filter(filename)) {

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

+18-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { normalizePath } from 'vite';
33
import { isDebugNamespaceEnabled, log } from './log.js';
44
import { loadSvelteConfig } from './load-svelte-config.js';
55
import {
6+
DEFAULT_SVELTE_EXT,
67
FAQ_LINK_MISSING_EXPORTS_CONDITION,
78
SVELTE_EXPORT_CONDITIONS,
89
SVELTE_IMPORTS,
@@ -11,7 +12,12 @@ import {
1112
} from './constants.js';
1213

1314
import path from 'node:path';
14-
import { esbuildSveltePlugin, facadeEsbuildSveltePluginName } from './esbuild.js';
15+
import {
16+
esbuildSvelteModulePlugin,
17+
esbuildSveltePlugin,
18+
facadeEsbuildSvelteModulePluginName,
19+
facadeEsbuildSveltePluginName
20+
} from './esbuild.js';
1521
import { addExtraPreprocessors } from './preprocess.js';
1622
import deepmerge from 'deepmerge';
1723
import {
@@ -137,7 +143,7 @@ export async function preResolveOptions(inlineOptions, viteUserConfig, viteEnv)
137143
const isBuild = viteEnv.command === 'build';
138144
/** @type {Partial<import('../types/options.d.ts').PreResolvedOptions>} */
139145
const defaultOptions = {
140-
extensions: ['.svelte'],
146+
extensions: DEFAULT_SVELTE_EXT,
141147
emitCss: true,
142148
prebundleSvelteLibraries: !isBuild
143149
};
@@ -383,7 +389,10 @@ export async function buildExtraViteConfig(options, config) {
383389
// Currently a placeholder as more information is needed after Vite config is resolved,
384390
// the real Svelte plugin is added in `patchResolvedViteConfig()`
385391
esbuildOptions: {
386-
plugins: [{ name: facadeEsbuildSveltePluginName, setup: () => {} }]
392+
plugins: [
393+
{ name: facadeEsbuildSveltePluginName, setup: () => {} },
394+
{ name: facadeEsbuildSvelteModulePluginName, setup: () => {} }
395+
]
387396
}
388397
};
389398
}
@@ -583,6 +592,12 @@ export function patchResolvedViteConfig(viteConfig, options) {
583592
if (facadeEsbuildSveltePlugin) {
584593
Object.assign(facadeEsbuildSveltePlugin, esbuildSveltePlugin(options));
585594
}
595+
const facadeEsbuildSvelteModulePlugin = viteConfig.optimizeDeps.esbuildOptions?.plugins?.find(
596+
(plugin) => plugin.name === facadeEsbuildSvelteModulePluginName
597+
);
598+
if (facadeEsbuildSvelteModulePlugin) {
599+
Object.assign(facadeEsbuildSvelteModulePlugin, esbuildSvelteModulePlugin(options));
600+
}
586601
}
587602

588603
/**

packages/vite-plugin-svelte/types/index.d.ts

+9
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,15 @@ declare module '@sveltejs/vite-plugin-svelte' {
173173
}
174174

175175
interface CompileModuleOptions {
176+
/**
177+
* infix that must be present in filename
178+
* @default ['.svelte.']
179+
*/
180+
infixes?: string[];
181+
/**
182+
* module extensions
183+
* @default ['.ts','.js']
184+
*/
176185
extensions?: string[];
177186
include?: Arrayable<string>;
178187
exclude?: Arrayable<string>;

packages/vite-plugin-svelte/types/index.d.ts.map

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@
2121
null,
2222
null
2323
],
24-
"mappings": ";;;;aAMYA,OAAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA6GFC,YAAYA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAqEZC,qBAAqBA;;;;;;;;;;;;;iBChKtBC,MAAMA;iBCTNC,cAAcA;iBCgBRC,gBAAgBA"
24+
"mappings": ";;;;aAMYA,OAAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA6GFC,YAAYA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA8EZC,qBAAqBA;;;;;;;;;;;;;iBCzKtBC,MAAMA;iBCTNC,cAAcA;iBCgBRC,gBAAgBA"
2525
}

pnpm-lock.yaml

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)