@jsenv/core 27.3.4 → 27.4.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,73 +1,269 @@
1
- export const displayErrorInDocument = (error) => {
2
- const title = "An error occured"
3
- let theme =
4
- error && error.cause && error.cause.code === "PARSE_ERROR"
5
- ? "light"
6
- : "dark"
7
- let message = errorToHTML(error)
8
- const css = `
9
- .jsenv-console {
10
- background: rgba(0, 0, 0, 0.95);
11
- position: absolute;
12
- top: 0;
13
- left: 0;
14
- width: 100%;
15
- height: 100%;
16
- display: flex;
17
- flex-direction: column;
18
- align-items: center;
19
- z-index: 1000;
20
- box-sizing: border-box;
21
- padding: 1em;
22
- }
1
+ const JSENV_ERROR_OVERLAY_TAGNAME = "jsenv-error-overlay"
23
2
 
24
- .jsenv-console h1 {
25
- color: red;
26
- display: flex;
27
- align-items: center;
28
- }
3
+ export const displayErrorInDocument = (
4
+ error,
5
+ { rootDirectoryUrl, url, line, column },
6
+ ) => {
7
+ document.querySelectorAll(JSENV_ERROR_OVERLAY_TAGNAME).forEach((node) => {
8
+ node.parentNode.removeChild(node)
9
+ })
10
+ const { theme, title, message, stack } = errorToHTML(error, {
11
+ url,
12
+ line,
13
+ column,
14
+ })
15
+ const jsenvErrorOverlay = new JsenvErrorOverlay({
16
+ theme,
17
+ title,
18
+ stack: stack
19
+ ? `${replaceLinks(message, { rootDirectoryUrl })}\n${replaceLinks(stack, {
20
+ rootDirectoryUrl,
21
+ })}`
22
+ : replaceLinks(message, { rootDirectoryUrl }),
23
+ })
24
+ document.body.appendChild(jsenvErrorOverlay)
25
+ }
29
26
 
30
- #button-close-jsenv-console {
31
- margin-left: 10px;
27
+ class JsenvErrorOverlay extends HTMLElement {
28
+ constructor({ title, stack, theme = "dark" }) {
29
+ super()
30
+ this.root = this.attachShadow({ mode: "open" })
31
+ this.root.innerHTML = overlayHtml
32
+ this.root.querySelector(".overlay").setAttribute("data-theme", theme)
33
+ this.root.querySelector(".title").innerHTML = title
34
+ this.root.querySelector(".stack").innerHTML = stack
35
+ this.root.querySelector(".backdrop").onclick = () => {
36
+ if (!this.parentNode) {
37
+ // not in document anymore
38
+ return
39
+ }
40
+ this.root.querySelector(".backdrop").onclick = null
41
+ this.parentNode.removeChild(this)
32
42
  }
43
+ }
44
+ }
33
45
 
34
- .jsenv-console pre {
35
- overflow: auto;
36
- max-width: 70em;
37
- /* avoid scrollbar to hide the text behind it */
38
- padding: 20px;
39
- }
46
+ if (customElements && !customElements.get(JSENV_ERROR_OVERLAY_TAGNAME)) {
47
+ customElements.define(JSENV_ERROR_OVERLAY_TAGNAME, JsenvErrorOverlay)
48
+ }
40
49
 
41
- .jsenv-console pre[data-theme="dark"] {
42
- background: #111;
43
- border: 1px solid #333;
44
- color: #eee;
45
- }
50
+ const overlayHtml = `
51
+ <style>
52
+ :host {
53
+ position: fixed;
54
+ z-index: 99999;
55
+ top: 0;
56
+ left: 0;
57
+ width: 100%;
58
+ height: 100%;
59
+ overflow-y: scroll;
60
+ margin: 0;
61
+ background: rgba(0, 0, 0, 0.66);
62
+ }
63
+
64
+ .backdrop {
65
+ position: absolute;
66
+ left: 0;
67
+ right: 0;
68
+ top: 0;
69
+ bottom: 0;
70
+ }
71
+
72
+ .overlay {
73
+ position: relative;
74
+ background: rgba(0, 0, 0, 0.95);
75
+ width: 800px;
76
+ margin: 30px auto;
77
+ padding: 25px 40px;
78
+ padding-top: 0;
79
+ overflow: hidden; /* for h1 margins */
80
+ border-radius: 4px 8px;
81
+ box-shadow: 0 20px 40px rgb(0 0 0 / 30%), 0 15px 12px rgb(0 0 0 / 20%);
82
+ box-sizing: border-box;
83
+ font-family: monospace;
84
+ direction: ltr;
85
+ }
46
86
 
47
- .jsenv-console pre[data-theme="light"] {
48
- background: #1E1E1E;
49
- border: 1px solid white;
50
- color: #EEEEEE;
87
+ h1 {
88
+ color: red;
89
+ text-align: center;
90
+ }
91
+
92
+ pre {
93
+ overflow: auto;
94
+ max-width: 100%;
95
+ /* padding is nice + prevents scrollbar from hiding the text behind it */
96
+ /* does not work nicely on firefox though https://bugzilla.mozilla.org/show_bug.cgi?id=748518 */
97
+ padding: 20px;
98
+ }
99
+
100
+ .tip {
101
+ border-top: 1px solid #999;
102
+ padding-top: 12px;
103
+ }
104
+
105
+ [data-theme="dark"] {
106
+ color: #999;
107
+ }
108
+ [data-theme="dark"] pre {
109
+ background: #111;
110
+ border: 1px solid #333;
111
+ color: #eee;
112
+ }
113
+
114
+ [data-theme="light"] {
115
+ color: #EEEEEE;
116
+ }
117
+ [data-theme="light"] pre {
118
+ background: #1E1E1E;
119
+ border: 1px solid white;
120
+ color: #EEEEEE;
121
+ }
122
+
123
+ pre a {
124
+ color: inherit;
125
+ }
126
+ </style>
127
+ <div class="backdrop"></div>
128
+ <div class="overlay">
129
+ <h1 class="title"></h1>
130
+ <pre class="stack"></pre>
131
+ <div class="tip">Click outside to close.</div>
132
+ </div>
133
+ `
134
+
135
+ const parseErrorInfo = (error) => {
136
+ if (error === undefined) {
137
+ return {
138
+ message: "undefined",
139
+ }
140
+ }
141
+ if (error === null) {
142
+ return {
143
+ message: "null",
144
+ }
145
+ }
146
+ if (typeof error === "string") {
147
+ return {
148
+ message: error,
149
+ }
150
+ }
151
+ if (error instanceof Error) {
152
+ if (error.name === "SyntaxError") {
153
+ return {
154
+ message: error.message,
155
+ }
156
+ }
157
+ if (error.cause && error.cause.code === "PARSE_ERROR") {
158
+ if (error.messageHTML) {
159
+ return {
160
+ message: error.messageHTML,
161
+ }
162
+ }
163
+ return {
164
+ message: error.message,
165
+ }
51
166
  }
167
+ // stackTrace formatted by V8
168
+ if (Error.captureStackTrace) {
169
+ return {
170
+ message: error.message,
171
+ stack: getErrorStackWithoutErrorMessage(error),
172
+ }
173
+ }
174
+ return {
175
+ message: error.message,
176
+ stack: error.stack ? ` ${error.stack}` : null,
177
+ }
178
+ }
179
+ if (typeof error === "object") {
180
+ return error
181
+ }
182
+ return {
183
+ message: JSON.stringify(error),
184
+ }
185
+ }
52
186
 
53
- .jsenv-console pre a {
54
- color: inherit;
187
+ const getErrorStackWithoutErrorMessage = (error) => {
188
+ let stack = error.stack
189
+ const messageInStack = `${error.name}: ${error.message}`
190
+ if (stack.startsWith(messageInStack)) {
191
+ stack = stack.slice(messageInStack.length)
192
+ }
193
+ const nextLineIndex = stack.indexOf("\n")
194
+ if (nextLineIndex > -1) {
195
+ stack = stack.slice(nextLineIndex + 1)
196
+ }
197
+ return stack
198
+ }
199
+
200
+ const errorToHTML = (error, { url, line, column }) => {
201
+ let { message, stack } = parseErrorInfo(error)
202
+ if (url) {
203
+ if (!stack || (error && error.name === "SyntaxError")) {
204
+ stack = ` at ${appendLineAndColumn(url, { line, column })}`
55
205
  }
56
- `
57
- const html = `
58
- <style type="text/css">${css}></style>
59
- <div class="jsenv-console">
60
- <h1>${title} <button id="button-close-jsenv-console">X</button></h1>
61
- <pre data-theme="${theme}">${message}</pre>
62
- </div>
63
- `
64
- const removeJsenvConsole = appendHMTLInside(html, document.body)
65
-
66
- document.querySelector("#button-close-jsenv-console").onclick = () => {
67
- removeJsenvConsole()
206
+ }
207
+ return {
208
+ theme:
209
+ error && error.cause && error.cause.code === "PARSE_ERROR"
210
+ ? "light"
211
+ : "dark",
212
+ title: "An error occured",
213
+ message,
214
+ stack,
68
215
  }
69
216
  }
70
217
 
218
+ const replaceLinks = (string, { rootDirectoryUrl }) => {
219
+ // normalize line breaks
220
+ string = string.replace(/\n/g, "\n")
221
+ string = escapeHtml(string)
222
+ // render links
223
+ string = stringToStringWithLink(string, {
224
+ transform: (url, { line, column }) => {
225
+ const urlObject = new URL(url)
226
+
227
+ const onFileUrl = (fileUrlObject) => {
228
+ const atFsIndex = fileUrlObject.pathname.indexOf("/@fs/")
229
+ let fileUrl
230
+ if (atFsIndex > -1) {
231
+ const afterAtFs = fileUrlObject.pathname.slice(
232
+ atFsIndex + "/@fs/".length,
233
+ )
234
+ fileUrl = new URL(afterAtFs, "file:///").href
235
+ } else {
236
+ fileUrl = fileUrlObject.href
237
+ }
238
+ fileUrl = appendLineAndColumn(fileUrl, {
239
+ line,
240
+ column,
241
+ })
242
+ return link({
243
+ href: `javascript:window.fetch('/__open_in_editor__/${fileUrl}')`,
244
+ text: fileUrl,
245
+ })
246
+ }
247
+
248
+ if (urlObject.origin === window.origin) {
249
+ const fileUrlObject = new URL(
250
+ `${urlObject.pathname.slice(1)}${urlObject.search}`,
251
+ rootDirectoryUrl,
252
+ )
253
+ return onFileUrl(fileUrlObject)
254
+ }
255
+ if (urlObject.href.startsWith("file:")) {
256
+ return onFileUrl(urlObject)
257
+ }
258
+ return link({
259
+ href: url,
260
+ text: appendLineAndColumn(url, { line, column }),
261
+ })
262
+ },
263
+ })
264
+ return string
265
+ }
266
+
71
267
  const escapeHtml = (string) => {
72
268
  return string
73
269
  .replace(/&/g, "&amp;")
@@ -77,36 +273,14 @@ const escapeHtml = (string) => {
77
273
  .replace(/'/g, "&#039;")
78
274
  }
79
275
 
80
- const errorToHTML = (error) => {
81
- let html
82
-
83
- if (error && error instanceof Error) {
84
- if (error.cause && error.cause.code === "PARSE_ERROR") {
85
- html = error.messageHTML || escapeHtml(error.message)
86
- }
87
- // stackTrace formatted by V8
88
- else if (Error.captureStackTrace) {
89
- html = escapeHtml(error.stack)
90
- } else {
91
- // other stack trace such as firefox do not contain error.message
92
- html = escapeHtml(`${error.message}
93
- ${error.stack}`)
94
- }
95
- } else if (typeof error === "string") {
96
- html = error
97
- } else if (error === undefined) {
98
- html = "undefined"
99
- } else {
100
- html = JSON.stringify(error)
276
+ const appendLineAndColumn = (url, { line, column }) => {
277
+ if (line !== undefined && column !== undefined) {
278
+ return `${url}:${line}:${column}`
101
279
  }
102
-
103
- const htmlWithCorrectLineBreaks = html.replace(/\n/g, "\n")
104
- const htmlWithLinks = stringToStringWithLink(htmlWithCorrectLineBreaks, {
105
- transform: (url) => {
106
- return { href: url, text: url }
107
- },
108
- })
109
- return htmlWithLinks
280
+ if (line !== undefined) {
281
+ return `${url}:${line}`
282
+ }
283
+ return url
110
284
  }
111
285
 
112
286
  // `Error: yo
@@ -144,28 +318,23 @@ const stringToStringWithLink = (
144
318
  const lineAndColumnString = lineAndColumMatch[0]
145
319
  const lineNumber = lineAndColumMatch[1]
146
320
  const columnNumber = lineAndColumMatch[2]
147
- const url = match.slice(0, -lineAndColumnString.length)
148
- const { href, text } = transform(url)
149
- linkHTML = link({ href, text: `${text}:${lineNumber}:${columnNumber}` })
321
+ linkHTML = transform(match.slice(0, -lineAndColumnString.length), {
322
+ line: lineNumber,
323
+ column: columnNumber,
324
+ })
150
325
  } else {
151
326
  const linePattern = /:([0-9]+)$/
152
327
  const lineMatch = match.match(linePattern)
153
328
  if (lineMatch) {
154
329
  const lineString = lineMatch[0]
155
330
  const lineNumber = lineMatch[1]
156
- const url = match.slice(0, -lineString.length)
157
- const { href, text } = transform(url)
158
- linkHTML = link({
159
- href,
160
- text: `${text}:${lineNumber}`,
331
+ linkHTML = transform(match.slice(0, -lineString.length), {
332
+ line: lineNumber,
161
333
  })
162
334
  } else {
163
- const url = match
164
- const { href, text } = transform(url)
165
- linkHTML = link({ href, text })
335
+ linkHTML = transform(match, {})
166
336
  }
167
337
  }
168
-
169
338
  if (endsWithSeparationChar) {
170
339
  return `${linkHTML}${lastChar}`
171
340
  }
@@ -174,25 +343,3 @@ const stringToStringWithLink = (
174
343
  }
175
344
 
176
345
  const link = ({ href, text = href }) => `<a href="${href}">${text}</a>`
177
-
178
- const appendHMTLInside = (html, parentNode) => {
179
- const temoraryParent = document.createElement("div")
180
- temoraryParent.innerHTML = html
181
- return transferChildren(temoraryParent, parentNode)
182
- }
183
-
184
- const transferChildren = (fromNode, toNode) => {
185
- const childNodes = [].slice.call(fromNode.childNodes, 0)
186
- let i = 0
187
- while (i < childNodes.length) {
188
- toNode.appendChild(childNodes[i])
189
- i++
190
- }
191
- return () => {
192
- let c = 0
193
- while (c < childNodes.length) {
194
- fromNode.appendChild(childNodes[c])
195
- c++
196
- }
197
- }
198
- }
@@ -4,7 +4,11 @@ import { displayErrorNotification } from "./error_in_notification.js"
4
4
 
5
5
  const { __html_supervisor__ } = window
6
6
 
7
- export const installHtmlSupervisor = ({ logs, measurePerf }) => {
7
+ export const installHtmlSupervisor = ({
8
+ logs,
9
+ measurePerf,
10
+ rootDirectoryUrl,
11
+ }) => {
8
12
  const errorTransformer = null // could implement error stack remapping if needed
9
13
  const scriptExecutionResults = {}
10
14
  let collectCalled = false
@@ -35,14 +39,14 @@ export const installHtmlSupervisor = ({ logs, measurePerf }) => {
35
39
  {
36
40
  currentScript,
37
41
  errorExposureInNotification = false,
38
- errorExposureInDocument = true,
42
+ errorExposureInDocument = false,
39
43
  },
40
44
  ) => {
41
45
  const error = executionResult.error
42
46
  if (error && error.code === "NETWORK_FAILURE") {
43
47
  if (currentScript) {
44
- const errorEvent = new Event("error")
45
- currentScript.dispatchEvent(errorEvent)
48
+ const currentScriptErrorEvent = new Event("error")
49
+ currentScript.dispatchEvent(currentScriptErrorEvent)
46
50
  }
47
51
  } else if (typeof error === "object") {
48
52
  const globalErrorEvent = new Event("error")
@@ -56,7 +60,7 @@ export const installHtmlSupervisor = ({ logs, measurePerf }) => {
56
60
  displayErrorNotification(error)
57
61
  }
58
62
  if (errorExposureInDocument) {
59
- displayErrorInDocument(error)
63
+ displayErrorInDocument(error, { rootDirectoryUrl })
60
64
  }
61
65
  executionResult.exceptionSource = unevalException(error)
62
66
  delete executionResult.error
@@ -202,6 +206,44 @@ export const installHtmlSupervisor = ({ logs, measurePerf }) => {
202
206
  copy.forEach((scriptToExecute) => {
203
207
  __html_supervisor__.addScriptToExecute(scriptToExecute)
204
208
  })
209
+
210
+ window.addEventListener("error", (errorEvent) => {
211
+ if (!errorEvent.isTrusted) {
212
+ // ignore custom error event (not sent by browser)
213
+ return
214
+ }
215
+ const { error } = errorEvent
216
+ displayErrorInDocument(error, {
217
+ rootDirectoryUrl,
218
+ url: errorEvent.filename,
219
+ line: errorEvent.lineno,
220
+ column: errorEvent.colno,
221
+ })
222
+ })
223
+ if (window.__jsenv_event_source_client__) {
224
+ const onServerErrorEvent = (serverErrorEvent) => {
225
+ const { reason, stack, url, line, column, contentFrame } = JSON.parse(
226
+ serverErrorEvent.data,
227
+ )
228
+ displayErrorInDocument(
229
+ {
230
+ message: reason,
231
+ stack: stack ? `${stack}\n\n${contentFrame}` : contentFrame,
232
+ },
233
+ {
234
+ rootDirectoryUrl,
235
+ url,
236
+ line,
237
+ column,
238
+ },
239
+ )
240
+ }
241
+ window.__jsenv_event_source_client__.addEventCallbacks({
242
+ file_not_found: onServerErrorEvent,
243
+ parse_error: onServerErrorEvent,
244
+ unexpected_error: onServerErrorEvent,
245
+ })
246
+ }
205
247
  }
206
248
 
207
249
  export const superviseScriptTypeModule = ({ src, isInline }) => {
@@ -4,6 +4,7 @@
4
4
  * - scripts are wrapped to be supervised
5
5
  */
6
6
 
7
+ import { fileURLToPath } from "node:url"
7
8
  import {
8
9
  parseHtmlString,
9
10
  stringifyHtmlAst,
@@ -20,6 +21,8 @@ import {
20
21
  } from "@jsenv/ast"
21
22
  import { generateInlineContentUrl } from "@jsenv/urls"
22
23
 
24
+ import { requireFromJsenv } from "@jsenv/core/src/require_from_jsenv.js"
25
+
23
26
  export const jsenvPluginHtmlSupervisor = ({
24
27
  logs = false,
25
28
  measurePerf = false,
@@ -40,8 +43,27 @@ export const jsenvPluginHtmlSupervisor = ({
40
43
  dev: true,
41
44
  test: true,
42
45
  },
46
+ serve: (request) => {
47
+ if (!request.ressource.startsWith("/__open_in_editor__/")) {
48
+ return null
49
+ }
50
+ const file = request.ressource.slice("/__open_in_editor__/".length)
51
+ if (!file) {
52
+ return {
53
+ status: 400,
54
+ body: 'Missing "file" in url search params',
55
+ }
56
+ }
57
+ const launch = requireFromJsenv("launch-editor")
58
+ launch(fileURLToPath(file), () => {
59
+ // ignore error for now
60
+ })
61
+ return {
62
+ status: 200,
63
+ }
64
+ },
43
65
  transformUrlContent: {
44
- html: ({ url, content }, { referenceUtils }) => {
66
+ html: ({ url, content }, context) => {
45
67
  const htmlAst = parseHtmlString(content)
46
68
  const scriptsToSupervise = []
47
69
 
@@ -59,7 +81,7 @@ export const jsenvPluginHtmlSupervisor = ({
59
81
  lineEnd,
60
82
  columnEnd,
61
83
  })
62
- const [inlineScriptReference] = referenceUtils.foundInline({
84
+ const [inlineScriptReference] = context.referenceUtils.foundInline({
63
85
  type: "script_src",
64
86
  expectedType: { classic: "js_classic", module: "js_module" }[
65
87
  scriptCategory
@@ -128,11 +150,12 @@ export const jsenvPluginHtmlSupervisor = ({
128
150
  }
129
151
  },
130
152
  })
131
- const [htmlSupervisorInstallerFileReference] = referenceUtils.inject({
132
- type: "js_import_export",
133
- expectedType: "js_module",
134
- specifier: htmlSupervisorInstallerFileUrl,
135
- })
153
+ const [htmlSupervisorInstallerFileReference] =
154
+ context.referenceUtils.inject({
155
+ type: "js_import_export",
156
+ expectedType: "js_module",
157
+ specifier: htmlSupervisorInstallerFileUrl,
158
+ })
136
159
  injectScriptNodeAsEarlyAsPossible(
137
160
  htmlAst,
138
161
  createHtmlNode({
@@ -146,6 +169,7 @@ export const jsenvPluginHtmlSupervisor = ({
146
169
  {
147
170
  logs,
148
171
  measurePerf,
172
+ rootDirectoryUrl: context.rootDirectoryUrl,
149
173
  },
150
174
  null,
151
175
  " ",
@@ -153,11 +177,12 @@ export const jsenvPluginHtmlSupervisor = ({
153
177
  "injected-by": "jsenv:html_supervisor",
154
178
  }),
155
179
  )
156
- const [htmlSupervisorSetupFileReference] = referenceUtils.inject({
157
- type: "script_src",
158
- expectedType: "js_classic",
159
- specifier: htmlSupervisorSetupFileUrl,
160
- })
180
+ const [htmlSupervisorSetupFileReference] =
181
+ context.referenceUtils.inject({
182
+ type: "script_src",
183
+ expectedType: "js_classic",
184
+ specifier: htmlSupervisorSetupFileUrl,
185
+ })
161
186
  injectScriptNodeAsEarlyAsPossible(
162
187
  htmlAst,
163
188
  createHtmlNode({
@@ -83,8 +83,6 @@ export const getCorePlugins = ({
83
83
  ? [
84
84
  jsenvPluginAutoreload({
85
85
  ...clientAutoreload,
86
- rootDirectoryUrl,
87
- urlGraph,
88
86
  scenario,
89
87
  clientFileChangeCallbackList,
90
88
  clientFilesPruneCallbackList,
@@ -79,7 +79,9 @@ export const jsenvPluginUrlAnalysis = ({
79
79
  type: "filesystem",
80
80
  subtype: "directory_entry",
81
81
  specifier: directoryEntryName,
82
- trace: `"${directoryRelativeUrl}${directoryEntryName}" entry in directory referenced by ${originalDirectoryReference.trace}`,
82
+ trace: {
83
+ message: `"${directoryRelativeUrl}${directoryEntryName}" entry in directory referenced by ${originalDirectoryReference.trace.message}`,
84
+ },
83
85
  })
84
86
  })
85
87
  },