11import path from "node:path" ;
2-
32import { promises as fs } from "node:fs" ;
43
54import MagicString from "magic-string" ;
65import { parse as htmlParse } from "node-html-parser" ;
6+ import { SourceMapConsumer } from "source-map" ;
7+ import { parse as StackTraceParse } from "stack-trace" ;
8+ import { codeFrameColumns } from "@babel/code-frame" ;
79
810import type { Plugin , ResolvedConfig } from "vite" ;
911
@@ -116,6 +118,9 @@ export function PrerenderPlugin({
116118 apply : "build" ,
117119 enforce : "post" ,
118120 configResolved ( config ) {
121+ // Enable sourcemaps for generating more actionable error messages
122+ config . build . sourcemap = true ;
123+
119124 viteConfig = config ;
120125 } ,
121126 async options ( opts ) {
@@ -212,7 +217,7 @@ export function PrerenderPlugin({
212217 JSON . stringify ( { type : "module" } ) ,
213218 ) ;
214219
215- let prerenderEntry ;
220+ let prerenderEntry : OutputChunk | undefined ;
216221 for ( const output of Object . keys ( bundle ) ) {
217222 if ( ! / \. j s $ / . test ( output ) || bundle [ output ] . type !== "chunk" ) continue ;
218223
@@ -222,7 +227,7 @@ export function PrerenderPlugin({
222227 ) ;
223228
224229 if ( ( bundle [ output ] as OutputChunk ) . exports ?. includes ( "prerender" ) ) {
225- prerenderEntry = bundle [ output ] ;
230+ prerenderEntry = bundle [ output ] as OutputChunk ;
226231 }
227232 }
228233 if ( ! prerenderEntry ) {
@@ -238,22 +243,61 @@ export function PrerenderPlugin({
238243 ) ;
239244 prerender = m . prerender ;
240245 } catch ( e ) {
241- const isReferenceError = e instanceof ReferenceError ;
246+ const stack = StackTraceParse ( e as Error ) . find ( s =>
247+ s . getFileName ( ) . includes ( tmpDir ) ,
248+ ) ;
242249
243- const message = `
250+ const isReferenceError = e instanceof ReferenceError ;
251+ let message = `\n
244252 ${ e }
245253
246254 This ${
247255 isReferenceError ? "is most likely" : "could be"
248256 } caused by using DOM/Web APIs which are not available
249- available to the prerendering process which runs in Node. Consider
257+ available to the prerendering process running in Node. Consider
250258 wrapping the offending code in a window check like so:
251259
252260 if (typeof window !== "undefined") {
253261 // do something in browsers only
254262 }
255263 ` . replace ( / ^ \t { 5 } / gm, "" ) ;
256264
265+ const sourceMapContent = prerenderEntry . map ;
266+ if ( stack && sourceMapContent ) {
267+ await SourceMapConsumer . with (
268+ sourceMapContent ,
269+ null ,
270+ async consumer => {
271+ let { source, line, column } = consumer . originalPositionFor ( {
272+ line : stack . getLineNumber ( ) ,
273+ column : stack . getColumnNumber ( ) ,
274+ } ) ;
275+
276+ if ( ! source || line == null || column == null ) {
277+ message += `\nUnable to locate source map for error!\n` ;
278+ this . error ( message ) ;
279+ }
280+
281+ // `source-map` returns 0-indexed column numbers
282+ column += 1 ;
283+
284+ const sourcePath = path . join (
285+ viteConfig . root ,
286+ source . replace ( / ^ ( ..\/ ) * / , "" ) ,
287+ ) ;
288+ const sourceContent = await fs . readFile ( sourcePath , "utf-8" ) ;
289+
290+ const frame = codeFrameColumns ( sourceContent , {
291+ start : { line, column } ,
292+ } ) ;
293+ message += `
294+ > ${ sourcePath } :${ line } :${ column } \n
295+ ${ frame }
296+ ` . replace ( / ^ \t { 7 } / gm, "" ) ;
297+ } ,
298+ ) ;
299+ }
300+
257301 this . error ( message ) ;
258302 }
259303
0 commit comments