Skip to content

Commit

Permalink
refactor: allow infix notation for svelte modules (#901)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
dominikg committed May 14, 2024
1 parent 85acc9f commit dfd4cc6
Show file tree
Hide file tree
Showing 15 changed files with 161 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .changeset/angry-pumpkins-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/vite-plugin-svelte': major
---

only prebundle files with default filenames (.svelte for components, .svelte.(js|ts) for modules)
5 changes: 5 additions & 0 deletions .changeset/friendly-wombats-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/vite-plugin-svelte': patch
---

prebundle with dev: true by default
8 changes: 8 additions & 0 deletions .changeset/lazy-bats-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@sveltejs/vite-plugin-svelte': minor
---

allow infix notation for svelte modules

Previously, only suffix notation `.svelte.js` was allowed, now you can also use `.svelte.test.js` or `.svelte.stories.js`.
This helps when writing testcases or other auxillary code where you may want to use runes too.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ async function expectPageToWork() {
expect(await getText('#api-only')).toBe('api loaded: true');
expect(await getText('#simple .label')).toBe('dependency-import');
expect(await getText('#exports-simple .label')).toBe('dependency-import');
expect(await getText('#module button:first-child')).toBe('count is 0');
}

if (!isBuild) {
Expand All @@ -31,6 +32,7 @@ if (!isBuild) {
expect(optimizedPaths).toContain('e2e-test-dep-svelte-exports-simple');
expect(optimizedPaths).toContain('e2e-test-dep-svelte-api-only');
expect(optimizedPaths).toContain('e2e-test-dep-svelte-nested');
expect(optimizedPaths).toContain('e2e-test-dep-svelte-module');
});

test('should not optimize excluded svelte dependencies', () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/e2e-tests/prebundle-svelte-deps/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"e2e-test-dep-svelte-exports-simple": "file:../_test_dependencies/svelte-exports-simple",
"e2e-test-dep-svelte-hybrid": "file:../_test_dependencies/svelte-hybrid",
"e2e-test-dep-svelte-nested": "file:../_test_dependencies/svelte-nested",
"e2e-test-dep-svelte-simple": "file:../_test_dependencies/svelte-simple"
"e2e-test-dep-svelte-simple": "file:../_test_dependencies/svelte-simple",
"e2e-test-dep-svelte-module": "file:../_test_dependencies/svelte-module"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "workspace:^",
Expand Down
4 changes: 4 additions & 0 deletions packages/e2e-tests/prebundle-svelte-deps/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { Message as Nested } from 'e2e-test-dep-svelte-nested';
import { setSomeContext } from 'e2e-test-dep-svelte-api-only';
import { getContext } from 'svelte';
import { Counter } from 'e2e-test-dep-svelte-module';
setSomeContext();
const apiOnlyLoaded = !!getContext('svelte-api-only');
</script>
Expand All @@ -23,4 +24,7 @@
<div id="exports-simple">
<Dependency />
</div>
<div id="module">
<Counter />
</div>
</main>
9 changes: 9 additions & 0 deletions packages/vite-plugin-svelte/src/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ interface ExperimentalOptions {
}

interface CompileModuleOptions {
/**
* infix that must be present in filename
* @default ['.svelte.']
*/
infixes?: string[];
/**
* module extensions
* @default ['.ts','.js']
*/
extensions?: string[];
include?: Arrayable<string>;
exclude?: Arrayable<string>;
Expand Down
3 changes: 1 addition & 2 deletions packages/vite-plugin-svelte/src/utils/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,11 @@ export function createCompileSvelte() {
...dynamicCompileOptions
}
: compileOptions;

const endStat = stats?.start(filename);
/** @type {import('svelte/compiler').CompileResult} */
let compiled;
try {
compiled = svelte.compile(finalCode, finalCompileOptions);
compiled = svelte.compile(finalCode, { ...finalCompileOptions, filename: filename });
// patch output with partial accept until svelte does it
// TODO remove later
if (
Expand Down
4 changes: 4 additions & 0 deletions packages/vite-plugin-svelte/src/utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ export const SVELTE_EXPORT_CONDITIONS = ['svelte'];

export const FAQ_LINK_MISSING_EXPORTS_CONDITION =
'https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/faq.md#missing-exports-condition';

export const DEFAULT_SVELTE_EXT = ['.svelte'];
export const DEFAULT_SVELTE_MODULE_INFIX = ['.svelte.'];
export const DEFAULT_SVELTE_MODULE_EXT = ['.js', '.ts'];
83 changes: 62 additions & 21 deletions packages/vite-plugin-svelte/src/utils/esbuild.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import { toESBuildError } from './error.js';
*/

export const facadeEsbuildSveltePluginName = 'vite-plugin-svelte:facade';

const svelteModuleExtension = '.svelte.js';
export const facadeEsbuildSvelteModulePluginName = 'vite-plugin-svelte-module:facade';

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

const svelteExtensions = (options.extensions ?? ['.svelte']).map((ext) => ext.slice(1));
svelteExtensions.push(svelteModuleExtension.slice(1));

const svelteFilter = new RegExp('\\.(' + svelteExtensions.join('|') + ')(\\?.*)?$');
const filter = /\.svelte(?:\?.*)?$/;
/** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */
let statsCollection;
build.onStart(() => {
statsCollection = options.stats?.startCollection('prebundle libraries', {
statsCollection = options.stats?.startCollection('prebundle library components', {
logResult: (c) => c.stats.length > 1
});
});
build.onLoad({ filter: svelteFilter }, async ({ path: filename }) => {
build.onLoad({ filter }, async ({ path: filename }) => {
const code = readFileSync(filename, 'utf8');
try {
const contents = await compileSvelte(options, { filename, code }, statsCollection);
Expand All @@ -58,26 +54,14 @@ export function esbuildSveltePlugin(options) {
* @returns {Promise<string>}
*/
async function compileSvelte(options, { filename, code }, statsCollection) {
if (filename.endsWith(svelteModuleExtension)) {
const endStat = statsCollection?.start(filename);
const compiled = svelte.compileModule(code, {
filename,
generate: 'client'
});
if (endStat) {
endStat();
}
return compiled.js.map
? compiled.js.code + '//# sourceMappingURL=' + compiled.js.map.toUrl()
: compiled.js.code;
}
let css = options.compilerOptions.css;
if (css !== 'injected') {
// TODO ideally we'd be able to externalize prebundled styles too, but for now always put them in the js
css = 'injected';
}
/** @type {import('svelte/compiler').CompileOptions} */
const compileOptions = {
dev: true, // default to dev: true because prebundling is only used in dev
...options.compilerOptions,
css,
filename,
Expand Down Expand Up @@ -127,3 +111,60 @@ async function compileSvelte(options, { filename, code }, statsCollection) {
? compiled.js.code + '//# sourceMappingURL=' + compiled.js.map.toUrl()
: compiled.js.code;
}

/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {EsbuildPlugin}
*/
export function esbuildSvelteModulePlugin(options) {
return {
name: 'vite-plugin-svelte-module:optimize-svelte',
setup(build) {
// Skip in scanning phase as Vite already handles scanning Svelte files.
// Otherwise this would heavily slow down the scanning phase.
if (build.initialOptions.plugins?.some((v) => v.name === 'vite:dep-scan')) return;

const filter = /\.svelte\.[jt]s(?:\?.*)?$/;
/** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */
let statsCollection;
build.onStart(() => {
statsCollection = options.stats?.startCollection('prebundle library modules', {
logResult: (c) => c.stats.length > 1
});
});
build.onLoad({ filter }, async ({ path: filename }) => {
const code = readFileSync(filename, 'utf8');
try {
const contents = await compileSvelteModule(options, { filename, code }, statsCollection);
return { contents };
} catch (e) {
return { errors: [toESBuildError(e, options)] };
}
});
build.onEnd(() => {
statsCollection?.finish();
});
}
};
}

/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @param {{ filename: string; code: string }} input
* @param {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection} [statsCollection]
* @returns {Promise<string>}
*/
async function compileSvelteModule(options, { filename, code }, statsCollection) {
const endStat = statsCollection?.start(filename);
const compiled = svelte.compileModule(code, {
dev: options.compilerOptions?.dev ?? true, // default to dev: true because prebundling is only used in dev
filename,
generate: 'client'
});
if (endStat) {
endStat();
}
return compiled.js.map
? compiled.js.code + '//# sourceMappingURL=' + compiled.js.map.toUrl()
: compiled.js.code;
}
25 changes: 23 additions & 2 deletions packages/vite-plugin-svelte/src/utils/id.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createFilter, normalizePath } from 'vite';
import * as fs from 'node:fs';
import { log } from './log.js';
import { DEFAULT_SVELTE_MODULE_EXT, DEFAULT_SVELTE_MODULE_INFIX } from './constants.js';

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

/**
* @param {import('../public.d.ts').Options['include'] | undefined} include
* @param {import('../public.d.ts').Options['exclude'] | undefined} exclude
* @param {string[]} infixes
* @param {string[]} extensions
* @returns {(filename: string) => boolean}
*/
function buildModuleFilter(include, exclude, infixes, extensions) {
const rollupFilter = createFilter(include, exclude);
return (filename) =>
rollupFilter(filename) &&
infixes.some((infix) => filename.includes(infix)) &&
extensions.some((ext) => filename.endsWith(ext));
}

/**
* @param {import('../types/options.d.ts').ResolvedOptions} options
* @returns {import('../types/id.d.ts').IdParser}
Expand All @@ -190,10 +206,15 @@ export function buildIdParser(options) {
* @returns {import('../types/id.d.ts').ModuleIdParser}
*/
export function buildModuleIdParser(options) {
const { include, exclude, extensions } = options?.experimental?.compileModule ?? {};
const {
include,
exclude,
infixes = DEFAULT_SVELTE_MODULE_INFIX,
extensions = DEFAULT_SVELTE_MODULE_EXT
} = options?.experimental?.compileModule ?? {};
const root = options.root;
const normalizedRoot = normalizePath(root);
const filter = buildFilter(include, exclude, extensions ?? ['.svelte.js', '.svelte.ts']);
const filter = buildModuleFilter(include, exclude, infixes, extensions);
return (id, ssr, timestamp = Date.now()) => {
const { filename, rawQuery } = splitId(id);
if (filter(filename)) {
Expand Down
21 changes: 18 additions & 3 deletions packages/vite-plugin-svelte/src/utils/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { normalizePath } from 'vite';
import { isDebugNamespaceEnabled, log } from './log.js';
import { loadSvelteConfig } from './load-svelte-config.js';
import {
DEFAULT_SVELTE_EXT,
FAQ_LINK_MISSING_EXPORTS_CONDITION,
SVELTE_EXPORT_CONDITIONS,
SVELTE_IMPORTS,
Expand All @@ -11,7 +12,12 @@ import {
} from './constants.js';

import path from 'node:path';
import { esbuildSveltePlugin, facadeEsbuildSveltePluginName } from './esbuild.js';
import {
esbuildSvelteModulePlugin,
esbuildSveltePlugin,
facadeEsbuildSvelteModulePluginName,
facadeEsbuildSveltePluginName
} from './esbuild.js';
import { addExtraPreprocessors } from './preprocess.js';
import deepmerge from 'deepmerge';
import {
Expand Down Expand Up @@ -137,7 +143,7 @@ export async function preResolveOptions(inlineOptions, viteUserConfig, viteEnv)
const isBuild = viteEnv.command === 'build';
/** @type {Partial<import('../types/options.d.ts').PreResolvedOptions>} */
const defaultOptions = {
extensions: ['.svelte'],
extensions: DEFAULT_SVELTE_EXT,
emitCss: true,
prebundleSvelteLibraries: !isBuild
};
Expand Down Expand Up @@ -383,7 +389,10 @@ export async function buildExtraViteConfig(options, config) {
// Currently a placeholder as more information is needed after Vite config is resolved,
// the real Svelte plugin is added in `patchResolvedViteConfig()`
esbuildOptions: {
plugins: [{ name: facadeEsbuildSveltePluginName, setup: () => {} }]
plugins: [
{ name: facadeEsbuildSveltePluginName, setup: () => {} },
{ name: facadeEsbuildSvelteModulePluginName, setup: () => {} }
]
}
};
}
Expand Down Expand Up @@ -583,6 +592,12 @@ export function patchResolvedViteConfig(viteConfig, options) {
if (facadeEsbuildSveltePlugin) {
Object.assign(facadeEsbuildSveltePlugin, esbuildSveltePlugin(options));
}
const facadeEsbuildSvelteModulePlugin = viteConfig.optimizeDeps.esbuildOptions?.plugins?.find(
(plugin) => plugin.name === facadeEsbuildSvelteModulePluginName
);
if (facadeEsbuildSvelteModulePlugin) {
Object.assign(facadeEsbuildSvelteModulePlugin, esbuildSvelteModulePlugin(options));
}
}

/**
Expand Down
9 changes: 9 additions & 0 deletions packages/vite-plugin-svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,15 @@ declare module '@sveltejs/vite-plugin-svelte' {
}

interface CompileModuleOptions {
/**
* infix that must be present in filename
* @default ['.svelte.']
*/
infixes?: string[];
/**
* module extensions
* @default ['.ts','.js']
*/
extensions?: string[];
include?: Arrayable<string>;
exclude?: Arrayable<string>;
Expand Down
2 changes: 1 addition & 1 deletion packages/vite-plugin-svelte/types/index.d.ts.map
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@
null,
null
],
"mappings": ";;;;aAMYA,OAAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA6GFC,YAAYA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAqEZC,qBAAqBA;;;;;;;;;;;;;iBChKtBC,MAAMA;iBCTNC,cAAcA;iBCgBRC,gBAAgBA"
"mappings": ";;;;aAMYA,OAAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA6GFC,YAAYA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA8EZC,qBAAqBA;;;;;;;;;;;;;iBCzKtBC,MAAMA;iBCTNC,cAAcA;iBCgBRC,gBAAgBA"
}
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit dfd4cc6

Please sign in to comment.