From 85e4bb4be9ead062327edb2dcb9ba8f6af2600ed Mon Sep 17 00:00:00 2001 From: Ryan Christian Date: Mon, 19 Feb 2024 23:56:12 -0600 Subject: [PATCH] feat: Form code frame from sourcemaps for prerender errors --- package-lock.json | 76 ++++++++++++++++++++++++++++++++++++++++------- package.json | 6 +++- src/prerender.ts | 56 ++++++++++++++++++++++++++++++---- 3 files changed, 121 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index f41ece7..044233a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,15 +18,19 @@ "kolorist": "^1.8.0", "magic-string": "0.30.5", "node-html-parser": "^6.1.10", - "resolve": "^1.22.8" + "resolve": "^1.22.8", + "source-map": "^0.7.4", + "stack-trace": "^1.0.0-pre2" }, "devDependencies": { "@babel/core": "^7.15.8", + "@types/babel__code-frame": "^7.0.6", "@types/babel__core": "^7.1.14", "@types/debug": "^4.1.5", "@types/estree": "^0.0.50", "@types/node": "^14.14.33", "@types/resolve": "^1.20.1", + "@types/stack-trace": "^0.0.33", "lint-staged": "^10.5.4", "preact": "^10.19.2", "preact-iso": "^2.3.2", @@ -604,6 +608,12 @@ "node": ">= 8.0.0" } }, + "node_modules/@types/babel__code-frame": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@types/babel__code-frame/-/babel__code-frame-7.0.6.tgz", + "integrity": "sha512-Anitqkl3+KrzcW2k77lRlg/GfLZLWXBuNgbEcIOU6M92yw42vsd3xV/Z/yAHEj8m+KUjL6bWOVOFqX8PFPJ4LA==", + "dev": true + }, "node_modules/@types/babel__core": { "version": "7.1.14", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.14.tgz", @@ -675,6 +685,12 @@ "integrity": "sha512-Ku5+GPFa12S3W26Uwtw+xyrtIpaZsGYHH6zxNbZlstmlvMYSZRzOwzwsXbxlVUbHyUucctSyuFtu6bNxwYomIw==", "dev": true }, + "node_modules/@types/stack-trace": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/stack-trace/-/stack-trace-0.0.33.tgz", + "integrity": "sha512-O7in6531Bbvlb2KEsJ0dq0CHZvc3iWSR5ZYMtvGgnHA56VgriAN/AU2LorfmcvAl2xc9N5fbCTRyMRRl8nd74g==", + "dev": true + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -2421,12 +2437,11 @@ } }, "node_modules/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, + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "engines": { - "node": ">=0.10.0" + "node": ">= 8" } }, "node_modules/source-map-js": { @@ -2447,6 +2462,23 @@ "source-map": "^0.6.0" } }, + "node_modules/source-map-support/node_modules/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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-trace": { + "version": "1.0.0-pre2", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-1.0.0-pre2.tgz", + "integrity": "sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==", + "engines": { + "node": ">=16" + } + }, "node_modules/string-argv": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", @@ -3152,6 +3184,12 @@ "picomatch": "^2.2.2" } }, + "@types/babel__code-frame": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@types/babel__code-frame/-/babel__code-frame-7.0.6.tgz", + "integrity": "sha512-Anitqkl3+KrzcW2k77lRlg/GfLZLWXBuNgbEcIOU6M92yw42vsd3xV/Z/yAHEj8m+KUjL6bWOVOFqX8PFPJ4LA==", + "dev": true + }, "@types/babel__core": { "version": "7.1.14", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.14.tgz", @@ -3223,6 +3261,12 @@ "integrity": "sha512-Ku5+GPFa12S3W26Uwtw+xyrtIpaZsGYHH6zxNbZlstmlvMYSZRzOwzwsXbxlVUbHyUucctSyuFtu6bNxwYomIw==", "dev": true }, + "@types/stack-trace": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/stack-trace/-/stack-trace-0.0.33.tgz", + "integrity": "sha512-O7in6531Bbvlb2KEsJ0dq0CHZvc3iWSR5ZYMtvGgnHA56VgriAN/AU2LorfmcvAl2xc9N5fbCTRyMRRl8nd74g==", + "dev": true + }, "aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -4364,10 +4408,9 @@ } }, "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 + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==" }, "source-map-js": { "version": "1.0.2", @@ -4382,8 +4425,21 @@ "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 + } } }, + "stack-trace": { + "version": "1.0.0-pre2", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-1.0.0-pre2.tgz", + "integrity": "sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==" + }, "string-argv": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", diff --git a/package.json b/package.json index b5687ac..0fe76df 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,9 @@ "kolorist": "^1.8.0", "magic-string": "0.30.5", "node-html-parser": "^6.1.10", - "resolve": "^1.22.8" + "resolve": "^1.22.8", + "source-map": "^0.7.4", + "stack-trace": "^1.0.0-pre2" }, "peerDependencies": { "@babel/core": "7.x", @@ -50,11 +52,13 @@ }, "devDependencies": { "@babel/core": "^7.15.8", + "@types/babel__code-frame": "^7.0.6", "@types/babel__core": "^7.1.14", "@types/debug": "^4.1.5", "@types/estree": "^0.0.50", "@types/node": "^14.14.33", "@types/resolve": "^1.20.1", + "@types/stack-trace": "^0.0.33", "lint-staged": "^10.5.4", "preact": "^10.19.2", "preact-iso": "^2.3.2", diff --git a/src/prerender.ts b/src/prerender.ts index 2a438ab..dd26a7e 100644 --- a/src/prerender.ts +++ b/src/prerender.ts @@ -1,9 +1,11 @@ import path from "node:path"; - import { promises as fs } from "node:fs"; import MagicString from "magic-string"; import { parse as htmlParse } from "node-html-parser"; +import { SourceMapConsumer } from "source-map"; +import { parse as StackTraceParse } from "stack-trace"; +import { codeFrameColumns } from "@babel/code-frame"; import type { Plugin, ResolvedConfig } from "vite"; @@ -116,6 +118,9 @@ export function PrerenderPlugin({ apply: "build", enforce: "post", configResolved(config) { + // Enable sourcemaps at least for prerendering for usable error messages + config.build.sourcemap = true; + viteConfig = config; }, async options(opts) { @@ -212,7 +217,7 @@ export function PrerenderPlugin({ JSON.stringify({ type: "module" }), ); - let prerenderEntry; + let prerenderEntry: OutputChunk | undefined; for (const output of Object.keys(bundle)) { if (!/\.js$/.test(output) || bundle[output].type !== "chunk") continue; @@ -222,7 +227,7 @@ export function PrerenderPlugin({ ); if ((bundle[output] as OutputChunk).exports?.includes("prerender")) { - prerenderEntry = bundle[output]; + prerenderEntry = bundle[output] as OutputChunk; } } if (!prerenderEntry) { @@ -238,15 +243,18 @@ export function PrerenderPlugin({ ); prerender = m.prerender; } catch (e) { - const isReferenceError = e instanceof ReferenceError; + const stack = StackTraceParse(e as Error).find(s => + s.getFileName().includes(tmpDir), + ); - const message = ` + const isReferenceError = e instanceof ReferenceError; + let message = `\n ${e} This ${ isReferenceError ? "is most likely" : "could be" } caused by using DOM/Web APIs which are not available - available to the prerendering process which runs in Node. Consider + available to the prerendering process running in Node. Consider wrapping the offending code in a window check like so: if (typeof window !== "undefined") { @@ -254,6 +262,42 @@ export function PrerenderPlugin({ } `.replace(/^\t{5}/gm, ""); + const sourceMapContent = prerenderEntry.map; + if (stack && sourceMapContent) { + await SourceMapConsumer.with( + sourceMapContent, + null, + async consumer => { + let { source, line, column } = consumer.originalPositionFor({ + line: stack.getLineNumber(), + column: stack.getColumnNumber(), + }); + + if (!source || line == null || column == null) { + message += `\nUnable to locate source map for error!\n`; + this.error(message); + } + + // `source-map` returns 0-indexed column numbers + column += 1; + + const sourcePath = path.join( + viteConfig.root, + source.replace(/^(..\/)*/, ""), + ); + const sourceContent = await fs.readFile(sourcePath, "utf-8"); + + const frame = codeFrameColumns(sourceContent, { + start: { line, column }, + }); + message += ` + > ${sourcePath}:${line}:${column}\n + ${frame} + `.replace(/^\t{7}/gm, ""); + }, + ); + } + this.error(message); }