@jsenv/core 27.5.1 → 27.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,32 +1,38 @@
1
+ import { formatError } from "./error_formatter.js"
2
+
1
3
  const JSENV_ERROR_OVERLAY_TAGNAME = "jsenv-error-overlay"
2
4
 
3
5
  export const displayErrorInDocument = (
4
6
  error,
5
7
  {
6
8
  rootDirectoryUrl,
9
+ errorBaseUrl,
7
10
  openInEditor,
8
11
  url,
9
12
  line,
10
13
  column,
11
- reportedBy,
12
- requestedRessource,
14
+ codeFrame,
13
15
  },
14
16
  ) => {
15
- document.querySelectorAll(JSENV_ERROR_OVERLAY_TAGNAME).forEach((node) => {
16
- node.parentNode.removeChild(node)
17
- })
18
- const { theme, title, message, stack, tip } = errorToHTML(error, {
17
+ const { theme, title, text, tip, errorDetailsPromise } = formatError(error, {
18
+ rootDirectoryUrl,
19
+ errorBaseUrl,
20
+ openInEditor,
19
21
  url,
20
22
  line,
21
23
  column,
22
- reportedBy,
23
- requestedRessource,
24
+ codeFrame,
24
25
  })
26
+
25
27
  let jsenvErrorOverlay = new JsenvErrorOverlay({
26
28
  theme,
27
29
  title,
28
- text: createErrorText({ rootDirectoryUrl, openInEditor, message, stack }),
30
+ text,
29
31
  tip,
32
+ errorDetailsPromise,
33
+ })
34
+ document.querySelectorAll(JSENV_ERROR_OVERLAY_TAGNAME).forEach((node) => {
35
+ node.parentNode.removeChild(node)
30
36
  })
31
37
  document.body.appendChild(jsenvErrorOverlay)
32
38
  const removeErrorOverlay = () => {
@@ -45,29 +51,24 @@ export const displayErrorInDocument = (
45
51
  return removeErrorOverlay
46
52
  }
47
53
 
48
- const createErrorText = ({
49
- rootDirectoryUrl,
50
- openInEditor,
51
- message,
52
- stack,
53
- }) => {
54
- if (message && stack) {
55
- return `${replaceLinks(message, {
56
- rootDirectoryUrl,
57
- openInEditor,
58
- })}\n${replaceLinks(stack, { rootDirectoryUrl, openInEditor })}`
59
- }
60
- if (stack) {
61
- return replaceLinks(stack, { rootDirectoryUrl, openInEditor })
62
- }
63
- return replaceLinks(message, { rootDirectoryUrl, openInEditor })
64
- }
65
-
66
54
  class JsenvErrorOverlay extends HTMLElement {
67
- constructor({ theme, title, text, tip }) {
55
+ constructor({ theme, title, text, tip, errorDetailsPromise }) {
68
56
  super()
69
57
  this.root = this.attachShadow({ mode: "open" })
70
- this.root.innerHTML = overlayHtml
58
+ this.root.innerHTML = `
59
+ <style>
60
+ ${overlayCSS}
61
+ </style>
62
+ <div class="backdrop"></div>
63
+ <div class="overlay" data-theme=${theme}>
64
+ <h1 class="title">
65
+ ${title}
66
+ </h1>
67
+ <pre class="text">${text}</pre>
68
+ <div class="tip">
69
+ ${tip}
70
+ </div>
71
+ </div>`
71
72
  this.root.querySelector(".backdrop").onclick = () => {
72
73
  if (!this.parentNode) {
73
74
  // not in document anymore
@@ -76,19 +77,44 @@ class JsenvErrorOverlay extends HTMLElement {
76
77
  this.root.querySelector(".backdrop").onclick = null
77
78
  this.parentNode.removeChild(this)
78
79
  }
79
- this.root.querySelector(".overlay").setAttribute("data-theme", theme)
80
- this.root.querySelector(".title").innerHTML = title
81
- this.root.querySelector(".text").innerHTML = text
82
- this.root.querySelector(".tip").innerHTML = tip
80
+ if (errorDetailsPromise) {
81
+ errorDetailsPromise.then((errorDetails) => {
82
+ if (!errorDetails || !this.parentNode) {
83
+ return
84
+ }
85
+ const { codeFrame, cause } = errorDetails
86
+ if (codeFrame) {
87
+ this.root.querySelector(".text").innerHTML += `\n\n${codeFrame}`
88
+ }
89
+ if (cause) {
90
+ const causeIndented = prefixRemainingLines(cause, " ")
91
+ this.root.querySelector(
92
+ ".text",
93
+ ).innerHTML += `\n [cause]: ${causeIndented}`
94
+ }
95
+ })
96
+ }
83
97
  }
84
98
  }
85
99
 
100
+ const prefixRemainingLines = (text, prefix) => {
101
+ const lines = text.split(/\r?\n/)
102
+ const firstLine = lines.shift()
103
+ let result = firstLine
104
+ let i = 0
105
+ while (i < lines.length) {
106
+ const line = lines[i]
107
+ i++
108
+ result += line.length ? `\n${prefix}${line}` : `\n`
109
+ }
110
+ return result
111
+ }
112
+
86
113
  if (customElements && !customElements.get(JSENV_ERROR_OVERLAY_TAGNAME)) {
87
114
  customElements.define(JSENV_ERROR_OVERLAY_TAGNAME, JsenvErrorOverlay)
88
115
  }
89
116
 
90
- const overlayHtml = `
91
- <style>
117
+ const overlayCSS = `
92
118
  :host {
93
119
  position: fixed;
94
120
  z-index: 99999;
@@ -162,240 +188,4 @@ pre {
162
188
 
163
189
  pre a {
164
190
  color: inherit;
165
- }
166
- </style>
167
- <div class="backdrop"></div>
168
- <div class="overlay">
169
- <h1 class="title"></h1>
170
- <pre class="text"></pre>
171
- <div class="tip"></div>
172
- </div>
173
- `
174
-
175
- const parseErrorInfo = (error) => {
176
- if (error === undefined) {
177
- return {
178
- message: "undefined",
179
- }
180
- }
181
- if (error === null) {
182
- return {
183
- message: "null",
184
- }
185
- }
186
- if (typeof error === "string") {
187
- return {
188
- message: error,
189
- }
190
- }
191
- if (error instanceof Error) {
192
- if (error.name === "SyntaxError") {
193
- return {
194
- message: error.message,
195
- }
196
- }
197
- if (error.cause && error.cause.code === "PARSE_ERROR") {
198
- if (error.messageHTML) {
199
- return {
200
- message: error.messageHTML,
201
- }
202
- }
203
- return {
204
- message: error.message,
205
- }
206
- }
207
- // stackTrace formatted by V8
208
- if (Error.captureStackTrace) {
209
- return {
210
- message: error.message,
211
- stack: getErrorStackWithoutErrorMessage(error),
212
- }
213
- }
214
- return {
215
- message: error.message,
216
- stack: error.stack ? ` ${error.stack}` : null,
217
- }
218
- }
219
- if (typeof error === "object") {
220
- return error
221
- }
222
- return {
223
- message: JSON.stringify(error),
224
- }
225
- }
226
-
227
- const getErrorStackWithoutErrorMessage = (error) => {
228
- let stack = error.stack
229
- const messageInStack = `${error.name}: ${error.message}`
230
- if (stack.startsWith(messageInStack)) {
231
- stack = stack.slice(messageInStack.length)
232
- }
233
- const nextLineIndex = stack.indexOf("\n")
234
- if (nextLineIndex > -1) {
235
- stack = stack.slice(nextLineIndex + 1)
236
- }
237
- return stack
238
- }
239
-
240
- const errorToHTML = (
241
- error,
242
- { url, line, column, reportedBy, requestedRessource },
243
- ) => {
244
- let { message, stack } = parseErrorInfo(error)
245
- if (url) {
246
- if (!stack || (error && error.name === "SyntaxError")) {
247
- stack = ` at ${appendLineAndColumn(url, { line, column })}`
248
- }
249
- }
250
- let tip = formatTip({ reportedBy, requestedRessource })
251
- return {
252
- theme:
253
- error && error.cause && error.cause.code === "PARSE_ERROR"
254
- ? "light"
255
- : "dark",
256
- title: "An error occured",
257
- message,
258
- stack,
259
- tip: `${tip}
260
- <br />
261
- Click outside to close.`,
262
- }
263
- }
264
-
265
- const formatTip = ({ reportedBy, requestedRessource }) => {
266
- if (reportedBy === "browser") {
267
- return `Reported by the browser while executing <code>${window.location.pathname}${window.location.search}</code>.`
268
- }
269
- return `Reported by the server while serving <code>${requestedRessource}</code>`
270
- }
271
-
272
- const replaceLinks = (string, { rootDirectoryUrl, openInEditor }) => {
273
- // normalize line breaks
274
- string = string.replace(/\n/g, "\n")
275
- string = escapeHtml(string)
276
- // render links
277
- string = stringToStringWithLink(string, {
278
- transform: (url, { line, column }) => {
279
- const urlObject = new URL(url)
280
-
281
- const onFileUrl = (fileUrlObject) => {
282
- const atFsIndex = fileUrlObject.pathname.indexOf("/@fs/")
283
- let fileUrl
284
- if (atFsIndex > -1) {
285
- const afterAtFs = fileUrlObject.pathname.slice(
286
- atFsIndex + "/@fs/".length,
287
- )
288
- fileUrl = new URL(afterAtFs, "file:///").href
289
- } else {
290
- fileUrl = fileUrlObject.href
291
- }
292
- fileUrl = appendLineAndColumn(fileUrl, {
293
- line,
294
- column,
295
- })
296
- return link({
297
- href: openInEditor
298
- ? `javascript:window.fetch('/__open_in_editor__/${fileUrl}')`
299
- : fileUrl,
300
- text: fileUrl,
301
- })
302
- }
303
-
304
- if (urlObject.origin === window.origin) {
305
- const fileUrlObject = new URL(
306
- `${urlObject.pathname.slice(1)}${urlObject.search}`,
307
- rootDirectoryUrl,
308
- )
309
- return onFileUrl(fileUrlObject)
310
- }
311
- if (urlObject.href.startsWith("file:")) {
312
- return onFileUrl(urlObject)
313
- }
314
- return link({
315
- href: url,
316
- text: appendLineAndColumn(url, { line, column }),
317
- })
318
- },
319
- })
320
- return string
321
- }
322
-
323
- const escapeHtml = (string) => {
324
- return string
325
- .replace(/&/g, "&amp;")
326
- .replace(/</g, "&lt;")
327
- .replace(/>/g, "&gt;")
328
- .replace(/"/g, "&quot;")
329
- .replace(/'/g, "&#039;")
330
- }
331
-
332
- const appendLineAndColumn = (url, { line, column }) => {
333
- if (line !== undefined && column !== undefined) {
334
- return `${url}:${line}:${column}`
335
- }
336
- if (line !== undefined) {
337
- return `${url}:${line}`
338
- }
339
- return url
340
- }
341
-
342
- // `Error: yo
343
- // at Object.execute (http://127.0.0.1:57300/build/src/__test__/file-throw.js:9:13)
344
- // at doExec (http://127.0.0.1:3000/src/__test__/file-throw.js:452:38)
345
- // at postOrderExec (http://127.0.0.1:3000/src/__test__/file-throw.js:448:16)
346
- // at http://127.0.0.1:3000/src/__test__/file-throw.js:399:18`.replace(/(?:https?|ftp|file):\/\/(.*+)$/gm, (...args) => {
347
- // debugger
348
- // })
349
- const stringToStringWithLink = (
350
- source,
351
- {
352
- transform = (url) => {
353
- return {
354
- href: url,
355
- text: url,
356
- }
357
- },
358
- } = {},
359
- ) => {
360
- return source.replace(/(?:https?|ftp|file):\/\/\S+/gm, (match) => {
361
- let linkHTML = ""
362
-
363
- const lastChar = match[match.length - 1]
364
-
365
- // hotfix because our url regex sucks a bit
366
- const endsWithSeparationChar = lastChar === ")" || lastChar === ":"
367
- if (endsWithSeparationChar) {
368
- match = match.slice(0, -1)
369
- }
370
-
371
- const lineAndColumnPattern = /:([0-9]+):([0-9]+)$/
372
- const lineAndColumMatch = match.match(lineAndColumnPattern)
373
- if (lineAndColumMatch) {
374
- const lineAndColumnString = lineAndColumMatch[0]
375
- const lineNumber = lineAndColumMatch[1]
376
- const columnNumber = lineAndColumMatch[2]
377
- linkHTML = transform(match.slice(0, -lineAndColumnString.length), {
378
- line: lineNumber,
379
- column: columnNumber,
380
- })
381
- } else {
382
- const linePattern = /:([0-9]+)$/
383
- const lineMatch = match.match(linePattern)
384
- if (lineMatch) {
385
- const lineString = lineMatch[0]
386
- const lineNumber = lineMatch[1]
387
- linkHTML = transform(match.slice(0, -lineString.length), {
388
- line: lineNumber,
389
- })
390
- } else {
391
- linkHTML = transform(match, {})
392
- }
393
- }
394
- if (endsWithSeparationChar) {
395
- return `${linkHTML}${lastChar}`
396
- }
397
- return linkHTML
398
- })
399
- }
400
-
401
- const link = ({ href, text = href }) => `<a href="${href}">${text}</a>`
191
+ }`
@@ -0,0 +1,85 @@
1
+ import { memoize } from "@jsenv/utils/src/memoize/memoize.js"
2
+ import { remapSourcePosition } from "@jsenv/sourcemap/src/error_stack_remap/remap_source_position.js"
3
+ import { SOURCEMAP } from "@jsenv/sourcemap/src/sourcemap_comment.js"
4
+ import { DATA_URL } from "@jsenv/urls/src/data_url.js"
5
+
6
+ const loadSourceMapConsumer = memoize(async () => {
7
+ const script = document.createElement("script")
8
+ script.src = "https://unpkg.com/source-map@0.7.3/dist/source-map.js"
9
+
10
+ const scriptLoadedPromise = new Promise((resolve) => {
11
+ script.onload = resolve
12
+ })
13
+ document.head.appendChild(script)
14
+ await scriptLoadedPromise
15
+ const { SourceMapConsumer } = window.sourceMap
16
+ await SourceMapConsumer.initialize({
17
+ "lib/mappings.wasm": "https://unpkg.com/source-map@0.7.3/lib/mappings.wasm",
18
+ })
19
+ return SourceMapConsumer
20
+ })
21
+
22
+ export const remapErrorSite = async ({ url, line, column }) => {
23
+ const asServerUrl = (url) => {
24
+ if (url.startsWith("file:///")) {
25
+ url = `${window.origin}/@fs/${url.slice("file:///".length)}`
26
+ }
27
+ return url
28
+ }
29
+
30
+ const SourceMapConsumer = await loadSourceMapConsumer()
31
+ const original = await remapSourcePosition({
32
+ source: url,
33
+ line,
34
+ column,
35
+ resolveFile: (specifier) => new URL(specifier, `${window.origin}/`).href,
36
+ urlToSourcemapConsumer: async (url) => {
37
+ const serverUrl = asServerUrl(url)
38
+ const fileResponse = await window.fetch(serverUrl)
39
+ const text = await fileResponse.text()
40
+ const jsSourcemapComment = SOURCEMAP.readComment({
41
+ contentType: "text/javascript",
42
+ content: text,
43
+ })
44
+
45
+ const jsSourcemapUrl = jsSourcemapComment.specifier
46
+ let sourcemapUrl
47
+ let sourcemapUrlContent
48
+ if (jsSourcemapUrl.startsWith("data:")) {
49
+ sourcemapUrl = url
50
+ sourcemapUrlContent = window.atob(DATA_URL.parse(jsSourcemapUrl).data)
51
+ } else {
52
+ sourcemapUrl = new URL(jsSourcemapUrl, url).href
53
+ const sourcemapResponse = await window.fetch(sourcemapUrl)
54
+ sourcemapUrlContent = await sourcemapResponse.text()
55
+ }
56
+
57
+ const sourceMap = JSON.parse(sourcemapUrlContent)
58
+ let { sourcesContent } = sourceMap
59
+ if (!sourcesContent) {
60
+ sourcesContent = []
61
+ sourceMap.sourcesContent = sourcesContent
62
+ }
63
+ let firstSourceMapSourceFailure = null
64
+ await Promise.all(
65
+ sourceMap.sources.map(async (source, index) => {
66
+ if (index in sourcesContent) return
67
+ let sourceUrl = new URL(source, sourcemapUrl).href
68
+ sourceUrl = asServerUrl(sourceUrl)
69
+ try {
70
+ const sourceResponse = await window.fetch(sourceUrl)
71
+ const sourceContent = await sourceResponse.text()
72
+ sourcesContent[index] = sourceContent
73
+ } catch (e) {
74
+ firstSourceMapSourceFailure = e
75
+ }
76
+ }),
77
+ )
78
+ if (firstSourceMapSourceFailure) {
79
+ return null
80
+ }
81
+ return new SourceMapConsumer(sourceMap)
82
+ },
83
+ })
84
+ return original
85
+ }
@@ -11,6 +11,7 @@ export const installHtmlSupervisor = ({
11
11
  logs,
12
12
  measurePerf,
13
13
  errorOverlay,
14
+ errorBaseUrl,
14
15
  openInEditor,
15
16
  }) => {
16
17
  const errorTransformer = null // could implement error stack remapping if needed
@@ -87,11 +88,15 @@ export const installHtmlSupervisor = ({
87
88
  let completed
88
89
  let result
89
90
  let error
91
+ const urlObject = new URL(src, window.location)
92
+ if (reload) {
93
+ urlObject.searchParams.set("hmr", Date.now())
94
+ }
95
+ __html_supervisor__.currentExecution = {
96
+ type: type === "module" ? "dynamic_import" : "script_injection",
97
+ url: urlObject.href,
98
+ }
90
99
  try {
91
- const urlObject = new URL(src, window.location)
92
- if (reload) {
93
- urlObject.searchParams.set("hmr", Date.now())
94
- }
95
100
  result = await execute(urlObject.href)
96
101
  completed = true
97
102
  } catch (e) {
@@ -109,6 +114,7 @@ export const installHtmlSupervisor = ({
109
114
  console.log(`${type} load ended`)
110
115
  console.groupEnd()
111
116
  }
117
+ __html_supervisor__.currentExecution = null
112
118
  return
113
119
  }
114
120
  const executionResult = {
@@ -139,6 +145,7 @@ export const installHtmlSupervisor = ({
139
145
  if (logs) {
140
146
  console.groupEnd()
141
147
  }
148
+ __html_supervisor__.currentExecution = null
142
149
  }
143
150
 
144
151
  const classicExecutionQueue = createExecutionQueue(performExecution)
@@ -219,86 +226,28 @@ export const installHtmlSupervisor = ({
219
226
  })
220
227
 
221
228
  if (errorOverlay) {
229
+ const onErrorReportedByBrowser = (error, { url, line, column }) => {
230
+ displayErrorInDocument(error, {
231
+ rootDirectoryUrl,
232
+ errorBaseUrl,
233
+ openInEditor,
234
+ url,
235
+ line,
236
+ column,
237
+ })
238
+ }
222
239
  window.addEventListener("error", (errorEvent) => {
223
240
  if (!errorEvent.isTrusted) {
224
241
  // ignore custom error event (not sent by browser)
225
242
  return
226
243
  }
227
- const { error } = errorEvent
228
- displayErrorInDocument(error, {
229
- rootDirectoryUrl,
230
- openInEditor,
231
- url: errorEvent.filename,
232
- line: errorEvent.lineno,
233
- column: errorEvent.colno,
234
- reportedBy: "browser",
244
+ const { error, filename, lineno, colno } = errorEvent
245
+ onErrorReportedByBrowser(error, {
246
+ url: filename,
247
+ line: lineno,
248
+ column: colno,
235
249
  })
236
250
  })
237
- if (window.__server_events__) {
238
- const isExecuting = () => {
239
- if (pendingExecutionCount > 0) {
240
- return true
241
- }
242
- if (
243
- document.readyState === "loading" ||
244
- document.readyState === "interactive"
245
- ) {
246
- return true
247
- }
248
- if (window.__reloader__ && window.__reloader__.status === "reloading") {
249
- return true
250
- }
251
- return false
252
- }
253
-
254
- window.__server_events__.addEventCallbacks({
255
- error_while_serving_file: (serverErrorEvent) => {
256
- if (!isExecuting()) {
257
- return
258
- }
259
- const {
260
- message,
261
- stack,
262
- traceUrl,
263
- traceLine,
264
- traceColumn,
265
- traceMessage,
266
- requestedRessource,
267
- isFaviconAutoRequest,
268
- } = JSON.parse(serverErrorEvent.data)
269
- if (isFaviconAutoRequest) {
270
- return
271
- }
272
- // setTimeout is to ensure the error
273
- // dispatched on window by browser is displayed first,
274
- // then the server error replaces it (because it contains more information)
275
- setTimeout(() => {
276
- displayErrorInDocument(
277
- {
278
- message,
279
- stack:
280
- stack && traceMessage
281
- ? `${stack}\n\n${traceMessage}`
282
- : stack
283
- ? stack
284
- : traceMessage
285
- ? `\n${traceMessage}`
286
- : "",
287
- },
288
- {
289
- rootDirectoryUrl,
290
- openInEditor,
291
- url: traceUrl,
292
- line: traceLine,
293
- column: traceColumn,
294
- reportedBy: "server",
295
- requestedRessource,
296
- },
297
- )
298
- }, 10)
299
- },
300
- })
301
- }
302
251
  }
303
252
  }
304
253