@jsenv/core 27.5.0 → 27.5.3

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,33 +1,64 @@
1
+ import { formatError } from "./error_formatter.js"
2
+
1
3
  const JSENV_ERROR_OVERLAY_TAGNAME = "jsenv-error-overlay"
2
4
 
5
+ let previousErrorInfo = null
6
+
3
7
  export const displayErrorInDocument = (
4
8
  error,
5
9
  {
6
10
  rootDirectoryUrl,
11
+ errorBaseUrl,
7
12
  openInEditor,
8
13
  url,
9
14
  line,
10
15
  column,
16
+ codeFrame,
11
17
  reportedBy,
12
18
  requestedRessource,
13
19
  },
14
20
  ) => {
15
- document.querySelectorAll(JSENV_ERROR_OVERLAY_TAGNAME).forEach((node) => {
16
- node.parentNode.removeChild(node)
17
- })
18
- const { theme, title, message, stack, tip } = errorToHTML(error, {
21
+ const nowMs = Date.now()
22
+ // ensure error dispatched on window by browser is displayed first
23
+ // then the server error replaces it (because it contains more information)
24
+ if (previousErrorInfo) {
25
+ const previousErrorReportedBy = previousErrorInfo.reportedBy
26
+ const msEllapsedSincePreviousError = nowMs - previousErrorInfo.ms
27
+ if (
28
+ previousErrorReportedBy === "server" &&
29
+ reportedBy === "browser" &&
30
+ msEllapsedSincePreviousError < 50
31
+ ) {
32
+ return () => {}
33
+ }
34
+ }
35
+ previousErrorInfo = {
36
+ ms: nowMs,
37
+ reportedBy,
38
+ }
39
+
40
+ const { theme, title, text, codeFramePromise, tip } = formatError(error, {
41
+ rootDirectoryUrl,
42
+ errorBaseUrl,
43
+ openInEditor,
19
44
  url,
20
45
  line,
21
46
  column,
47
+ codeFrame,
22
48
  reportedBy,
23
49
  requestedRessource,
24
50
  })
51
+
25
52
  let jsenvErrorOverlay = new JsenvErrorOverlay({
26
53
  theme,
27
54
  title,
28
- text: createErrorText({ rootDirectoryUrl, openInEditor, message, stack }),
55
+ text,
56
+ codeFramePromise,
29
57
  tip,
30
58
  })
59
+ document.querySelectorAll(JSENV_ERROR_OVERLAY_TAGNAME).forEach((node) => {
60
+ node.parentNode.removeChild(node)
61
+ })
31
62
  document.body.appendChild(jsenvErrorOverlay)
32
63
  const removeErrorOverlay = () => {
33
64
  if (jsenvErrorOverlay && jsenvErrorOverlay.parentNode) {
@@ -45,29 +76,24 @@ export const displayErrorInDocument = (
45
76
  return removeErrorOverlay
46
77
  }
47
78
 
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
79
  class JsenvErrorOverlay extends HTMLElement {
67
- constructor({ theme, title, text, tip }) {
80
+ constructor({ theme, title, text, codeFramePromise, tip }) {
68
81
  super()
69
82
  this.root = this.attachShadow({ mode: "open" })
70
- this.root.innerHTML = overlayHtml
83
+ this.root.innerHTML = `
84
+ <style>
85
+ ${overlayCSS}
86
+ </style>
87
+ <div class="backdrop"></div>
88
+ <div class="overlay" data-theme=${theme}>
89
+ <h1 class="title">
90
+ ${title}
91
+ </h1>
92
+ <pre class="text">${text}</pre>
93
+ <div class="tip">
94
+ ${tip}
95
+ </div>
96
+ </div>`
71
97
  this.root.querySelector(".backdrop").onclick = () => {
72
98
  if (!this.parentNode) {
73
99
  // not in document anymore
@@ -76,10 +102,13 @@ class JsenvErrorOverlay extends HTMLElement {
76
102
  this.root.querySelector(".backdrop").onclick = null
77
103
  this.parentNode.removeChild(this)
78
104
  }
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
105
+ if (codeFramePromise) {
106
+ codeFramePromise.then((codeFrame) => {
107
+ if (this.parentNode) {
108
+ this.root.querySelector(".text").innerHTML += `\n\n${codeFrame}`
109
+ }
110
+ })
111
+ }
83
112
  }
84
113
  }
85
114
 
@@ -87,8 +116,7 @@ if (customElements && !customElements.get(JSENV_ERROR_OVERLAY_TAGNAME)) {
87
116
  customElements.define(JSENV_ERROR_OVERLAY_TAGNAME, JsenvErrorOverlay)
88
117
  }
89
118
 
90
- const overlayHtml = `
91
- <style>
119
+ const overlayCSS = `
92
120
  :host {
93
121
  position: fixed;
94
122
  z-index: 99999;
@@ -162,240 +190,4 @@ pre {
162
190
 
163
191
  pre a {
164
192
  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>`
193
+ }`
@@ -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
@@ -224,13 +225,14 @@ export const installHtmlSupervisor = ({
224
225
  // ignore custom error event (not sent by browser)
225
226
  return
226
227
  }
227
- const { error } = errorEvent
228
+ const { error, filename, lineno, colno } = errorEvent
228
229
  displayErrorInDocument(error, {
229
230
  rootDirectoryUrl,
231
+ errorBaseUrl,
230
232
  openInEditor,
231
- url: errorEvent.filename,
232
- line: errorEvent.lineno,
233
- column: errorEvent.colno,
233
+ url: filename,
234
+ line: lineno,
235
+ column: colno,
234
236
  reportedBy: "browser",
235
237
  })
236
238
  })
@@ -250,7 +252,6 @@ export const installHtmlSupervisor = ({
250
252
  }
251
253
  return false
252
254
  }
253
-
254
255
  window.__server_events__.addEventCallbacks({
255
256
  error_while_serving_file: (serverErrorEvent) => {
256
257
  if (!isExecuting()) {
@@ -269,33 +270,23 @@ export const installHtmlSupervisor = ({
269
270
  if (isFaviconAutoRequest) {
270
271
  return
271
272
  }
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)
273
+ displayErrorInDocument(
274
+ {
275
+ message,
276
+ stack,
277
+ },
278
+ {
279
+ rootDirectoryUrl,
280
+ errorBaseUrl,
281
+ openInEditor,
282
+ url: traceUrl,
283
+ line: traceLine,
284
+ column: traceColumn,
285
+ codeFrame: traceMessage,
286
+ reportedBy: "server",
287
+ requestedRessource,
288
+ },
289
+ )
299
290
  },
300
291
  })
301
292
  }
@@ -19,7 +19,7 @@ import {
19
19
  removeHtmlNodeText,
20
20
  setHtmlNodeText,
21
21
  } from "@jsenv/ast"
22
- import { generateInlineContentUrl } from "@jsenv/urls"
22
+ import { generateInlineContentUrl, stringifyUrlSite } from "@jsenv/urls"
23
23
 
24
24
  import { requireFromJsenv } from "@jsenv/core/src/require_from_jsenv.js"
25
25
 
@@ -28,6 +28,7 @@ export const jsenvPluginHtmlSupervisor = ({
28
28
  measurePerf = false,
29
29
  errorOverlay = true,
30
30
  openInEditor = true,
31
+ errorBaseUrl,
31
32
  }) => {
32
33
  const htmlSupervisorSetupFileUrl = new URL(
33
34
  "./client/html_supervisor_setup.js?js_classic",
@@ -45,24 +46,60 @@ export const jsenvPluginHtmlSupervisor = ({
45
46
  dev: true,
46
47
  test: true,
47
48
  },
48
- serve: (request) => {
49
- if (!request.ressource.startsWith("/__open_in_editor__/")) {
50
- return null
51
- }
52
- const file = request.ressource.slice("/__open_in_editor__/".length)
53
- if (!file) {
49
+ serve: (request, context) => {
50
+ if (request.ressource.startsWith("/__open_in_editor__/")) {
51
+ const file = request.ressource.slice("/__open_in_editor__/".length)
52
+ if (!file) {
53
+ return {
54
+ status: 400,
55
+ body: "Missing file in url",
56
+ }
57
+ }
58
+ const launch = requireFromJsenv("launch-editor")
59
+ launch(fileURLToPath(file), () => {
60
+ // ignore error for now
61
+ })
54
62
  return {
55
- status: 400,
56
- body: 'Missing "file" in url search params',
63
+ status: 200,
64
+ headers: {
65
+ "cache-control": "no-store",
66
+ },
57
67
  }
58
68
  }
59
- const launch = requireFromJsenv("launch-editor")
60
- launch(fileURLToPath(file), () => {
61
- // ignore error for now
62
- })
63
- return {
64
- status: 200,
69
+ if (request.ressource.startsWith("/__get_code_frame__/")) {
70
+ const url = request.ressource.slice("/__get_code_frame__/".length)
71
+ const match = url.match(/:([0-9]+):([0-9]+)$/)
72
+ if (!match) {
73
+ return {
74
+ status: 400,
75
+ body: "Missing line and column in url",
76
+ }
77
+ }
78
+ const file = url.slice(0, match.index)
79
+ const line = parseInt(match[1])
80
+ const column = parseInt(match[2])
81
+ const urlInfo = context.urlGraph.getUrlInfo(file)
82
+ if (!urlInfo) {
83
+ return {
84
+ status: 404,
85
+ }
86
+ }
87
+ const codeFrame = stringifyUrlSite({
88
+ url: file,
89
+ line,
90
+ column,
91
+ content: urlInfo.originalContent,
92
+ })
93
+ return {
94
+ status: 200,
95
+ headers: {
96
+ "content-type": "text/plain",
97
+ "content-length": Buffer.byteLength(codeFrame),
98
+ },
99
+ body: codeFrame,
100
+ }
65
101
  }
102
+ return null
66
103
  },
67
104
  transformUrlContent: {
68
105
  html: ({ url, content }, context) => {
@@ -173,6 +210,7 @@ export const jsenvPluginHtmlSupervisor = ({
173
210
  installHtmlSupervisor(${JSON.stringify(
174
211
  {
175
212
  rootDirectoryUrl: context.rootDirectoryUrl,
213
+ errorBaseUrl,
176
214
  logs,
177
215
  measurePerf,
178
216
  errorOverlay,
@@ -60,8 +60,7 @@ export const executeTestPlan = async ({
60
60
  cooldownBetweenExecutions = 0,
61
61
  gcBetweenExecutions = logMemoryHeapUsage,
62
62
 
63
- coverageEnabled = process.argv.includes("--cover") ||
64
- process.argv.includes("--coverage"),
63
+ coverageEnabled = process.argv.includes("--coverage"),
65
64
  coverageConfig = {
66
65
  "./src/": true,
67
66
  },