@sveltejs/vite-plugin-svelte 1.0.0-next.3 → 1.0.0-next.33

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,92 @@
1
+ import { RollupError } from 'rollup';
2
+ import { 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): RollupError {
12
+ const { filename, frame, start, code, name } = 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: ''
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): PartialMessage {
37
+ const { filename, frame, start } = 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
+ return partialMessage;
50
+ }
51
+
52
+ /**
53
+ * extract line with number from codeframe
54
+ */
55
+ function lineFromFrame(lineNo: number, frame?: string): string {
56
+ if (!frame) {
57
+ return '';
58
+ }
59
+ const lines = frame.split('\n');
60
+ const errorLine = lines.find((line) => line.trimStart().startsWith(`${lineNo}: `));
61
+ return errorLine ? errorLine.substring(errorLine.indexOf(': ') + 3) : '';
62
+ }
63
+
64
+ /**
65
+ * vite error overlay expects a specific format to show frames
66
+ * this reformats svelte frame (colon separated, less whitespace)
67
+ * to one that vite displays on overlay ( pipe separated, more whitespace)
68
+ * e.g.
69
+ * ```
70
+ * 1: foo
71
+ * 2: bar;
72
+ * ^
73
+ * 3: baz
74
+ * ```
75
+ * to
76
+ * ```
77
+ * 1 | foo
78
+ * 2 | bar;
79
+ * ^
80
+ * 3 | baz
81
+ * ```
82
+ * @see https://github.com/vitejs/vite/blob/96591bf9989529de839ba89958755eafe4c445ae/packages/vite/src/client/overlay.ts#L116
83
+ */
84
+ function formatFrameForVite(frame?: string): string {
85
+ if (!frame) {
86
+ return '';
87
+ }
88
+ return frame
89
+ .split('\n')
90
+ .map((line) => (line.match(/^\s+\^/) ? ' ' + line : ' ' + line.replace(':', ' | ')))
91
+ .join('\n');
92
+ }
@@ -0,0 +1,102 @@
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
+ type EsbuildPluginBuild = Parameters<EsbuildPlugin['setup']>[0];
12
+
13
+ export const facadeEsbuildSveltePluginName = 'vite-plugin-svelte:facade';
14
+
15
+ export function esbuildSveltePlugin(options: ResolvedOptions): EsbuildPlugin {
16
+ return {
17
+ name: 'vite-plugin-svelte:optimize-svelte',
18
+ setup(build) {
19
+ disableVitePrebundleSvelte(build);
20
+
21
+ const svelteExtensions = (options.extensions ?? ['.svelte']).map((ext) => ext.slice(1));
22
+ const svelteFilter = new RegExp(`\\.(` + svelteExtensions.join('|') + `)(\\?.*)?$`);
23
+
24
+ build.onLoad({ filter: svelteFilter }, async ({ path: filename }) => {
25
+ const code = await fs.readFile(filename, 'utf8');
26
+ try {
27
+ const contents = await compileSvelte(options, { filename, code });
28
+ return { contents };
29
+ } catch (e) {
30
+ return { errors: [toESBuildError(e)] };
31
+ }
32
+ });
33
+ }
34
+ };
35
+ }
36
+
37
+ function disableVitePrebundleSvelte(build: EsbuildPluginBuild) {
38
+ const viteDepPrebundlePlugin = build.initialOptions.plugins?.find(
39
+ (v) => v.name === 'vite:dep-pre-bundle'
40
+ );
41
+
42
+ if (!viteDepPrebundlePlugin) return;
43
+
44
+ // Prevent vite:dep-pre-bundle from externalizing svelte files
45
+ const _setup = viteDepPrebundlePlugin.setup.bind(viteDepPrebundlePlugin);
46
+ viteDepPrebundlePlugin.setup = function (build) {
47
+ const _onResolve = build.onResolve.bind(build);
48
+ build.onResolve = function (options, callback) {
49
+ if (options.filter.source.includes('svelte')) {
50
+ options.filter = new RegExp(
51
+ options.filter.source.replace('|svelte', ''),
52
+ options.filter.flags
53
+ );
54
+ }
55
+ return _onResolve(options, callback);
56
+ };
57
+ return _setup(build);
58
+ };
59
+ }
60
+
61
+ async function compileSvelte(
62
+ options: ResolvedOptions,
63
+ { filename, code }: { filename: string; code: string }
64
+ ): Promise<string> {
65
+ const compileOptions: CompileOptions = {
66
+ ...options.compilerOptions,
67
+ css: true,
68
+ filename,
69
+ format: 'esm',
70
+ generate: 'dom'
71
+ };
72
+
73
+ let preprocessed;
74
+
75
+ if (options.preprocess) {
76
+ preprocessed = await preprocess(code, options.preprocess, { filename });
77
+ if (preprocessed.map) compileOptions.sourcemap = preprocessed.map;
78
+ }
79
+
80
+ const finalCode = preprocessed ? preprocessed.code : code;
81
+
82
+ const dynamicCompileOptions = await options.experimental?.dynamicCompileOptions?.({
83
+ filename,
84
+ code: finalCode,
85
+ compileOptions
86
+ });
87
+
88
+ if (dynamicCompileOptions && log.debug.enabled) {
89
+ log.debug(`dynamic compile options for ${filename}: ${JSON.stringify(dynamicCompileOptions)}`);
90
+ }
91
+
92
+ const finalCompileOptions = dynamicCompileOptions
93
+ ? {
94
+ ...compileOptions,
95
+ ...dynamicCompileOptions
96
+ }
97
+ : compileOptions;
98
+
99
+ const compiled = compile(finalCode, finalCompileOptions) as Compiled;
100
+
101
+ return compiled.js.code + '//# sourceMappingURL=' + compiled.js.map.toUrl();
102
+ }
@@ -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')).substr(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
+ }
@@ -0,0 +1,135 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import { createFilter } from '@rollup/pluginutils';
3
+ import { Arrayable, ResolvedOptions } from './options';
4
+ import { normalizePath } from 'vite';
5
+ import * as fs from 'fs';
6
+
7
+ const VITE_FS_PREFIX = '/@fs/';
8
+ const IS_WINDOWS = process.platform === 'win32';
9
+
10
+ export type SvelteQueryTypes = 'style' | 'script';
11
+
12
+ export interface RequestQuery {
13
+ // our own
14
+ svelte?: boolean;
15
+ type?: SvelteQueryTypes;
16
+ // vite specific
17
+ url?: boolean;
18
+ raw?: boolean;
19
+ }
20
+
21
+ export interface SvelteRequest {
22
+ id: string;
23
+ cssId: string;
24
+ filename: string;
25
+ normalizedFilename: string;
26
+ query: RequestQuery;
27
+ timestamp: number;
28
+ ssr: boolean;
29
+ }
30
+
31
+ function splitId(id: string) {
32
+ const parts = id.split(`?`, 2);
33
+ const filename = parts[0];
34
+ const rawQuery = parts[1];
35
+ return { filename, rawQuery };
36
+ }
37
+
38
+ function parseToSvelteRequest(
39
+ id: string,
40
+ filename: string,
41
+ rawQuery: string,
42
+ root: string,
43
+ timestamp: number,
44
+ ssr: boolean
45
+ ): SvelteRequest | undefined {
46
+ const query = parseRequestQuery(rawQuery);
47
+ if (query.url || query.raw) {
48
+ // skip requests with special vite tags
49
+ return;
50
+ }
51
+ const normalizedFilename = normalize(filename, root);
52
+ const cssId = createVirtualImportId(filename, root, 'style');
53
+
54
+ return {
55
+ id,
56
+ filename,
57
+ normalizedFilename,
58
+ cssId,
59
+ query,
60
+ timestamp,
61
+ ssr
62
+ };
63
+ }
64
+
65
+ function createVirtualImportId(filename: string, root: string, type: SvelteQueryTypes) {
66
+ const parts = ['svelte', `type=${type}`];
67
+ if (type === 'style') {
68
+ parts.push('lang.css');
69
+ }
70
+ if (existsInRoot(filename, root)) {
71
+ filename = root + filename;
72
+ } else if (filename.startsWith(VITE_FS_PREFIX)) {
73
+ filename = IS_WINDOWS
74
+ ? filename.slice(VITE_FS_PREFIX.length) // remove /@fs/ from /@fs/C:/...
75
+ : filename.slice(VITE_FS_PREFIX.length - 1); // remove /@fs from /@fs/home/user
76
+ }
77
+ // return same virtual id format as vite-plugin-vue eg ...App.svelte?svelte&type=style&lang.css
78
+ return `${filename}?${parts.join('&')}`;
79
+ }
80
+
81
+ function parseRequestQuery(rawQuery: string): RequestQuery {
82
+ const query = Object.fromEntries(new URLSearchParams(rawQuery));
83
+ for (const key in query) {
84
+ if (query[key] === '') {
85
+ // @ts-ignore
86
+ query[key] = true;
87
+ }
88
+ }
89
+ return query as RequestQuery;
90
+ }
91
+
92
+ /**
93
+ * posixify and remove root at start
94
+ *
95
+ * @param filename
96
+ * @param normalizedRoot
97
+ */
98
+ function normalize(filename: string, normalizedRoot: string) {
99
+ return stripRoot(normalizePath(filename), normalizedRoot);
100
+ }
101
+
102
+ function existsInRoot(filename: string, root: string) {
103
+ if (filename.startsWith(VITE_FS_PREFIX)) {
104
+ return false; // vite already tagged it as out of root
105
+ }
106
+ return fs.existsSync(root + filename);
107
+ }
108
+
109
+ function stripRoot(normalizedFilename: string, normalizedRoot: string) {
110
+ return normalizedFilename.startsWith(normalizedRoot + '/')
111
+ ? normalizedFilename.slice(normalizedRoot.length)
112
+ : normalizedFilename;
113
+ }
114
+
115
+ function buildFilter(
116
+ include: Arrayable<string> | undefined,
117
+ exclude: Arrayable<string> | undefined,
118
+ extensions: string[]
119
+ ): (filename: string) => boolean {
120
+ const rollupFilter = createFilter(include, exclude);
121
+ return (filename) => rollupFilter(filename) && extensions.some((ext) => filename.endsWith(ext));
122
+ }
123
+
124
+ export type IdParser = (id: string, ssr: boolean, timestamp?: number) => SvelteRequest | undefined;
125
+ export function buildIdParser(options: ResolvedOptions): IdParser {
126
+ const { include, exclude, extensions, root } = options;
127
+ const normalizedRoot = normalizePath(root);
128
+ const filter = buildFilter(include, exclude, extensions!);
129
+ return (id, ssr, timestamp = Date.now()) => {
130
+ const { filename, rawQuery } = splitId(id);
131
+ if (filter(filename)) {
132
+ return parseToSvelteRequest(id, filename, rawQuery, normalizedRoot, timestamp, ssr);
133
+ }
134
+ };
135
+ }
@@ -0,0 +1,108 @@
1
+ import { createRequire } from 'module';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import { pathToFileURL } from 'url';
5
+ import { log } from './log';
6
+ import { Options } from './options';
7
+ import { UserConfig } from 'vite';
8
+
9
+ // used to require cjs config in esm.
10
+ // NOTE dynamic import() cjs technically works, but timestamp query cache bust
11
+ // have no effect, likely because it has another internal cache?
12
+ let esmRequire: NodeRequire;
13
+
14
+ export const knownSvelteConfigNames = [
15
+ 'svelte.config.js',
16
+ 'svelte.config.cjs',
17
+ 'svelte.config.mjs'
18
+ ];
19
+
20
+ // hide dynamic import from ts transform to prevent it turning into a require
21
+ // see https://github.com/microsoft/TypeScript/issues/43329#issuecomment-811606238
22
+ // also use timestamp query to avoid caching on reload
23
+ const dynamicImportDefault = new Function(
24
+ 'path',
25
+ 'return import(path + "?t=" + Date.now()).then(m => m.default)'
26
+ );
27
+
28
+ export async function loadSvelteConfig(
29
+ viteConfig: UserConfig,
30
+ inlineOptions: Partial<Options>
31
+ ): Promise<Partial<Options> | undefined> {
32
+ const configFile = findConfigToLoad(viteConfig, inlineOptions);
33
+ if (configFile) {
34
+ let err;
35
+ // try to use dynamic import for svelte.config.js first
36
+ if (configFile.endsWith('.js') || configFile.endsWith('.mjs')) {
37
+ try {
38
+ const result = await dynamicImportDefault(pathToFileURL(configFile).href);
39
+ if (result != null) {
40
+ return {
41
+ ...result,
42
+ configFile
43
+ };
44
+ } else {
45
+ throw new Error(`invalid export in ${configFile}`);
46
+ }
47
+ } catch (e) {
48
+ log.error(`failed to import config ${configFile}`, e);
49
+ err = e;
50
+ }
51
+ }
52
+ // cjs or error with dynamic import
53
+ if (!configFile.endsWith('.mjs')) {
54
+ try {
55
+ // identify which require function to use (esm and cjs mode)
56
+ const _require = import.meta.url
57
+ ? (esmRequire ??= createRequire(import.meta.url))
58
+ : require;
59
+
60
+ // avoid loading cached version on reload
61
+ delete _require.cache[_require.resolve(configFile)];
62
+ const result = _require(configFile);
63
+ if (result != null) {
64
+ return {
65
+ ...result,
66
+ configFile
67
+ };
68
+ } else {
69
+ throw new Error(`invalid export in ${configFile}`);
70
+ }
71
+ } catch (e) {
72
+ log.error(`failed to require config ${configFile}`, e);
73
+ if (!err) {
74
+ err = e;
75
+ }
76
+ }
77
+ }
78
+ // failed to load existing config file
79
+ throw err;
80
+ }
81
+ }
82
+
83
+ function findConfigToLoad(viteConfig: UserConfig, inlineOptions: Partial<Options>) {
84
+ const root = viteConfig.root || process.cwd();
85
+ if (inlineOptions.configFile) {
86
+ const abolutePath = path.isAbsolute(inlineOptions.configFile)
87
+ ? inlineOptions.configFile
88
+ : path.resolve(root, inlineOptions.configFile);
89
+ if (!fs.existsSync(abolutePath)) {
90
+ throw new Error(`failed to find svelte config file ${abolutePath}.`);
91
+ }
92
+ return abolutePath;
93
+ } else {
94
+ const existingKnownConfigFiles = knownSvelteConfigNames
95
+ .map((candidate) => path.resolve(root, candidate))
96
+ .filter((file) => fs.existsSync(file));
97
+ if (existingKnownConfigFiles.length === 0) {
98
+ log.debug(`no svelte config found at ${root}`);
99
+ return;
100
+ } else if (existingKnownConfigFiles.length > 1) {
101
+ log.warn(
102
+ `found more than one svelte config file, using ${existingKnownConfigFiles[0]}. you should only have one!`,
103
+ existingKnownConfigFiles
104
+ );
105
+ }
106
+ return existingKnownConfigFiles[0];
107
+ }
108
+ }
@@ -0,0 +1,170 @@
1
+ /* eslint-disable no-unused-vars,no-console */
2
+ import { cyan, yellow, red } from 'kleur/colors';
3
+ import debug from 'debug';
4
+ import { ResolvedOptions, Warning } from './options';
5
+
6
+ const levels: string[] = ['debug', 'info', 'warn', 'error', 'silent'];
7
+ const prefix = 'vite-plugin-svelte';
8
+ const loggers: { [key: string]: any } = {
9
+ debug: {
10
+ log: debug(`vite:${prefix}`),
11
+ enabled: false,
12
+ isDebug: true
13
+ },
14
+ info: {
15
+ color: cyan,
16
+ log: console.log,
17
+ enabled: true
18
+ },
19
+ warn: {
20
+ color: yellow,
21
+ log: console.warn,
22
+ enabled: true
23
+ },
24
+ error: {
25
+ color: red,
26
+ log: console.error,
27
+ enabled: true
28
+ },
29
+ silent: {
30
+ enabled: false
31
+ }
32
+ };
33
+
34
+ let _level: string = 'info';
35
+ function setLevel(level: string) {
36
+ if (level === _level) {
37
+ return;
38
+ }
39
+ const levelIndex = levels.indexOf(level);
40
+ if (levelIndex > -1) {
41
+ _level = level;
42
+ for (let i = 0; i < levels.length; i++) {
43
+ loggers[levels[i]].enabled = i >= levelIndex;
44
+ }
45
+ } else {
46
+ _log(loggers.error, `invalid log level: ${level} `);
47
+ }
48
+ }
49
+
50
+ function _log(logger: any, message: string, payload?: any) {
51
+ if (!logger.enabled) {
52
+ return;
53
+ }
54
+ if (logger.isDebug) {
55
+ payload !== undefined ? logger.log(message, payload) : logger.log(message);
56
+ } else {
57
+ logger.log(logger.color(`${new Date().toLocaleTimeString()} [${prefix}] ${message}`));
58
+ if (payload) {
59
+ logger.log(payload);
60
+ }
61
+ }
62
+ }
63
+
64
+ export interface LogFn {
65
+ (message: string, payload?: any): void;
66
+ enabled: boolean;
67
+ once: (message: string, payload?: any) => void;
68
+ }
69
+
70
+ function createLogger(level: string): LogFn {
71
+ const logger = loggers[level];
72
+ const logFn: LogFn = _log.bind(null, logger) as LogFn;
73
+ const logged = new Set<String>();
74
+ const once = function (message: string, payload?: any) {
75
+ if (logged.has(message)) {
76
+ return;
77
+ }
78
+ logged.add(message);
79
+ logFn.apply(null, [message, payload]);
80
+ };
81
+ Object.defineProperty(logFn, 'enabled', {
82
+ get() {
83
+ return logger.enabled;
84
+ }
85
+ });
86
+ Object.defineProperty(logFn, 'once', {
87
+ get() {
88
+ return once;
89
+ }
90
+ });
91
+ return logFn;
92
+ }
93
+
94
+ export const log = {
95
+ debug: createLogger('debug'),
96
+ info: createLogger('info'),
97
+ warn: createLogger('warn'),
98
+ error: createLogger('error'),
99
+ setLevel
100
+ };
101
+
102
+ export function logCompilerWarnings(warnings: Warning[], options: ResolvedOptions) {
103
+ const { emitCss, onwarn, isBuild } = options;
104
+ const warn = isBuild ? warnBuild : warnDev;
105
+ const notIgnoredWarnings = warnings?.filter((w) => !ignoreCompilerWarning(w, isBuild, emitCss));
106
+ const extraWarnings = buildExtraWarnings(warnings, isBuild);
107
+ [...notIgnoredWarnings, ...extraWarnings].forEach((warning) => {
108
+ if (onwarn) {
109
+ onwarn(warning, warn);
110
+ } else {
111
+ warn(warning);
112
+ }
113
+ });
114
+ }
115
+
116
+ function ignoreCompilerWarning(
117
+ warning: Warning,
118
+ isBuild: boolean,
119
+ emitCss: boolean | undefined
120
+ ): boolean {
121
+ return (
122
+ (!emitCss && warning.code === 'css-unused-selector') || // same as rollup-plugin-svelte
123
+ (!isBuild && isNoScopableElementWarning(warning))
124
+ );
125
+ }
126
+
127
+ function isNoScopableElementWarning(warning: Warning) {
128
+ // see https://github.com/sveltejs/vite-plugin-svelte/issues/153
129
+ return warning.code === 'css-unused-selector' && warning.message.includes('"*"');
130
+ }
131
+
132
+ function buildExtraWarnings(warnings: Warning[], isBuild: boolean): Warning[] {
133
+ const extraWarnings = [];
134
+ if (!isBuild) {
135
+ const noScopableElementWarnings = warnings.filter((w) => isNoScopableElementWarning(w));
136
+ if (noScopableElementWarnings.length > 0) {
137
+ // in case there are multiple, use last one as that is the one caused by our *{} rule
138
+ const noScopableElementWarning =
139
+ noScopableElementWarnings[noScopableElementWarnings.length - 1];
140
+ extraWarnings.push({
141
+ ...noScopableElementWarning,
142
+ code: 'vite-plugin-svelte-css-no-scopable-elements',
143
+ message: `No scopable elements found in template. If you're using global styles in the style tag, you should move it into an external stylesheet file and import it in JS. See https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/faq.md#where-should-i-put-my-global-styles.`
144
+ });
145
+ }
146
+ }
147
+ return extraWarnings;
148
+ }
149
+
150
+ function warnDev(w: Warning) {
151
+ log.info.enabled && log.info(buildExtendedLogMessage(w));
152
+ }
153
+
154
+ function warnBuild(w: Warning) {
155
+ log.warn.enabled && log.warn(buildExtendedLogMessage(w), w.frame);
156
+ }
157
+
158
+ export function buildExtendedLogMessage(w: Warning) {
159
+ const parts = [];
160
+ if (w.filename) {
161
+ parts.push(w.filename);
162
+ }
163
+ if (w.start) {
164
+ parts.push(':', w.start.line, ':', w.start.column);
165
+ }
166
+ if (w.message) {
167
+ parts.push(' ', w.message);
168
+ }
169
+ return parts.join('');
170
+ }