@jsenv/core 27.5.1 → 27.5.2

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,43 @@
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,
14
+ codeFrame,
11
15
  reportedBy,
12
16
  requestedRessource,
13
17
  },
14
18
  ) => {
15
- document.querySelectorAll(JSENV_ERROR_OVERLAY_TAGNAME).forEach((node) => {
16
- node.parentNode.removeChild(node)
17
- })
18
- const { theme, title, message, stack, tip } = errorToHTML(error, {
19
+ const { theme, title, text, codeFramePromise, tip } = formatError(error, {
20
+ rootDirectoryUrl,
21
+ errorBaseUrl,
22
+ openInEditor,
19
23
  url,
20
24
  line,
21
25
  column,
26
+ codeFrame,
22
27
  reportedBy,
23
28
  requestedRessource,
24
29
  })
30
+
25
31
  let jsenvErrorOverlay = new JsenvErrorOverlay({
26
32
  theme,
27
33
  title,
28
- text: createErrorText({ rootDirectoryUrl, openInEditor, message, stack }),
34
+ text,
35
+ codeFramePromise,
29
36
  tip,
30
37
  })
38
+ document.querySelectorAll(JSENV_ERROR_OVERLAY_TAGNAME).forEach((node) => {
39
+ node.parentNode.removeChild(node)
40
+ })
31
41
  document.body.appendChild(jsenvErrorOverlay)
32
42
  const removeErrorOverlay = () => {
33
43
  if (jsenvErrorOverlay && jsenvErrorOverlay.parentNode) {
@@ -45,29 +55,24 @@ export const displayErrorInDocument = (
45
55
  return removeErrorOverlay
46
56
  }
47
57
 
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
58
  class JsenvErrorOverlay extends HTMLElement {
67
- constructor({ theme, title, text, tip }) {
59
+ constructor({ theme, title, text, codeFramePromise, tip }) {
68
60
  super()
69
61
  this.root = this.attachShadow({ mode: "open" })
70
- this.root.innerHTML = overlayHtml
62
+ this.root.innerHTML = `
63
+ <style>
64
+ ${overlayCSS}
65
+ </style>
66
+ <div class="backdrop"></div>
67
+ <div class="overlay" data-theme=${theme}>
68
+ <h1 class="title">
69
+ ${title}
70
+ </h1>
71
+ <pre class="text">${text}</pre>
72
+ <div class="tip">
73
+ ${tip}
74
+ </div>
75
+ </div>`
71
76
  this.root.querySelector(".backdrop").onclick = () => {
72
77
  if (!this.parentNode) {
73
78
  // not in document anymore
@@ -76,10 +81,13 @@ class JsenvErrorOverlay extends HTMLElement {
76
81
  this.root.querySelector(".backdrop").onclick = null
77
82
  this.parentNode.removeChild(this)
78
83
  }
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
84
+ if (codeFramePromise) {
85
+ codeFramePromise.then((codeFrame) => {
86
+ if (this.parentNode) {
87
+ this.root.querySelector(".text").innerHTML += `\n\n${codeFrame}`
88
+ }
89
+ })
90
+ }
83
91
  }
84
92
  }
85
93
 
@@ -87,8 +95,7 @@ if (customElements && !customElements.get(JSENV_ERROR_OVERLAY_TAGNAME)) {
87
95
  customElements.define(JSENV_ERROR_OVERLAY_TAGNAME, JsenvErrorOverlay)
88
96
  }
89
97
 
90
- const overlayHtml = `
91
- <style>
98
+ const overlayCSS = `
92
99
  :host {
93
100
  position: fixed;
94
101
  z-index: 99999;
@@ -162,240 +169,4 @@ pre {
162
169
 
163
170
  pre a {
164
171
  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>`
172
+ }`
@@ -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
@@ -227,6 +228,7 @@ export const installHtmlSupervisor = ({
227
228
  const { error } = errorEvent
228
229
  displayErrorInDocument(error, {
229
230
  rootDirectoryUrl,
231
+ errorBaseUrl,
230
232
  openInEditor,
231
233
  url: errorEvent.filename,
232
234
  line: errorEvent.lineno,
@@ -276,21 +278,16 @@ export const installHtmlSupervisor = ({
276
278
  displayErrorInDocument(
277
279
  {
278
280
  message,
279
- stack:
280
- stack && traceMessage
281
- ? `${stack}\n\n${traceMessage}`
282
- : stack
283
- ? stack
284
- : traceMessage
285
- ? `\n${traceMessage}`
286
- : "",
281
+ stack,
287
282
  },
288
283
  {
289
284
  rootDirectoryUrl,
285
+ errorBaseUrl,
290
286
  openInEditor,
291
287
  url: traceUrl,
292
288
  line: traceLine,
293
289
  column: traceColumn,
290
+ codeFrame: traceMessage,
294
291
  reportedBy: "server",
295
292
  requestedRessource,
296
293
  },
@@ -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,