@rsvelte/vite-plugin-svelte 0.1.0

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,274 @@
1
+ import { normalizePath } from 'vite';
2
+ import fs from 'node:fs';
3
+ import process from 'node:process';
4
+ import { log } from './log.js';
5
+ import {
6
+ DEFAULT_SVELTE_EXT,
7
+ DEFAULT_SVELTE_MODULE_EXT,
8
+ DEFAULT_SVELTE_MODULE_INFIX,
9
+ SVELTE_VIRTUAL_STYLE_ID_REGEX
10
+ } from './constants.js';
11
+ import { arraify } from './options.js';
12
+
13
+ const VITE_FS_PREFIX = '/@fs/';
14
+ const IS_WINDOWS = process.platform === 'win32';
15
+
16
+ const SUPPORTED_COMPILER_OPTIONS = ['generate', 'dev', 'css', 'customElement', 'immutable'];
17
+ const TYPES_WITH_COMPILER_OPTIONS = ['style', 'script', 'all'];
18
+
19
+ /**
20
+ * @param {string} id
21
+ * @returns {{ filename: string, rawQuery: string }}
22
+ */
23
+ function splitId(id) {
24
+ const parts = id.split('?', 2);
25
+ const filename = parts[0];
26
+ const rawQuery = parts[1];
27
+ return { filename, rawQuery };
28
+ }
29
+
30
+ /**
31
+ * @param {string} id
32
+ * @param {string} filename
33
+ * @param {string} rawQuery
34
+ * @param {string} root
35
+ * @param {number} timestamp
36
+ * @param {boolean} ssr
37
+ * @returns {import('../types/id.d.ts').SvelteRequest | undefined}
38
+ */
39
+ function parseToSvelteRequest(id, filename, rawQuery, root, timestamp, ssr) {
40
+ const query = parseRequestQuery(rawQuery);
41
+ const rawOrDirect = !!(query.raw || query.direct);
42
+ if (query.url || (!query.svelte && rawOrDirect)) {
43
+ // skip requests with special vite tags
44
+ return;
45
+ }
46
+ const raw = rawOrDirect;
47
+ const normalizedFilename = normalize(filename, root);
48
+ const cssId = createVirtualImportId(filename, root, 'style');
49
+
50
+ return {
51
+ id,
52
+ filename,
53
+ normalizedFilename,
54
+ cssId,
55
+ query,
56
+ timestamp,
57
+ ssr,
58
+ raw
59
+ };
60
+ }
61
+
62
+ /**
63
+ * @param {string} filename
64
+ * @param {string} root
65
+ * @param {import('../types/id.d.ts').SvelteQueryTypes} type
66
+ * @returns {string}
67
+ */
68
+ function createVirtualImportId(filename, root, type) {
69
+ const parts = ['svelte', `type=${type}`];
70
+ if (type === 'style') {
71
+ parts.push('lang.css');
72
+ }
73
+ if (existsInRoot(filename, root)) {
74
+ filename = root + filename;
75
+ } else if (filename.startsWith(VITE_FS_PREFIX)) {
76
+ filename = IS_WINDOWS
77
+ ? filename.slice(VITE_FS_PREFIX.length) // remove /@fs/ from /@fs/C:/...
78
+ : filename.slice(VITE_FS_PREFIX.length - 1); // remove /@fs from /@fs/home/user
79
+ }
80
+ // return same virtual id format as vite-plugin-vue eg ...App.svelte?svelte&type=style&lang.css
81
+ return `${filename}?${parts.join('&')}`;
82
+ }
83
+
84
+ /**
85
+ * @param {string} rawQuery
86
+ * @returns {import('../types/id.d.ts').RequestQuery}
87
+ */
88
+ function parseRequestQuery(rawQuery) {
89
+ const query = Object.fromEntries(new URLSearchParams(rawQuery));
90
+ for (const key in query) {
91
+ if (query[key] === '') {
92
+ // @ts-expect-error not boolean
93
+ query[key] = true;
94
+ }
95
+ }
96
+ const compilerOptions = query.compilerOptions;
97
+ if (compilerOptions) {
98
+ if (!((query.raw || query.direct) && TYPES_WITH_COMPILER_OPTIONS.includes(query.type))) {
99
+ throw new Error(
100
+ `Invalid compilerOptions in query ${rawQuery}. CompilerOptions are only supported for raw or direct queries with type in "${TYPES_WITH_COMPILER_OPTIONS.join(
101
+ ', '
102
+ )}" e.g. '?svelte&raw&type=script&compilerOptions={"generate":"server","dev":false}`
103
+ );
104
+ }
105
+ try {
106
+ const parsed = JSON.parse(compilerOptions);
107
+ const invalid = Object.keys(parsed).filter(
108
+ (key) => !SUPPORTED_COMPILER_OPTIONS.includes(key)
109
+ );
110
+ if (invalid.length) {
111
+ throw new Error(
112
+ `Invalid compilerOptions in query ${rawQuery}: ${invalid.join(
113
+ ', '
114
+ )}. Supported: ${SUPPORTED_COMPILER_OPTIONS.join(', ')}`
115
+ );
116
+ }
117
+ query.compilerOptions = parsed;
118
+ } catch (e) {
119
+ log.error('failed to parse request query compilerOptions', e);
120
+ throw e;
121
+ }
122
+ }
123
+
124
+ return /** @type {import('../types/id.d.ts').RequestQuery}*/ query;
125
+ }
126
+
127
+ /**
128
+ * posixify and remove root at start
129
+ *
130
+ * @param {string} filename
131
+ * @param {string} normalizedRoot
132
+ * @returns {string}
133
+ */
134
+ export function normalize(filename, normalizedRoot) {
135
+ return stripRoot(normalizePath(filename), normalizedRoot);
136
+ }
137
+
138
+ /**
139
+ * @param {string} filename
140
+ * @param {string} root
141
+ * @returns {boolean}
142
+ */
143
+ function existsInRoot(filename, root) {
144
+ if (filename.startsWith(VITE_FS_PREFIX)) {
145
+ return false; // vite already tagged it as out of root
146
+ }
147
+ return fs.existsSync(root + filename);
148
+ }
149
+
150
+ /**
151
+ * @param {string} normalizedFilename
152
+ * @param {string} normalizedRoot
153
+ * @returns {string}
154
+ */
155
+ function stripRoot(normalizedFilename, normalizedRoot) {
156
+ return normalizedFilename.startsWith(normalizedRoot + '/')
157
+ ? normalizedFilename.slice(normalizedRoot.length)
158
+ : normalizedFilename;
159
+ }
160
+
161
+ /**
162
+ *
163
+ * @param {string} s
164
+ * @returns {string}
165
+ */
166
+ function escapeRE(s) {
167
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
168
+ }
169
+
170
+ /**
171
+ * @param {import('../types/options.d.ts').ResolvedOptions} options
172
+ * @returns {import('../types/id.d.ts').IdFilter}
173
+ */
174
+ export function buildIdFilter(options) {
175
+ const { include = [], exclude = [], extensions = DEFAULT_SVELTE_EXT } = options;
176
+ // this regex combines configured extensions and looks for them at the end of the string or directly before first ? or #
177
+ const extensionsRE = new RegExp(
178
+ `^[^?#]+\\.(?:${extensions
179
+ .map((e) => (e.startsWith('.') ? e.slice(1) : e))
180
+ .map(escapeRE)
181
+ .join('|')})(?:[?#]|$)`
182
+ );
183
+ return {
184
+ id: {
185
+ include: [extensionsRE, .../**@type {Array<string|RegExp>}*/ arraify(include)],
186
+ exclude: /**@type {Array<string|RegExp>}*/ [
187
+ '\0',
188
+ SVELTE_VIRTUAL_STYLE_ID_REGEX, // exclude from regular pipeline, we load it in a separate plugin
189
+ ...arraify(exclude)
190
+ ]
191
+ }
192
+ };
193
+ }
194
+
195
+ /**
196
+ * @param {import('../types/options.d.ts').ResolvedOptions} options
197
+ * @returns {import('../types/id.d.ts').IdParser}
198
+ */
199
+ export function buildIdParser(options) {
200
+ const normalizedRoot = normalizePath(options.root);
201
+ return (id, ssr, timestamp = Date.now()) => {
202
+ const { filename, rawQuery } = splitId(id);
203
+ return parseToSvelteRequest(id, filename, rawQuery, normalizedRoot, timestamp, ssr);
204
+ };
205
+ }
206
+
207
+ /**
208
+ * @param {import('../types/options.d.ts').ResolvedOptions} options
209
+ * @returns {import('../types/id.d.ts').IdFilter}
210
+ */
211
+ export function buildModuleIdFilter(options) {
212
+ const {
213
+ infixes = DEFAULT_SVELTE_MODULE_INFIX,
214
+ include = [],
215
+ exclude = [],
216
+ extensions = DEFAULT_SVELTE_MODULE_EXT
217
+ } = options.experimental?.compileModule ?? {};
218
+ // this regex combines configured infixes and extensions
219
+ // and looks for them at the end of the string or directly before first ? or #
220
+ const infixWithExtRE = new RegExp(
221
+ `^[^?#]+(?:${infixes.map(escapeRE).join('|')})(?:[^.\\\\/]+\\.)*(?:${extensions
222
+ .map((e) => (e.startsWith('.') ? e.slice(1) : e))
223
+ .map(escapeRE)
224
+ .join('|')})(?:[?#]|$)`
225
+ );
226
+ return {
227
+ id: {
228
+ include: [infixWithExtRE, .../**@type {Array<string|RegExp>}*/ arraify(include)],
229
+ exclude: ['\0', .../**@type {Array<string|RegExp>}*/ arraify(exclude)]
230
+ }
231
+ };
232
+ }
233
+
234
+ /**
235
+ * @param {import('../types/options.d.ts').ResolvedOptions} options
236
+ * @returns {import('../types/id.d.ts').ModuleIdParser}
237
+ */
238
+ export function buildModuleIdParser(options) {
239
+ const root = options.root;
240
+ const normalizedRoot = normalizePath(root);
241
+ return (id, ssr, timestamp = Date.now()) => {
242
+ const { filename, rawQuery } = splitId(id);
243
+ return parseToSvelteModuleRequest(id, filename, rawQuery, normalizedRoot, timestamp, ssr);
244
+ };
245
+ }
246
+
247
+ /**
248
+ * @param {string} id
249
+ * @param {string} filename
250
+ * @param {string} rawQuery
251
+ * @param {string} root
252
+ * @param {number} timestamp
253
+ * @param {boolean} ssr
254
+ * @returns {import('../types/id.d.ts').SvelteModuleRequest | undefined}
255
+ */
256
+ function parseToSvelteModuleRequest(id, filename, rawQuery, root, timestamp, ssr) {
257
+ const query = parseRequestQuery(rawQuery);
258
+
259
+ if (query.url || query.raw || query.direct) {
260
+ // skip requests with special vite tags
261
+ return;
262
+ }
263
+
264
+ const normalizedFilename = normalize(filename, root);
265
+
266
+ return {
267
+ id,
268
+ filename,
269
+ normalizedFilename,
270
+ query,
271
+ timestamp,
272
+ ssr
273
+ };
274
+ }
@@ -0,0 +1,80 @@
1
+ import path from 'node:path';
2
+ import process from 'node:process';
3
+ import fs from 'node:fs';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { log } from './log.js';
6
+
7
+ export const knownSvelteConfigNames = ['js', 'ts', 'mjs', 'mts'].map(
8
+ (ext) => `svelte.config.${ext}`
9
+ );
10
+
11
+ /**
12
+ * @param {string} filePath
13
+ * @param {number} timestamp
14
+ */
15
+ async function dynamicImportDefault(filePath, timestamp) {
16
+ return await import(filePath + '?t=' + timestamp).then((m) => m.default);
17
+ }
18
+
19
+ /**
20
+ * @param {import('vite').UserConfig} [viteConfig]
21
+ * @param {Partial<import('../public.d.ts').Options>} [inlineOptions]
22
+ * @returns {Promise<Partial<import('../public.d.ts').SvelteConfig> | undefined>}
23
+ */
24
+ export async function loadSvelteConfig(viteConfig, inlineOptions) {
25
+ if (inlineOptions?.configFile === false) {
26
+ return;
27
+ }
28
+ const configFile = findConfigToLoad(viteConfig, inlineOptions);
29
+ if (configFile) {
30
+ try {
31
+ const result = await dynamicImportDefault(
32
+ pathToFileURL(configFile).href,
33
+ fs.statSync(configFile).mtimeMs
34
+ );
35
+ if (result != null) {
36
+ return {
37
+ ...result,
38
+ configFile
39
+ };
40
+ } else {
41
+ throw new Error(`invalid export in ${configFile}`);
42
+ }
43
+ } catch (e) {
44
+ log.error(`failed to import config ${configFile}`, e);
45
+ throw e;
46
+ }
47
+ }
48
+ }
49
+
50
+ /**
51
+ * @param {import('vite').UserConfig | undefined} viteConfig
52
+ * @param {Partial<import('../public.d.ts').Options> | undefined} inlineOptions
53
+ * @returns {string | undefined}
54
+ */
55
+ function findConfigToLoad(viteConfig, inlineOptions) {
56
+ const root = viteConfig?.root || process.cwd();
57
+ if (inlineOptions?.configFile) {
58
+ const abolutePath = path.isAbsolute(inlineOptions.configFile)
59
+ ? inlineOptions.configFile
60
+ : path.resolve(root, inlineOptions.configFile);
61
+ if (!fs.existsSync(abolutePath)) {
62
+ throw new Error(`failed to find svelte config file ${abolutePath}.`);
63
+ }
64
+ return abolutePath;
65
+ } else {
66
+ const existingKnownConfigFiles = knownSvelteConfigNames
67
+ .map((candidate) => path.resolve(root, candidate))
68
+ .filter((file) => fs.existsSync(file));
69
+ if (existingKnownConfigFiles.length === 0) {
70
+ log.info(`no Svelte config found at ${root} - using default configuration.`);
71
+ return;
72
+ } else if (existingKnownConfigFiles.length > 1) {
73
+ log.warn(
74
+ `found more than one svelte config file, using ${existingKnownConfigFiles[0]}. you should only have one!`,
75
+ existingKnownConfigFiles
76
+ );
77
+ }
78
+ return existingKnownConfigFiles[0];
79
+ }
80
+ }
@@ -0,0 +1,256 @@
1
+ /* eslint-disable no-console */
2
+
3
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
4
+ import { styleText } from 'node:util';
5
+ const cyan = (/** @type {string} */ txt) => styleText('cyan', txt);
6
+ const yellow = (/** @type {string} */ txt) => styleText('yellow', txt);
7
+ const red = (/** @type {string} */ txt) => styleText('red', txt);
8
+
9
+ import { createDebug, enabled } from 'obug';
10
+
11
+ /** @type {import('../types/log.d.ts').LogLevel[]} */
12
+ const levels = ['debug', 'info', 'warn', 'error', 'silent'];
13
+ const prefix = 'vite-plugin-svelte';
14
+ /** @type {Record<import('../types/log.d.ts').LogLevel, any>} */
15
+ const loggers = {
16
+ debug: {
17
+ log: createDebug(`${prefix}`),
18
+ enabled: false,
19
+ isDebug: true
20
+ },
21
+ info: {
22
+ color: cyan,
23
+ log: console.log,
24
+ enabled: true
25
+ },
26
+ warn: {
27
+ color: yellow,
28
+ log: console.warn,
29
+ enabled: true
30
+ },
31
+ error: {
32
+ color: red,
33
+ log: console.error,
34
+ enabled: true
35
+ },
36
+ silent: {
37
+ enabled: false
38
+ }
39
+ };
40
+
41
+ /** @type {import('../types/log.d.ts').LogLevel} */
42
+ let _level = 'info';
43
+ /**
44
+ * @param {import('../types/log.d.ts').LogLevel} level
45
+ * @returns {void}
46
+ */
47
+ function setLevel(level) {
48
+ if (level === _level) {
49
+ return;
50
+ }
51
+ const levelIndex = levels.indexOf(level);
52
+ if (levelIndex > -1) {
53
+ _level = level;
54
+ for (let i = 0; i < levels.length; i++) {
55
+ loggers[levels[i]].enabled = i >= levelIndex;
56
+ }
57
+ } else {
58
+ _log(loggers.error, `invalid log level: ${level} `);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * @param {any} logger
64
+ * @param {string} message
65
+ * @param {any} [payload]
66
+ * @param {string} [namespace]
67
+ * @returns
68
+ */
69
+ function _log(logger, message, payload, namespace) {
70
+ if (!logger.enabled) {
71
+ return;
72
+ }
73
+ if (logger.isDebug) {
74
+ let log = logger.log;
75
+ if (namespace) {
76
+ if (!isDebugNamespaceEnabled(namespace)) {
77
+ return;
78
+ }
79
+ log = logger.log.extend(namespace);
80
+ }
81
+ if (payload !== undefined) {
82
+ log(message, payload);
83
+ } else {
84
+ log(message);
85
+ }
86
+ } else {
87
+ logger.log(
88
+ logger.color(
89
+ `${new Date().toLocaleTimeString()} [${prefix}${
90
+ namespace ? `:${namespace}` : ''
91
+ }] ${message}`
92
+ )
93
+ );
94
+ if (payload) {
95
+ logger.log(payload);
96
+ }
97
+ }
98
+ }
99
+
100
+ /**
101
+ * @param {import('../types/log.d.ts').LogLevel} level
102
+ * @returns {import('../types/log.d.ts').LogFn}
103
+ */
104
+ function createLogger(level) {
105
+ const logger = loggers[level];
106
+ const logFn = /** @type {import('../types/log.d.ts').LogFn} */ (_log.bind(null, logger));
107
+ /** @type {Set<string>} */
108
+ const logged = new Set();
109
+ /** @type {import('../types/log.d.ts').SimpleLogFn} */
110
+ const once = function (message, payload, namespace) {
111
+ if (!logger.enabled || logged.has(message)) {
112
+ return;
113
+ }
114
+ logged.add(message);
115
+ logFn.apply(null, [message, payload, namespace]);
116
+ };
117
+ Object.defineProperty(logFn, 'enabled', {
118
+ get() {
119
+ return logger.enabled;
120
+ }
121
+ });
122
+ Object.defineProperty(logFn, 'once', {
123
+ get() {
124
+ return once;
125
+ }
126
+ });
127
+ return logFn;
128
+ }
129
+
130
+ export const log = {
131
+ debug: createLogger('debug'),
132
+ info: createLogger('info'),
133
+ warn: createLogger('warn'),
134
+ error: createLogger('error'),
135
+ setLevel
136
+ };
137
+
138
+ /**
139
+ * @param {import('../types/id.d.ts').SvelteRequest | import('../types/id.d.ts').SvelteModuleRequest} svelteRequest
140
+ * @param {import('@rsvelte/compiler').Warning[]} warnings
141
+ * @param {import('../types/options.d.ts').ResolvedOptions} options
142
+ */
143
+ export function logCompilerWarnings(svelteRequest, warnings, options) {
144
+ const { emitCss, onwarn, isBuild } = options;
145
+ const sendViaWS = !isBuild && options.experimental?.sendWarningsToBrowser;
146
+ let warn = isBuild ? warnBuild : warnDev;
147
+ /** @type {import('@rsvelte/compiler').Warning[]} */
148
+ const handledByDefaultWarn = [];
149
+ const allWarnings = warnings?.filter((w) => !ignoreCompilerWarning(w, isBuild, emitCss));
150
+ if (sendViaWS) {
151
+ const _warn = warn;
152
+ /** @type {(w: import('@rsvelte/compiler').Warning) => void} */
153
+ warn = (w) => {
154
+ handledByDefaultWarn.push(w);
155
+ _warn(w);
156
+ };
157
+ }
158
+ allWarnings.forEach((warning) => {
159
+ if (onwarn) {
160
+ onwarn(warning, warn);
161
+ } else {
162
+ warn(warning);
163
+ }
164
+ });
165
+ if (sendViaWS) {
166
+ /** @type {import('../types/log.d.ts').SvelteWarningsMessage} */
167
+ const message = {
168
+ id: svelteRequest.id,
169
+ filename: svelteRequest.filename,
170
+ normalizedFilename: svelteRequest.normalizedFilename,
171
+ timestamp: svelteRequest.timestamp,
172
+ warnings: handledByDefaultWarn, // allWarnings filtered by warnings where onwarn did not call the default handler
173
+ allWarnings, // includes warnings filtered by onwarn and our extra vite plugin svelte warnings
174
+ rawWarnings: warnings // raw compiler output
175
+ };
176
+ log.debug(`sending svelte:warnings message for ${svelteRequest.normalizedFilename}`);
177
+ options.server?.ws?.send('svelte:warnings', message);
178
+ }
179
+ }
180
+
181
+ /**
182
+ * @param {import('@rsvelte/compiler').Warning} warning
183
+ * @param {boolean} isBuild
184
+ * @param {boolean} [emitCss]
185
+ * @returns {boolean}
186
+ */
187
+ function ignoreCompilerWarning(warning, isBuild, emitCss) {
188
+ return (
189
+ (!emitCss && warning.code === 'css_unused_selector') || // same as rollup-plugin-svelte
190
+ (!isBuild && isNoScopableElementWarning(warning))
191
+ );
192
+ }
193
+
194
+ /**
195
+ *
196
+ * @param {import('@rsvelte/compiler').Warning} warning
197
+ * @returns {boolean}
198
+ */
199
+ function isNoScopableElementWarning(warning) {
200
+ // see https://github.com/sveltejs/vite-plugin-svelte/issues/153
201
+ return warning.code === 'css_unused_selector' && warning.message.includes('"*"');
202
+ }
203
+
204
+ /**
205
+ * @param {import('@rsvelte/compiler').Warning} w
206
+ */
207
+ function warnDev(w) {
208
+ if (w.filename?.includes('node_modules')) {
209
+ if (isDebugNamespaceEnabled('node-modules-onwarn')) {
210
+ log.debug(buildExtendedLogMessage(w), undefined, 'node-modules-onwarn');
211
+ }
212
+ } else if (log.info.enabled) {
213
+ log.info(buildExtendedLogMessage(w));
214
+ }
215
+ }
216
+
217
+ /**
218
+ * @param {import('@rsvelte/compiler').Warning & {frame?: string}} w
219
+ */
220
+ function warnBuild(w) {
221
+ if (w.filename?.includes('node_modules')) {
222
+ if (isDebugNamespaceEnabled('node-modules-onwarn')) {
223
+ log.debug(buildExtendedLogMessage(w), w.frame, 'node-modules-onwarn');
224
+ }
225
+ } else if (log.warn.enabled) {
226
+ log.warn(buildExtendedLogMessage(w), w.frame);
227
+ }
228
+ }
229
+
230
+ /**
231
+ * @param {import('@rsvelte/compiler').Warning} w
232
+ */
233
+ export function buildExtendedLogMessage(w) {
234
+ const parts = [];
235
+ if (w.filename) {
236
+ parts.push(w.filename);
237
+ }
238
+ if (w.start) {
239
+ parts.push(':', w.start.line, ':', w.start.column);
240
+ }
241
+ if (w.message) {
242
+ if (parts.length > 0) {
243
+ parts.push(' ');
244
+ }
245
+ parts.push(w.message);
246
+ }
247
+ return parts.join('');
248
+ }
249
+
250
+ /**
251
+ * @param {string} namespace
252
+ * @returns {boolean}
253
+ */
254
+ export function isDebugNamespaceEnabled(namespace) {
255
+ return enabled(`${prefix}:${namespace}`);
256
+ }