fix: preserve absolute file URLs in sourcemaps

Preserve file URLs across JS and CSS sourcemaps, and resolve relative preprocessor sources against file URL component filenames.

Fixes #5793

Implementation and tests prepared with OpenAI Codex.
pull/18854/head
Kunpeng Xie 1 week ago committed by GitHub
parent 636eaaaa6f
commit 6506f86555
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: preserve absolute file URLs in JavaScript and CSS sourcemaps

@ -4,7 +4,7 @@
import MagicString from 'magic-string';
import { walk } from 'zimmerframe';
import { is_keyframes_node, regex_css_name_boundary, remove_css_prefix } from '../../css.js';
import { merge_with_preprocessor_map } from '../../../utils/mapped_code.js';
import { get_source_name, merge_with_preprocessor_map } from '../../../utils/mapped_code.js';
import { dev } from '../../../state.js';
/**
@ -63,6 +63,8 @@ export function render_stylesheet(source, analysis, options) {
hasGlobal: analysis.css.has_global
};
// MagicString treats absolute URLs as filesystem paths.
css.map.sources[0] = get_source_name(options.filename, options.cssOutputFilename, 'input.svelte');
merge_with_preprocessor_map(css, options, css.map.sources[0]);
if (dev && analysis.inject_styles && css.code) {

@ -429,7 +429,10 @@ export function merge_with_preprocessor_map(result, options, source_name) {
// relative to the input file again in case the output code is in a different directory.
if (file_basename !== source_name) {
result.map.sources = result.map.sources.map(
/** @param {string} source */ (source) => get_relative_path(source_name, source)
/** @param {string} source */ (source) =>
source_name.startsWith('file://')
? new URL(source, source_name).href
: get_relative_path(source_name, source)
);
}
}
@ -440,6 +443,8 @@ export function merge_with_preprocessor_map(result, options, source_name) {
* @param {string} to
*/
function get_relative_path(from, to) {
if (to.startsWith('file://')) return to;
// Don't use node's utils here to ensure the compiler is usable in a browser environment
const from_parts = from.split(/[/\\]/);
const to_parts = to.split(/[/\\]/);
@ -469,5 +474,7 @@ export function get_basename(filename) {
* @param {string} fallback
*/
export function get_source_name(filename, output_filename, fallback) {
if (filename.startsWith('file://')) return filename;
return output_filename ? get_relative_path(output_filename, filename) : get_basename(filename);
}

@ -0,0 +1,90 @@
import { assert, describe, it } from 'vitest';
import { compile, preprocess } from 'svelte/compiler';
import MagicString, { Bundle } from 'magic-string';
const source = `<script>console.log('component');</script>
<h1>Hello world!</h1>
<style>h1 { color: red; }</style>`;
describe.each(['client', 'server'] as const)('%s sourcemap file URLs', (generate) => {
it.each([
'file:///project/src/input.svelte',
'file:///project/with%20spaces/input.svelte',
'file:///C:/project/src/input.svelte',
'file://server/share/input.svelte'
])('preserves the absolute filename %s', (filename) => {
for (const output_filename of [undefined, '/build/output.js', 'file:///build/output.js']) {
const result = compile(source, {
filename,
outputFilename: output_filename,
cssOutputFilename: output_filename?.replace('.js', '.css'),
generate
});
assert.deepEqual(result.js.map.sources, [filename]);
assert.deepEqual(result.css?.map.sources, [filename]);
assert.deepEqual(result.js.map.sourcesContent, [source]);
assert.deepEqual(result.css?.map.sourcesContent, [source]);
}
});
it('resolves relative preprocessor sources against the component URL', async () => {
const filename = 'file:///project/src/input.svelte';
const processed = await preprocess(
source,
{
script: ({ content }) => {
const bundle = new Bundle({ separator: '\n' });
for (const [filename, code] of [
['input.svelte', content],
['../shared.js', "console.log('relative');"],
['file:///external/helper.js', "console.log('absolute');"]
]) {
bundle.addSource({ filename, content: new MagicString(code) });
}
return { code: bundle.toString(), map: bundle.generateMap({ hires: true }) };
}
},
{ filename }
);
const result = compile(processed.code, {
filename,
outputFilename: '/build/output.js',
cssOutputFilename: '/build/output.css',
sourcemap: processed.map,
generate
});
assert.deepEqual(result.js.map.sources.toSorted(), [
'file:///external/helper.js',
'file:///project/shared.js',
filename
]);
assert.deepEqual(result.css?.map.sources, [filename]);
});
it('preserves preprocessor file URLs when the component uses a filesystem path', async () => {
const filename = '/project/input.svelte';
const original = 'file:///original/input.svelte';
const processed = await preprocess(
source,
{
markup: ({ content }) => ({
code: content,
map: new MagicString(content).generateMap({ source: original, hires: true })
})
},
{ filename }
);
const result = compile(processed.code, {
filename,
outputFilename: '/project/build/output.js',
cssOutputFilename: '/project/build/output.css',
sourcemap: processed.map,
generate
});
assert.deepEqual(result.js.map.sources, [original]);
assert.deepEqual(result.css?.map.sources, [original]);
});
});
Loading…
Cancel
Save