Skip to content

Commit 04904c2

Browse files
committed
fix: load raw&svelte files ourselves to prevent vite asset middleware from turning them into a default export, reorder plugins
1 parent fd8c291 commit 04904c2

File tree

6 files changed

+182
-174
lines changed

6 files changed

+182
-174
lines changed

packages/vite-plugin-svelte/src/index.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,15 @@ export function svelte(inlineOptions) {
2525
// @ts-expect-error initialize empty to guard against early use
2626
const api = {}; // initialized by configure plugin, used in others
2727
return [
28+
{ name: 'vite-plugin-svelte' }, // marker for detection logic in other plugins that expect this name
2829
configure(api, inlineOptions),
2930
setupOptimizer(api),
30-
preprocess(api),
31-
compile(api),
32-
hotUpdate(api),
3331
loadCompiledCss(api),
3432
loadCustom(api),
33+
preprocess(api),
34+
compile(api),
3535
compileModule(api),
36+
hotUpdate(api),
3637
svelteInspector()
3738
];
3839
}

packages/vite-plugin-svelte/src/plugins/compile-module.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function compileModule(api) {
1818
let idParser;
1919
/** @type {import('vite').Plugin} */
2020
const plugin = {
21-
name: 'vite-plugin-svelte-module',
21+
name: 'vite-plugin-svelte:compile-module',
2222
enforce: 'post',
2323
async configResolved() {
2424
options = api.options;

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

Lines changed: 123 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { toRollupError } from '../utils/error.js';
2-
import { logCompilerWarnings } from '../utils/log.js';
2+
import { log, logCompilerWarnings } from '../utils/log.js';
3+
import fs from 'node:fs';
34
import { ensureWatchedFile } from '../utils/watch.js';
45

56
/**
@@ -27,21 +28,90 @@ export function compile(api) {
2728
},
2829
transform: {
2930
async handler(code, id) {
30-
const svelteMeta = this.getModuleInfo(id)?.meta?.svelte;
31-
const cache = api.getEnvironmentCache(this);
3231
const ssr = this.environment.config.consumer === 'server';
3332
const svelteRequest = api.idParser(id, ssr);
34-
if (!svelteRequest || svelteRequest.query.type === 'style' || svelteRequest.raw) {
33+
if (!svelteRequest) {
3534
return;
3635
}
36+
const cache = api.getEnvironmentCache(this);
3737
let compileData;
3838
try {
39-
compileData = await compileSvelte(svelteRequest, code, options, svelteMeta?.preprocessed);
39+
/**
40+
* @type {import("../types/options.js").ResolvedOptions}
41+
*/
42+
const finalOptions = svelteRequest.raw
43+
? {
44+
...options,
45+
// don't use dynamic vite-plugin-svelte defaults here to ensure stable result between ssr,dev and build
46+
compilerOptions: {
47+
dev: false,
48+
css: 'external',
49+
hmr: false,
50+
...svelteRequest.query.compilerOptions
51+
},
52+
emitCss: true
53+
}
54+
: options;
55+
const svelteMeta = this.getModuleInfo(id)?.meta?.svelte;
56+
compileData = await compileSvelte(
57+
svelteRequest,
58+
code,
59+
finalOptions,
60+
svelteMeta?.preprocessed
61+
);
4062
} catch (e) {
4163
cache.setError(svelteRequest, e);
4264
throw toRollupError(e, options);
4365
}
44-
logCompilerWarnings(svelteRequest, compileData.compiled.warnings, options);
66+
if (compileData.compiled?.warnings) {
67+
logCompilerWarnings(svelteRequest, compileData.compiled.warnings, options);
68+
}
69+
if (svelteRequest.raw) {
70+
const query = svelteRequest.query;
71+
let result;
72+
if (query.type === 'style') {
73+
result = compileData.compiled.css ?? { code: '', map: null };
74+
} else if (query.type === 'script') {
75+
result = compileData.compiled.js;
76+
} else if (query.type === 'preprocessed') {
77+
result = compileData.preprocessed;
78+
} else if (query.type === 'all' && query.raw) {
79+
return allToRawExports(compileData, fs.readFileSync(compileData.filename, 'utf-8'));
80+
} else {
81+
throw new Error(
82+
`invalid "type=${query.type}" in ${id}. supported are script, style, preprocessed, all`
83+
);
84+
}
85+
if (query.direct) {
86+
const supportedDirectTypes = ['script', 'style'];
87+
if (!supportedDirectTypes.includes(query.type)) {
88+
throw new Error(
89+
`invalid "type=${
90+
query.type
91+
}" combined with direct in ${id}. supported are: ${supportedDirectTypes.join(', ')}`
92+
);
93+
}
94+
log.debug(`load returns direct result for ${id}`, undefined, 'load');
95+
let directOutput = result.code;
96+
// @ts-expect-error might not be SourceMap but toUrl check should suffice
97+
if (query.sourcemap && result.map?.toUrl) {
98+
// @ts-expect-error toUrl might not exist
99+
const map = `sourceMappingURL=${result.map.toUrl()}`;
100+
if (query.type === 'style') {
101+
directOutput += `\n\n/*# ${map} */\n`;
102+
} else if (query.type === 'script') {
103+
directOutput += `\n\n//# ${map}\n`;
104+
}
105+
}
106+
return directOutput;
107+
} else if (query.raw) {
108+
log.debug(`load returns raw result for ${id}`, undefined, 'load');
109+
return toRawExports(result);
110+
} else {
111+
throw new Error(`invalid raw mode in ${id}, supported are raw, direct`);
112+
}
113+
}
114+
45115
cache.update(compileData);
46116
if (compileData.dependencies?.length) {
47117
if (options.server) {
@@ -68,3 +138,50 @@ export function compile(api) {
68138
};
69139
return plugin;
70140
}
141+
142+
/**
143+
* turn compileData and source into a flat list of raw exports
144+
*
145+
* @param {import('../types/compile.d.ts').CompileData} compileData
146+
* @param {string} source
147+
*/
148+
function allToRawExports(compileData, source) {
149+
// flatten CompileData
150+
/** @type {Partial<import('../types/compile.d.ts').CompileData & { source: string }>} */
151+
const exports = {
152+
...compileData,
153+
...compileData.compiled,
154+
source
155+
};
156+
delete exports.compiled;
157+
delete exports.filename; // absolute path, remove to avoid it in output
158+
return toRawExports(exports);
159+
}
160+
161+
/**
162+
* turn object into raw exports.
163+
*
164+
* every prop is returned as a const export, and if prop 'code' exists it is additionally added as default export
165+
*
166+
* eg {'foo':'bar','code':'baz'} results in
167+
*
168+
* ```js
169+
* export const code='baz'
170+
* export const foo='bar'
171+
* export default code
172+
* ```
173+
* @param {object} object
174+
* @returns {string}
175+
*/
176+
function toRawExports(object) {
177+
let exports =
178+
Object.entries(object)
179+
.filter(([_key, value]) => typeof value !== 'function') // preprocess output has a toString function that's enumerable
180+
.sort(([a], [b]) => (a < b ? -1 : a === b ? 0 : 1))
181+
.map(([key, value]) => `export const ${key}=${JSON.stringify(value)}`)
182+
.join('\n') + '\n';
183+
if (Object.prototype.hasOwnProperty.call(object, 'code')) {
184+
exports += 'export default code\n';
185+
}
186+
return exports;
187+
}
Lines changed: 11 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import fs from 'node:fs';
2-
import { toRollupError } from '../utils/error.js';
32
import { log } from '../utils/log.js';
43

54
/**
5+
* if svelte config includes files that vite treats as assets (e.g. .svg)
6+
* we have to manually load them to avoid getting urls
7+
*
68
* @param {import('../types/plugin-api.d.ts').PluginAPI} api
79
* @returns {import('vite').Plugin}
810
*/
@@ -23,148 +25,18 @@ export function loadCustom(api) {
2325
const ssr = config.consumer === 'server';
2426
const svelteRequest = api.idParser(id, ssr);
2527
if (svelteRequest) {
26-
const { filename, raw } = svelteRequest;
27-
if (raw) {
28-
const code = await compileRaw(svelteRequest, api.compileSvelte, api.options);
29-
// prevent vite from injecting sourcemaps in the results.
30-
return {
31-
code,
32-
map: {
33-
mappings: ''
34-
}
35-
};
36-
} else {
37-
// prevent vite asset plugin from loading files as url that should be compiled in transform
38-
if (config.assetsInclude(filename)) {
39-
log.debug(`load returns raw content for ${filename}`, undefined, 'load');
40-
return fs.readFileSync(filename, 'utf-8');
41-
}
28+
const { filename, raw, query } = svelteRequest;
29+
if (!query.url && (raw || config.assetsInclude(filename))) {
30+
log.debug(
31+
`loading ${filename} to prevent vite asset handling to turn it into a url by default`,
32+
undefined,
33+
'load'
34+
);
35+
return fs.readFileSync(filename, 'utf-8');
4236
}
4337
}
4438
}
4539
}
4640
};
4741
return plugin;
4842
}
49-
50-
/**
51-
* utility function to compile ?raw and ?direct requests in load hook
52-
*
53-
* @param {import('../types/id.d.ts').SvelteRequest} svelteRequest
54-
* @param {import('../types/compile.d.ts').CompileSvelte} compileSvelte
55-
* @param {import('../types/options.d.ts').ResolvedOptions} options
56-
* @returns {Promise<string>}
57-
*/
58-
async function compileRaw(svelteRequest, compileSvelte, options) {
59-
const { id, filename, query } = svelteRequest;
60-
61-
// raw svelte subrequest, compile on the fly and return requested subpart
62-
let compileData;
63-
const source = fs.readFileSync(filename, 'utf-8');
64-
try {
65-
//avoid compileSvelte doing extra ssr stuff unless requested
66-
svelteRequest.ssr = query.compilerOptions?.generate === 'server';
67-
compileData = await compileSvelte(svelteRequest, source, {
68-
...options,
69-
// don't use dynamic vite-plugin-svelte defaults here to ensure stable result between ssr,dev and build
70-
compilerOptions: {
71-
dev: false,
72-
css: 'external',
73-
hmr: false,
74-
...svelteRequest.query.compilerOptions
75-
},
76-
emitCss: true
77-
});
78-
} catch (e) {
79-
throw toRollupError(e, options);
80-
}
81-
let result;
82-
if (query.type === 'style') {
83-
result = compileData.compiled.css ?? { code: '', map: null };
84-
} else if (query.type === 'script') {
85-
result = compileData.compiled.js;
86-
} else if (query.type === 'preprocessed') {
87-
result = compileData.preprocessed;
88-
} else if (query.type === 'all' && query.raw) {
89-
return allToRawExports(compileData, source);
90-
} else {
91-
throw new Error(
92-
`invalid "type=${query.type}" in ${id}. supported are script, style, preprocessed, all`
93-
);
94-
}
95-
if (query.direct) {
96-
const supportedDirectTypes = ['script', 'style'];
97-
if (!supportedDirectTypes.includes(query.type)) {
98-
throw new Error(
99-
`invalid "type=${
100-
query.type
101-
}" combined with direct in ${id}. supported are: ${supportedDirectTypes.join(', ')}`
102-
);
103-
}
104-
log.debug(`load returns direct result for ${id}`, undefined, 'load');
105-
let directOutput = result.code;
106-
// @ts-expect-error might not be SourceMap but toUrl check should suffice
107-
if (query.sourcemap && result.map?.toUrl) {
108-
// @ts-expect-error toUrl might not exist
109-
const map = `sourceMappingURL=${result.map.toUrl()}`;
110-
if (query.type === 'style') {
111-
directOutput += `\n\n/*# ${map} */\n`;
112-
} else if (query.type === 'script') {
113-
directOutput += `\n\n//# ${map}\n`;
114-
}
115-
}
116-
return directOutput;
117-
} else if (query.raw) {
118-
log.debug(`load returns raw result for ${id}`, undefined, 'load');
119-
return toRawExports(result);
120-
} else {
121-
throw new Error(`invalid raw mode in ${id}, supported are raw, direct`);
122-
}
123-
}
124-
125-
/**
126-
* turn compileData and source into a flat list of raw exports
127-
*
128-
* @param {import('../types/compile.d.ts').CompileData} compileData
129-
* @param {string} source
130-
*/
131-
function allToRawExports(compileData, source) {
132-
// flatten CompileData
133-
/** @type {Partial<import('../types/compile.d.ts').CompileData & { source: string }>} */
134-
const exports = {
135-
...compileData,
136-
...compileData.compiled,
137-
source
138-
};
139-
delete exports.compiled;
140-
delete exports.filename; // absolute path, remove to avoid it in output
141-
return toRawExports(exports);
142-
}
143-
144-
/**
145-
* turn object into raw exports.
146-
*
147-
* every prop is returned as a const export, and if prop 'code' exists it is additionally added as default export
148-
*
149-
* eg {'foo':'bar','code':'baz'} results in
150-
*
151-
* ```js
152-
* export const code='baz'
153-
* export const foo='bar'
154-
* export default code
155-
* ```
156-
* @param {object} object
157-
* @returns {string}
158-
*/
159-
function toRawExports(object) {
160-
let exports =
161-
Object.entries(object)
162-
.filter(([_key, value]) => typeof value !== 'function') // preprocess output has a toString function that's enumerable
163-
.sort(([a], [b]) => (a < b ? -1 : a === b ? 0 : 1))
164-
.map(([key, value]) => `export const ${key}=${JSON.stringify(value)}`)
165-
.join('\n') + '\n';
166-
if (Object.prototype.hasOwnProperty.call(object, 'code')) {
167-
exports += 'export default code\n';
168-
}
169-
return exports;
170-
}

0 commit comments

Comments
 (0)