From be9505041bb07844af629344a1115240fb0a8e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=A0=20/=20green?= Date: Wed, 5 Apr 2023 06:07:09 +0900 Subject: [PATCH] fix(ssr): load sourcemaps alongside modules (#11780) Co-authored-by: Justice Almanzar --- packages/vite/src/node/ssr/ssrModuleLoader.ts | 28 +++++++++-- .../ssr-html/__tests__/ssr-html.spec.ts | 27 +++++++++++ playground/ssr-html/package.json | 4 +- playground/ssr-html/src/error.js | 3 ++ playground/ssr-html/test-stacktrace.js | 47 +++++++++++++++++++ 5 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 playground/ssr-html/src/error.js create mode 100644 playground/ssr-html/test-stacktrace.js diff --git a/packages/vite/src/node/ssr/ssrModuleLoader.ts b/packages/vite/src/node/ssr/ssrModuleLoader.ts index cd234876a748e6..2f9e71f55569bd 100644 --- a/packages/vite/src/node/ssr/ssrModuleLoader.ts +++ b/packages/vite/src/node/ssr/ssrModuleLoader.ts @@ -11,6 +11,7 @@ import { import { transformRequest } from '../server/transformRequest' import type { InternalResolveOptionsWithOverrideConditions } from '../plugins/resolve' import { tryNodeResolve } from '../plugins/resolve' +import { genSourceMapUrl } from '../server/sourcemap' import { ssrDynamicImportKey, ssrExportAllKey, @@ -26,6 +27,16 @@ interface SSRContext { type SSRModule = Record +// eslint-disable-next-line @typescript-eslint/no-empty-function +const AsyncFunction = async function () {}.constructor as typeof Function +let fnDeclarationLineCount = 0 +{ + const body = '/*code*/' + const source = new AsyncFunction('a', 'b', body).toString() + fnDeclarationLineCount = + source.slice(0, source.indexOf(body)).split('\n').length - 1 +} + const pendingModules = new Map>() const pendingImports = new Map() const importErrors = new WeakMap() @@ -190,9 +201,18 @@ async function instantiateModule( } } + let sourceMapSuffix = '' + if (result.map) { + const moduleSourceMap = Object.assign({}, result.map, { + // currently we need to offset the line + // https://github.com/nodejs/node/issues/43047#issuecomment-1180632750 + mappings: ';'.repeat(fnDeclarationLineCount) + result.map.mappings, + }) + sourceMapSuffix = + '\n//# sourceMappingURL=' + genSourceMapUrl(moduleSourceMap) + } + try { - // eslint-disable-next-line @typescript-eslint/no-empty-function - const AsyncFunction = async function () {}.constructor as typeof Function const initModule = new AsyncFunction( `global`, ssrModuleExportsKey, @@ -200,7 +220,9 @@ async function instantiateModule( ssrImportKey, ssrDynamicImportKey, ssrExportAllKey, - '"use strict";' + result.code + `\n//# sourceURL=${mod.url}`, + '"use strict";' + + result.code + + `\n//# sourceURL=${mod.url}${sourceMapSuffix}`, ) await initModule( context.global, diff --git a/playground/ssr-html/__tests__/ssr-html.spec.ts b/playground/ssr-html/__tests__/ssr-html.spec.ts index fa871fce04efb8..2b9da5cadd01c0 100644 --- a/playground/ssr-html/__tests__/ssr-html.spec.ts +++ b/playground/ssr-html/__tests__/ssr-html.spec.ts @@ -1,3 +1,6 @@ +import { execFile } from 'node:child_process' +import { promisify } from 'node:util' +import path from 'node:path' import fetch from 'node-fetch' import { describe, expect, test } from 'vitest' import { port } from './serve' @@ -55,3 +58,27 @@ describe.runIf(isServe)('hmr', () => { }, '[wow]') }) }) + +describe.runIf(isServe)('stacktrace', () => { + const execFileAsync = promisify(execFile) + + for (const sourcemapsEnabled of [false, true]) { + test(`stacktrace is correct when sourcemaps is${ + sourcemapsEnabled ? '' : ' not' + } enabled in Node.js`, async () => { + const testStacktraceFile = path.resolve( + __dirname, + '../test-stacktrace.js', + ) + + const p = await execFileAsync('node', [ + testStacktraceFile, + '' + sourcemapsEnabled, + ]) + const line = p.stdout + .split('\n') + .find((line) => line.includes('Module.error')) + expect(line.trim()).toMatch(/[\\/]src[\\/]error\.js:2:9/) + }) + } +}) diff --git a/playground/ssr-html/package.json b/playground/ssr-html/package.json index 090faa1b578684..f1fb19aaa25488 100644 --- a/playground/ssr-html/package.json +++ b/playground/ssr-html/package.json @@ -6,7 +6,9 @@ "scripts": { "dev": "node server", "serve": "NODE_ENV=production node server", - "debug": "node --inspect-brk server" + "debug": "node --inspect-brk server", + "test-stacktrace:off": "node test-stacktrace false", + "test-stacktrace:on": "node test-stacktrace true" }, "dependencies": {}, "devDependencies": { diff --git a/playground/ssr-html/src/error.js b/playground/ssr-html/src/error.js new file mode 100644 index 00000000000000..fe8eeb20af8f8a --- /dev/null +++ b/playground/ssr-html/src/error.js @@ -0,0 +1,3 @@ +export function error() { + throw new Error('e') +} diff --git a/playground/ssr-html/test-stacktrace.js b/playground/ssr-html/test-stacktrace.js new file mode 100644 index 00000000000000..c3ce5e56736799 --- /dev/null +++ b/playground/ssr-html/test-stacktrace.js @@ -0,0 +1,47 @@ +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { createServer } from 'vite' + +const isSourceMapEnabled = process.argv[2] === 'true' +process.setSourceMapsEnabled(isSourceMapEnabled) +console.log('# sourcemaps enabled:', isSourceMapEnabled) + +const version = (() => { + const m = process.version.match(/^v(\d+)\.(\d+)\.\d+$/) + if (!m) throw new Error(`Failed to parse version: ${process.version}`) + + return { major: +m[1], minor: +m[2] } +})() + +// https://github.com/nodejs/node/pull/43428 +const isFunctionSourceMapSupported = + (version.major === 16 && version.minor >= 17) || + (version.major === 18 && version.minor >= 6) || + version.major >= 19 + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const isTest = process.env.VITEST + +const vite = await createServer({ + root: __dirname, + logLevel: isTest ? 'error' : 'info', + server: { + middlewareMode: true, + }, + appType: 'custom', +}) + +const mod = await vite.ssrLoadModule('/src/error.js') +try { + mod.error() +} catch (e) { + // this should not be called + // when sourcemap support for `new Function` is supported and sourcemap is enabled + // because the stacktrace is already rewritten by Node.js + if (!(isSourceMapEnabled && isFunctionSourceMapSupported)) { + vite.ssrFixStacktrace(e) + } + console.log(e) +} + +await vite.close()