@sveltejs/vite-plugin-svelte 1.0.0-next.9 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,159 @@
1
+ import { CompileOptions, ResolvedOptions } from './options';
2
+ import { compile, preprocess, walk } from 'svelte/compiler';
3
+ // @ts-ignore
4
+ import { createMakeHot } from 'svelte-hmr';
5
+ import { SvelteRequest } from './id';
6
+ import { safeBase64Hash } from './hash';
7
+ import { log } from './log';
8
+
9
+ const scriptLangRE = /<script [^>]*lang=["']?([^"' >]+)["']?[^>]*>/;
10
+
11
+ const _createCompileSvelte = (makeHot: Function) =>
12
+ async function compileSvelte(
13
+ svelteRequest: SvelteRequest,
14
+ code: string,
15
+ options: Partial<ResolvedOptions>
16
+ ): Promise<CompileData> {
17
+ const { filename, normalizedFilename, cssId, ssr } = svelteRequest;
18
+ const { emitCss = true } = options;
19
+ const dependencies = [];
20
+
21
+ const compileOptions: CompileOptions = {
22
+ ...options.compilerOptions,
23
+ filename,
24
+ generate: ssr ? 'ssr' : 'dom',
25
+ format: 'esm'
26
+ };
27
+ if (options.hot && options.emitCss) {
28
+ const hash = `s-${safeBase64Hash(normalizedFilename)}`;
29
+ log.debug(`setting cssHash ${hash} for ${normalizedFilename}`);
30
+ compileOptions.cssHash = () => hash;
31
+ }
32
+ if (ssr && compileOptions.enableSourcemap !== false) {
33
+ if (typeof compileOptions.enableSourcemap === 'object') {
34
+ compileOptions.enableSourcemap.css = false;
35
+ } else {
36
+ compileOptions.enableSourcemap = { js: true, css: false };
37
+ }
38
+ }
39
+
40
+ let preprocessed;
41
+
42
+ if (options.preprocess) {
43
+ try {
44
+ preprocessed = await preprocess(code, options.preprocess, { filename });
45
+ } catch (e) {
46
+ e.message = `Error while preprocessing ${filename}${e.message ? ` - ${e.message}` : ''}`;
47
+ throw e;
48
+ }
49
+
50
+ if (preprocessed.dependencies) dependencies.push(...preprocessed.dependencies);
51
+ if (preprocessed.map) compileOptions.sourcemap = preprocessed.map;
52
+ }
53
+ const finalCode = preprocessed ? preprocessed.code : code;
54
+ const dynamicCompileOptions = await options.experimental?.dynamicCompileOptions?.({
55
+ filename,
56
+ code: finalCode,
57
+ compileOptions
58
+ });
59
+ if (dynamicCompileOptions && log.debug.enabled) {
60
+ log.debug(
61
+ `dynamic compile options for ${filename}: ${JSON.stringify(dynamicCompileOptions)}`
62
+ );
63
+ }
64
+ const finalCompileOptions = dynamicCompileOptions
65
+ ? {
66
+ ...compileOptions,
67
+ ...dynamicCompileOptions
68
+ }
69
+ : compileOptions;
70
+ const compiled = compile(finalCode, finalCompileOptions);
71
+
72
+ if (emitCss && compiled.css.code) {
73
+ // TODO properly update sourcemap?
74
+ compiled.js.code += `\nimport ${JSON.stringify(cssId)};\n`;
75
+ }
76
+
77
+ // only apply hmr when not in ssr context and hot options are set
78
+ if (!ssr && makeHot) {
79
+ compiled.js.code = makeHot({
80
+ id: filename,
81
+ compiledCode: compiled.js.code,
82
+ hotOptions: options.hot,
83
+ compiled,
84
+ originalCode: code,
85
+ compileOptions: finalCompileOptions
86
+ });
87
+ }
88
+
89
+ compiled.js.dependencies = dependencies;
90
+
91
+ return {
92
+ filename,
93
+ normalizedFilename,
94
+ lang: code.match(scriptLangRE)?.[1] || 'js',
95
+ // @ts-ignore
96
+ compiled,
97
+ ssr,
98
+ dependencies
99
+ };
100
+ };
101
+
102
+ function buildMakeHot(options: ResolvedOptions) {
103
+ const needsMakeHot = options.hot !== false && options.isServe && !options.isProduction;
104
+ if (needsMakeHot) {
105
+ // @ts-ignore
106
+ const hotApi = options?.hot?.hotApi;
107
+ // @ts-ignore
108
+ const adapter = options?.hot?.adapter;
109
+ return createMakeHot({
110
+ walk,
111
+ hotApi,
112
+ adapter,
113
+ hotOptions: { noOverlay: true, ...(options.hot as object) }
114
+ });
115
+ }
116
+ }
117
+
118
+ export function createCompileSvelte(options: ResolvedOptions) {
119
+ const makeHot = buildMakeHot(options);
120
+ return _createCompileSvelte(makeHot);
121
+ }
122
+
123
+ export interface Code {
124
+ code: string;
125
+ map?: any;
126
+ dependencies?: any[];
127
+ }
128
+
129
+ export interface Compiled {
130
+ js: Code;
131
+ css: Code;
132
+ ast: any; // TODO type
133
+ warnings: any[]; // TODO type
134
+ vars: {
135
+ name: string;
136
+ export_name: string;
137
+ injected: boolean;
138
+ module: boolean;
139
+ mutated: boolean;
140
+ reassigned: boolean;
141
+ referenced: boolean;
142
+ writable: boolean;
143
+ referenced_from_script: boolean;
144
+ }[];
145
+ stats: {
146
+ timings: {
147
+ total: number;
148
+ };
149
+ };
150
+ }
151
+
152
+ export interface CompileData {
153
+ filename: string;
154
+ normalizedFilename: string;
155
+ lang: string;
156
+ compiled: Compiled;
157
+ ssr: boolean | undefined;
158
+ dependencies: string[];
159
+ }
@@ -0,0 +1,20 @@
1
+ const VITE_RESOLVE_MAIN_FIELDS = ['module', 'jsnext:main', 'jsnext'];
2
+
3
+ export const SVELTE_RESOLVE_MAIN_FIELDS = ['svelte', ...VITE_RESOLVE_MAIN_FIELDS];
4
+
5
+ export const SVELTE_IMPORTS = [
6
+ 'svelte/animate',
7
+ 'svelte/easing',
8
+ 'svelte/internal',
9
+ 'svelte/motion',
10
+ 'svelte/ssr',
11
+ 'svelte/store',
12
+ 'svelte/transition',
13
+ 'svelte'
14
+ ];
15
+
16
+ export const SVELTE_HMR_IMPORTS = [
17
+ 'svelte-hmr/runtime/hot-api-esm.js',
18
+ 'svelte-hmr/runtime/proxy-adapter-dom.js',
19
+ 'svelte-hmr'
20
+ ];
@@ -0,0 +1,241 @@
1
+ import { log } from './log';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import { createRequire } from 'module';
5
+
6
+ export function findRootSvelteDependencies(root: string, cwdFallback = true): SvelteDependency[] {
7
+ log.debug(`findSvelteDependencies: searching svelte dependencies in ${root}`);
8
+ const pkgFile = path.join(root, 'package.json');
9
+ if (!fs.existsSync(pkgFile)) {
10
+ if (cwdFallback) {
11
+ const cwd = process.cwd();
12
+ if (root !== cwd) {
13
+ log.debug(`no package.json found in vite root ${root}`);
14
+ return findRootSvelteDependencies(cwd, false);
15
+ }
16
+ }
17
+ log.warn(`no package.json found, findRootSvelteDependencies failed`);
18
+ return [];
19
+ }
20
+
21
+ const pkg = parsePkg(root);
22
+ if (!pkg) {
23
+ return [];
24
+ }
25
+
26
+ const deps = [
27
+ ...Object.keys(pkg.dependencies || {}),
28
+ ...Object.keys(pkg.devDependencies || {})
29
+ ].filter((dep) => !is_common_without_svelte_field(dep));
30
+
31
+ return getSvelteDependencies(deps, root);
32
+ }
33
+
34
+ function getSvelteDependencies(
35
+ deps: string[],
36
+ pkgDir: string,
37
+ path: string[] = []
38
+ ): SvelteDependency[] {
39
+ const result = [];
40
+ const localRequire = createRequire(`${pkgDir}/package.json`);
41
+ const resolvedDeps = deps
42
+ .map((dep) => resolveDependencyData(dep, localRequire))
43
+ .filter(Boolean) as DependencyData[];
44
+ for (const { pkg, dir } of resolvedDeps) {
45
+ const type = getSvelteDependencyType(pkg);
46
+ if (!type) continue;
47
+ result.push({ name: pkg.name, type, pkg, dir, path });
48
+ // continue crawling for component libraries so we can optimize them, js libraries are fine
49
+ if (type === 'component-library' && pkg.dependencies) {
50
+ let dependencyNames = Object.keys(pkg.dependencies);
51
+ const circular = dependencyNames.filter((name) => path.includes(name));
52
+ if (circular.length > 0) {
53
+ log.warn.enabled &&
54
+ log.warn(
55
+ `skipping circular svelte dependencies in automated vite optimizeDeps handling`,
56
+ circular.map((x) => path.concat(x).join('>'))
57
+ );
58
+ dependencyNames = dependencyNames.filter((name) => !path.includes(name));
59
+ }
60
+ if (path.length === 3) {
61
+ log.debug.once(`encountered deep svelte dependency tree: ${path.join('>')}`);
62
+ }
63
+ result.push(...getSvelteDependencies(dependencyNames, dir, path.concat(pkg.name)));
64
+ }
65
+ }
66
+ return result;
67
+ }
68
+
69
+ export function resolveDependencyData(
70
+ dep: string,
71
+ localRequire: NodeRequire
72
+ ): DependencyData | void {
73
+ try {
74
+ const pkgJson = `${dep}/package.json`;
75
+ const pkg = localRequire(pkgJson);
76
+ const dir = path.dirname(localRequire.resolve(pkgJson));
77
+ return { dir, pkg };
78
+ } catch (e) {
79
+ log.debug.once(`dependency ${dep} does not export package.json`, e);
80
+ // walk up from default export until we find package.json with name=dep
81
+ try {
82
+ let dir = path.dirname(localRequire.resolve(dep));
83
+ while (dir) {
84
+ const pkg = parsePkg(dir, true);
85
+ if (pkg && pkg.name === dep) {
86
+ return { dir, pkg };
87
+ }
88
+ const parent = path.dirname(dir);
89
+ if (parent === dir) {
90
+ break;
91
+ }
92
+ dir = parent;
93
+ }
94
+ } catch (e) {
95
+ log.debug.once(`error while trying to find package.json of ${dep}`, e);
96
+ }
97
+ }
98
+ log.debug.once(`failed to resolve ${dep}`);
99
+ }
100
+
101
+ function parsePkg(dir: string, silent = false): Pkg | void {
102
+ const pkgFile = path.join(dir, 'package.json');
103
+ try {
104
+ return JSON.parse(fs.readFileSync(pkgFile, 'utf-8'));
105
+ } catch (e) {
106
+ !silent && log.warn.enabled && log.warn(`failed to parse ${pkgFile}`, e);
107
+ }
108
+ }
109
+
110
+ function getSvelteDependencyType(pkg: Pkg): SvelteDependencyType | undefined {
111
+ if (isSvelteComponentLib(pkg)) {
112
+ return 'component-library';
113
+ } else if (isSvelteLib(pkg)) {
114
+ return 'js-library';
115
+ } else {
116
+ return undefined;
117
+ }
118
+ }
119
+
120
+ function isSvelteComponentLib(pkg: Pkg) {
121
+ return !!pkg.svelte;
122
+ }
123
+
124
+ function isSvelteLib(pkg: Pkg) {
125
+ return !!pkg.dependencies?.svelte || !!pkg.peerDependencies?.svelte;
126
+ }
127
+
128
+ const COMMON_DEPENDENCIES_WITHOUT_SVELTE_FIELD = [
129
+ '@lukeed/uuid',
130
+ '@playwright/test',
131
+ '@sveltejs/vite-plugin-svelte',
132
+ '@sveltejs/kit',
133
+ 'autoprefixer',
134
+ 'cookie',
135
+ 'dotenv',
136
+ 'esbuild',
137
+ 'eslint',
138
+ 'jest',
139
+ 'mdsvex',
140
+ 'playwright',
141
+ 'postcss',
142
+ 'prettier',
143
+ 'svelte',
144
+ 'svelte-check',
145
+ 'svelte-hmr',
146
+ 'svelte-preprocess',
147
+ 'tslib',
148
+ 'typescript',
149
+ 'vite',
150
+ 'vitest',
151
+ '__vite-browser-external' // see https://github.com/sveltejs/vite-plugin-svelte/issues/362
152
+ ];
153
+ const COMMON_PREFIXES_WITHOUT_SVELTE_FIELD = [
154
+ '@fontsource/',
155
+ '@postcss-plugins/',
156
+ '@rollup/',
157
+ '@sveltejs/adapter-',
158
+ '@types/',
159
+ '@typescript-eslint/',
160
+ 'eslint-',
161
+ 'jest-',
162
+ 'postcss-plugin-',
163
+ 'prettier-plugin-',
164
+ 'rollup-plugin-',
165
+ 'vite-plugin-'
166
+ ];
167
+
168
+ /**
169
+ * Test for common dependency names that tell us it is not a package including a svelte field, eg. eslint + plugins.
170
+ *
171
+ * This speeds up the find process as we don't have to try and require the package.json for all of them
172
+ *
173
+ * @param dependency {string}
174
+ * @returns {boolean} true if it is a dependency without a svelte field
175
+ */
176
+ export function is_common_without_svelte_field(dependency: string): boolean {
177
+ return (
178
+ COMMON_DEPENDENCIES_WITHOUT_SVELTE_FIELD.includes(dependency) ||
179
+ COMMON_PREFIXES_WITHOUT_SVELTE_FIELD.some(
180
+ (prefix) =>
181
+ prefix.startsWith('@')
182
+ ? dependency.startsWith(prefix)
183
+ : dependency.substring(dependency.lastIndexOf('/') + 1).startsWith(prefix) // check prefix omitting @scope/
184
+ )
185
+ );
186
+ }
187
+
188
+ export function needsOptimization(dep: string, localRequire: NodeRequire): boolean {
189
+ const depData = resolveDependencyData(dep, localRequire);
190
+ if (!depData) return false;
191
+ const pkg = depData.pkg;
192
+ // only optimize if is cjs, using the below as heuristic
193
+ // see https://github.com/sveltejs/vite-plugin-svelte/issues/162
194
+ const hasEsmFields = pkg.module || pkg.exports;
195
+ if (hasEsmFields) return false;
196
+ if (pkg.main) {
197
+ // ensure entry is js so vite can prebundle it
198
+ // see https://github.com/sveltejs/vite-plugin-svelte/issues/233
199
+ const entryExt = path.extname(pkg.main);
200
+ return !entryExt || entryExt === '.js' || entryExt === '.cjs';
201
+ } else {
202
+ // check if has implicit index.js entrypoint
203
+ // https://github.com/sveltejs/vite-plugin-svelte/issues/281
204
+ try {
205
+ localRequire.resolve(`${dep}/index.js`);
206
+ return true;
207
+ } catch {
208
+ return false;
209
+ }
210
+ }
211
+ }
212
+
213
+ interface DependencyData {
214
+ dir: string;
215
+ pkg: Pkg;
216
+ }
217
+
218
+ export interface SvelteDependency {
219
+ name: string;
220
+ type: SvelteDependencyType;
221
+ dir: string;
222
+ pkg: Pkg;
223
+ path: string[];
224
+ }
225
+
226
+ // component-library => exports svelte components
227
+ // js-library => only uses svelte api, no components
228
+ export type SvelteDependencyType = 'component-library' | 'js-library';
229
+
230
+ export interface Pkg {
231
+ name: string;
232
+ svelte?: string;
233
+ dependencies?: DependencyList;
234
+ devDependencies?: DependencyList;
235
+ peerDependencies?: DependencyList;
236
+ [key: string]: any;
237
+ }
238
+
239
+ export interface DependencyList {
240
+ [key: string]: string;
241
+ }
@@ -0,0 +1,95 @@
1
+ import { RollupError } from 'rollup';
2
+ import { ResolvedOptions, Warning } from './options';
3
+ import { buildExtendedLogMessage } from './log';
4
+ import { PartialMessage } from 'esbuild';
5
+
6
+ /**
7
+ * convert an error thrown by svelte.compile to a RollupError so that vite displays it in a user friendly way
8
+ * @param error a svelte compiler error, which is a mix of Warning and an error
9
+ * @returns {RollupError} the converted error
10
+ */
11
+ export function toRollupError(error: Warning & Error, options: ResolvedOptions): RollupError {
12
+ const { filename, frame, start, code, name, stack } = error;
13
+ const rollupError: RollupError = {
14
+ name, // needed otherwise sveltekit coalesce_to_error turns it into a string
15
+ id: filename,
16
+ message: buildExtendedLogMessage(error), // include filename:line:column so that it's clickable
17
+ frame: formatFrameForVite(frame),
18
+ code,
19
+ stack: options.isBuild || options.isDebug || !frame ? stack : ''
20
+ };
21
+ if (start) {
22
+ rollupError.loc = {
23
+ line: start.line,
24
+ column: start.column,
25
+ file: filename
26
+ };
27
+ }
28
+ return rollupError;
29
+ }
30
+
31
+ /**
32
+ * convert an error thrown by svelte.compile to an esbuild PartialMessage
33
+ * @param error a svelte compiler error, which is a mix of Warning and an error
34
+ * @returns {PartialMessage} the converted error
35
+ */
36
+ export function toESBuildError(error: Warning & Error, options: ResolvedOptions): PartialMessage {
37
+ const { filename, frame, start, stack } = error;
38
+ const partialMessage: PartialMessage = {
39
+ text: buildExtendedLogMessage(error)
40
+ };
41
+ if (start) {
42
+ partialMessage.location = {
43
+ line: start.line,
44
+ column: start.column,
45
+ file: filename,
46
+ lineText: lineFromFrame(start.line, frame) // needed to get a meaningful error message on cli
47
+ };
48
+ }
49
+ if (options.isBuild || options.isDebug || !frame) {
50
+ partialMessage.detail = stack;
51
+ }
52
+ return partialMessage;
53
+ }
54
+
55
+ /**
56
+ * extract line with number from codeframe
57
+ */
58
+ function lineFromFrame(lineNo: number, frame?: string): string {
59
+ if (!frame) {
60
+ return '';
61
+ }
62
+ const lines = frame.split('\n');
63
+ const errorLine = lines.find((line) => line.trimStart().startsWith(`${lineNo}: `));
64
+ return errorLine ? errorLine.substring(errorLine.indexOf(': ') + 3) : '';
65
+ }
66
+
67
+ /**
68
+ * vite error overlay expects a specific format to show frames
69
+ * this reformats svelte frame (colon separated, less whitespace)
70
+ * to one that vite displays on overlay ( pipe separated, more whitespace)
71
+ * e.g.
72
+ * ```
73
+ * 1: foo
74
+ * 2: bar;
75
+ * ^
76
+ * 3: baz
77
+ * ```
78
+ * to
79
+ * ```
80
+ * 1 | foo
81
+ * 2 | bar;
82
+ * ^
83
+ * 3 | baz
84
+ * ```
85
+ * @see https://github.com/vitejs/vite/blob/96591bf9989529de839ba89958755eafe4c445ae/packages/vite/src/client/overlay.ts#L116
86
+ */
87
+ function formatFrameForVite(frame?: string): string {
88
+ if (!frame) {
89
+ return '';
90
+ }
91
+ return frame
92
+ .split('\n')
93
+ .map((line) => (line.match(/^\s+\^/) ? ' ' + line : ' ' + line.replace(':', ' | ')))
94
+ .join('\n');
95
+ }
@@ -0,0 +1,84 @@
1
+ import { promises as fs } from 'fs';
2
+ import { compile, preprocess } from 'svelte/compiler';
3
+ import { DepOptimizationOptions } from 'vite';
4
+ import { Compiled } from './compile';
5
+ import { log } from './log';
6
+ import { CompileOptions, ResolvedOptions } from './options';
7
+ import { toESBuildError } from './error';
8
+
9
+ type EsbuildOptions = NonNullable<DepOptimizationOptions['esbuildOptions']>;
10
+ type EsbuildPlugin = NonNullable<EsbuildOptions['plugins']>[number];
11
+
12
+ export const facadeEsbuildSveltePluginName = 'vite-plugin-svelte:facade';
13
+
14
+ export function esbuildSveltePlugin(options: ResolvedOptions): EsbuildPlugin {
15
+ return {
16
+ name: 'vite-plugin-svelte:optimize-svelte',
17
+ setup(build) {
18
+ // Skip in scanning phase as Vite already handles scanning Svelte files.
19
+ // Otherwise this would heavily slow down the scanning phase.
20
+ if (build.initialOptions.plugins?.some((v) => v.name === 'vite:dep-scan')) return;
21
+
22
+ const svelteExtensions = (options.extensions ?? ['.svelte']).map((ext) => ext.slice(1));
23
+ const svelteFilter = new RegExp(`\\.(` + svelteExtensions.join('|') + `)(\\?.*)?$`);
24
+
25
+ build.onLoad({ filter: svelteFilter }, async ({ path: filename }) => {
26
+ const code = await fs.readFile(filename, 'utf8');
27
+ try {
28
+ const contents = await compileSvelte(options, { filename, code });
29
+ return { contents };
30
+ } catch (e) {
31
+ return { errors: [toESBuildError(e, options)] };
32
+ }
33
+ });
34
+ }
35
+ };
36
+ }
37
+
38
+ async function compileSvelte(
39
+ options: ResolvedOptions,
40
+ { filename, code }: { filename: string; code: string }
41
+ ): Promise<string> {
42
+ const compileOptions: CompileOptions = {
43
+ ...options.compilerOptions,
44
+ css: true,
45
+ filename,
46
+ format: 'esm',
47
+ generate: 'dom'
48
+ };
49
+
50
+ let preprocessed;
51
+
52
+ if (options.preprocess) {
53
+ try {
54
+ preprocessed = await preprocess(code, options.preprocess, { filename });
55
+ } catch (e) {
56
+ e.message = `Error while preprocessing ${filename}${e.message ? ` - ${e.message}` : ''}`;
57
+ throw e;
58
+ }
59
+ if (preprocessed.map) compileOptions.sourcemap = preprocessed.map;
60
+ }
61
+
62
+ const finalCode = preprocessed ? preprocessed.code : code;
63
+
64
+ const dynamicCompileOptions = await options.experimental?.dynamicCompileOptions?.({
65
+ filename,
66
+ code: finalCode,
67
+ compileOptions
68
+ });
69
+
70
+ if (dynamicCompileOptions && log.debug.enabled) {
71
+ log.debug(`dynamic compile options for ${filename}: ${JSON.stringify(dynamicCompileOptions)}`);
72
+ }
73
+
74
+ const finalCompileOptions = dynamicCompileOptions
75
+ ? {
76
+ ...compileOptions,
77
+ ...dynamicCompileOptions
78
+ }
79
+ : compileOptions;
80
+
81
+ const compiled = compile(finalCode, finalCompileOptions) as Compiled;
82
+
83
+ return compiled.js.code + '//# sourceMappingURL=' + compiled.js.map.toUrl();
84
+ }
@@ -0,0 +1,32 @@
1
+ import * as crypto from 'crypto';
2
+
3
+ const hashes = Object.create(null);
4
+
5
+ //TODO shorter?
6
+ const hash_length = 12;
7
+
8
+ export function safeBase64Hash(input: string) {
9
+ if (hashes[input]) {
10
+ return hashes[input];
11
+ }
12
+ //TODO if performance really matters, use a faster one like xx-hash etc.
13
+ // should be evenly distributed because short input length and similarities in paths could cause collisions otherwise
14
+ // OR DON'T USE A HASH AT ALL, what about a simple counter?
15
+ const md5 = crypto.createHash('md5');
16
+ md5.update(input);
17
+ const hash = toSafe(md5.digest('base64')).slice(0, hash_length);
18
+ hashes[input] = hash;
19
+ return hash;
20
+ }
21
+
22
+ const replacements: { [key: string]: string } = {
23
+ '+': '-',
24
+ '/': '_',
25
+ '=': ''
26
+ };
27
+
28
+ const replaceRE = new RegExp(`[${Object.keys(replacements).join('')}]`, 'g');
29
+
30
+ function toSafe(base64: string) {
31
+ return base64.replace(replaceRE, (x) => replacements[x]);
32
+ }