diff --git a/package-lock.json b/package-lock.json index ad4941a25..3a5dd78b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -968,6 +968,13 @@ "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==", "requires": { "source-map": "~0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } } }, "cli-cursor": { @@ -5096,9 +5103,9 @@ "dev": true }, "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" }, "source-map-resolve": { "version": "0.5.2", @@ -5121,6 +5128,14 @@ "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "source-map-url": { @@ -5448,6 +5463,14 @@ "commander": "^2.20.0", "source-map": "~0.6.1", "source-map-support": "~0.5.12" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "terser-webpack-plugin": { @@ -5466,6 +5489,14 @@ "terser": "^4.0.0", "webpack-sources": "^1.3.0", "worker-farm": "^1.7.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "text-table": { @@ -5623,6 +5654,13 @@ "requires": { "commander": "~2.20.0", "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } } }, "unbzip2-stream": { @@ -5857,6 +5895,14 @@ "requires": { "source-list-map": "^2.0.0", "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "which": { diff --git a/package.json b/package.json index 31e13d475..9dbae3980 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "html-minifier": "^4.0.0", "http-link-header": "^1.0.2", "shimport": "^1.0.1", + "source-map": "^0.7.3", "sourcemap-codec": "^1.4.6", "string-hash": "^1.1.3" }, diff --git a/runtime/src/server/middleware/get_page_handler.ts b/runtime/src/server/middleware/get_page_handler.ts index c39e96c0c..fd9cf64da 100644 --- a/runtime/src/server/middleware/get_page_handler.ts +++ b/runtime/src/server/middleware/get_page_handler.ts @@ -6,6 +6,7 @@ import devalue from 'devalue'; import fetch from 'node-fetch'; import URL from 'url'; import { Manifest, Page, Req, Res } from './types'; +import { sourcemapStacktrace } from './sourcemap_stacktrace'; import { build_dir, dev, src_dir } from '@sapper/internal/manifest-server'; import App from '@sapper/internal/App.svelte'; @@ -223,6 +224,10 @@ export function get_page_handler( l++; }); + if (error instanceof Error && error.stack) { + error.stack = sourcemapStacktrace(error.stack); + } + const props = { stores: { page: { diff --git a/runtime/src/server/middleware/sourcemap_stacktrace.ts b/runtime/src/server/middleware/sourcemap_stacktrace.ts new file mode 100644 index 000000000..40903918b --- /dev/null +++ b/runtime/src/server/middleware/sourcemap_stacktrace.ts @@ -0,0 +1,95 @@ +import fs from 'fs'; +import path from 'path'; +import { SourceMapConsumer, RawSourceMap } from 'source-map'; + +function retrieveSourceMapURL(contents: string) { + const reversed = contents + .split('\n') + .reverse() + .join('\n'); + + const match = /\/[/*]#[ \t]+sourceMappingURL=([^\s'"]+?)(?:[ \t]+|$)/gm.exec(reversed); + if (match) return match[1]; + + return undefined; +} + +const fileCache = new Map(); + +function getFileContents(path: string) { + if (fileCache.has(path)) { + return fileCache.get(path); + } + if (fs.existsSync(path)) { + try { + const data = fs.readFileSync(path, 'utf8'); + fileCache.set(path, data); + return data; + } catch { + return undefined; + } + } +} + +function sourcemapStacktrace(stack: string) { + const replaceFn = (line: string) => + line.replace( + /^ {4}at (?:(.+?)\s+\()?(?:(.+?):(\d+)(?::(\d+))?)\)?/, + (input, varName, filePath, line, column) => { + if (!filePath) return input; + + const contents = getFileContents(filePath); + if (!contents) return input; + + const srcMapPathOrBase64 = retrieveSourceMapURL(contents); + if (!srcMapPathOrBase64) return input; + + let dir = path.dirname(filePath); + let srcMapData: string; + + if (/^data:application\/json[^,]+base64,/.test(srcMapPathOrBase64)) { + const rawData = srcMapPathOrBase64.slice(srcMapPathOrBase64.indexOf(',') + 1); + try { + srcMapData = Buffer.from(rawData, 'base64').toString(); + } catch { + return input; + } + } else { + const absSrcMapPath = path.resolve(dir, srcMapPathOrBase64); + const data = getFileContents(absSrcMapPath); + if (!data) return input; + + srcMapData = data; + dir = path.dirname(absSrcMapPath); + } + + let rawSourceMap: RawSourceMap; + try { + rawSourceMap = JSON.parse(srcMapData); + } catch { + return input; + } + + const consumer = new SourceMapConsumer(rawSourceMap); + const pos = consumer.originalPositionFor({ + line: Number(line), + column: Number(column) + }); + if (!pos.source) return input; + + const absSrcPath = path.resolve(dir, pos.source); + const urlPart = `${absSrcPath}:${pos.line || 0}:${pos.column || 0}`; + + if (!varName) return ` at ${urlPart}`; + return ` at ${varName} (${urlPart})`; + } + ); + + fileCache.clear(); + return stack + .split('\n') + .map(replaceFn) + .join('\n'); +} + +export { sourcemapStacktrace }; \ No newline at end of file