Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/brave-files-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: support sourcemaps in production
115 changes: 2 additions & 113 deletions packages/kit/src/core/postbuild/prerender.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
import { SourceMap } from 'node:module';
import { dirname, join, relative, resolve as resolve_path } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { dirname, join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { mkdirp, walk } from '../../utils/filesystem.js';
import { posixify } from '../../utils/os.js';
import { noop } from '../../utils/functions.js';
Expand All @@ -17,7 +16,6 @@ import * as devalue from 'devalue';
import { createReadableStream } from '@sveltejs/kit/node';
import generate_fallback from './fallback.js';
import { stringify_remote_arg } from '../../runtime/shared.js';
import { SRC_ROOT } from '../../constants.js';
import { log_response } from '../../exports/vite/utils.js';

export default forked(import.meta.url, prerender);
Expand Down Expand Up @@ -50,7 +48,6 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
// essential we do this before analysing the code
internal.set_building();
internal.set_prerendering();
internal.set_fix_stack_trace(adjust_stack_trace);

// `set_env` and `Server` live in modules that import the user's `src/env` config. We import them
// *after* `set_building()` so that `building`-dependent expressions resolve correctly
Expand Down Expand Up @@ -119,114 +116,6 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
}
}

/** @type {Map<string, { map: SourceMap; directory: string } | null>} */
const source_maps = new Map();
/** @type {Map<string, Array<string | undefined>>} */
const source_regions = new Map();

/** @param {string} file */
function get_source_map(file) {
if (source_maps.has(file)) return source_maps.get(file);

let source;
let directory = dirname(file);
try {
const code = readFileSync(file, 'utf8');
const matches = Array.from(code.matchAll(/\/\/[#@]\s*sourceMappingURL=(\S+)/g));
const url = matches.at(-1)?.[1];

if (url?.startsWith('data:')) {
const comma = url.indexOf(',');
const metadata = url.slice(5, comma);
const data = url.slice(comma + 1);
source = metadata.endsWith(';base64')
? Buffer.from(data, 'base64').toString()
: decodeURIComponent(data);
} else {
const map_file = url ? resolve_path(dirname(file), decodeURIComponent(url)) : `${file}.map`;
if (existsSync(map_file)) {
directory = dirname(map_file);
source = readFileSync(map_file, 'utf8');
}
}

const source_map = source ? { map: new SourceMap(JSON.parse(source)), directory } : null;
source_maps.set(file, source_map);
return source_map;
} catch {
source_maps.set(file, null);
return null;
}
}

/** @param {unknown} error */
function adjust_stack_trace(error) {
if (!(error instanceof Error) || !error.stack) return;

error.stack = error.stack
.split('\n')
.map((line) => {
const match = line.match(/(file:\/\/\/.*):(\d+):(\d+)(\)?)$/);
if (!match) return line;

const file = fileURLToPath(match[1]);
// Filter out internal stack trace lines
if (file.includes('/svelte/src/internal/server/') || file.startsWith(SRC_ROOT)) {
return null;
}
// Only annotate files from our own server directory
if (!file.startsWith(`${out}/server/`)) return line;

const source_map = get_source_map(file);
const entry = source_map?.map.findEntry(Number(match[2]) - 1, Number(match[3]) - 1);
if (
source_map &&
entry &&
'originalSource' in entry &&
entry.originalSource &&
typeof entry.originalLine === 'number' &&
typeof entry.originalColumn === 'number'
) {
const source = entry.originalSource.startsWith('file:')
? fileURLToPath(entry.originalSource)
: resolve_path(source_map.directory, entry.originalSource);

// Filter out internal stack trace lines (gotta do this again here on the original)
if (source.includes('/svelte/src/internal/server/') || source.startsWith(SRC_ROOT)) {
return null;
}

const location = `${match[1]}:${match[2]}:${match[3]}`;
const original = `${posixify(relative(vite_config.root, source))}:${entry.originalLine + 1}:${entry.originalColumn + 1}`;

return line.replace(location, original);
}

let regions = source_regions.get(file);
if (!regions) {
/** @type {string | undefined} */
let source;
regions = readFileSync(file, 'utf8')
.split('\n')
.map((line) => {
// Vite will merge multiple files into one but add region markers
// with the original file names, which we try to extract here.
const start = line.match(/^\/\/#region (.+)$/);
if (start) source = start[1];
if (line === '//#endregion') source = undefined;
return source;
});
source_regions.set(file, regions);
}

const source = regions[Number(match[2]) - 1];
// Output is something like "at file:///... [src/routes/+page.svelte]"
return source ? `${line} [${source}]` : line;
})
.filter(Boolean)
.join('\n');
}

const OK = 2;
const REDIRECT = 3;

Expand Down
27 changes: 9 additions & 18 deletions packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,14 @@ function kit({ svelte_config }) {
// dev and preview config can be shared
/** @type {UserConfig} */
const new_config = {
environments: {
ssr: {
build: {
sourcemap:
config.environments?.ssr?.build?.sourcemap ?? config.build?.sourcemap ?? true
}
}
},
resolve: {
alias: [
{ find: '__SERVER__', replacement: `${generated}/server` },
Expand Down Expand Up @@ -1543,15 +1551,6 @@ function kit({ svelte_config }) {
},

async buildApp(builder) {
const sourcemap = builder.config.build.sourcemap;
const ssr_sourcemap = builder.environments.ssr.config.build.sourcemap;
const temporary_sourcemap = !(ssr_sourcemap ?? sourcemap);

if (temporary_sourcemap) {
// Temporarily override so that we get better stack traces for prerendering errors
builder.environments.ssr.config.build.sourcemap = 'hidden';
}

// clears the output directories
if (!builder.config.build.watch) {
rimraf(out);
Expand Down Expand Up @@ -1860,16 +1859,8 @@ function kit({ svelte_config }) {
// Unforeseen error, rethrow as-is
throw e;
}
} finally {
if (temporary_sourcemap) {
// If we did override it, set it back to false and remove the sourcemaps
builder.environments.ssr.config.build.sourcemap = ssr_sourcemap;

for (const file of fs.globSync(`${out}/server/**/*.js.map`)) {
fs.rmSync(file);
}
}
}

prerendered = prerender_results.prerendered;

await treeshake_prerendered_remotes(
Expand Down
15 changes: 2 additions & 13 deletions packages/kit/src/runtime/server/internal.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,12 @@
/** @import { SSRManifest } from '@sveltejs/kit'; */

/**
* @param {Error} _error
* @returns void
*/
export let fix_stack_trace = (_error) => {};

/**
* @type {((path: string) => ReadableStream<any>) | null}
*/
export let read_implementation = null;

export let manifest = /** @type {SSRManifest} */ (/** @type {unknown} */ (null));

/**
* @param {(error: Error) => void} fn
*/
export function set_fix_stack_trace(fn) {
fix_stack_trace = fn;
}

/**
* @param {(path: string) => ReadableStream<any>} fn
*/
Expand All @@ -34,3 +21,5 @@ export function set_read_implementation(fn) {
export function set_manifest(value) {
manifest = value;
}

export { fix_stack_trace, set_fix_stack_trace } from './sourcemaps.js';
Loading
Loading