@jsenv/core 28.0.2 → 28.1.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.
Files changed (55) hide show
  1. package/dist/controllable_child_process.mjs +1 -2
  2. package/dist/controllable_worker_thread.mjs +1 -2
  3. package/dist/js/autoreload.js +25 -9
  4. package/dist/js/execute_using_dynamic_import.js +804 -1
  5. package/dist/js/script_type_module_supervisor.js +122 -0
  6. package/dist/js/supervisor.js +915 -0
  7. package/dist/main.js +432 -492
  8. package/package.json +13 -13
  9. package/readme.md +1 -1
  10. package/src/build/inject_global_version_mappings.js +3 -3
  11. package/src/dev/start_dev_server.js +2 -2
  12. package/src/execute/execute.js +1 -1
  13. package/src/execute/run.js +26 -38
  14. package/src/execute/runtimes/browsers/from_playwright.js +51 -77
  15. package/src/execute/runtimes/node/node_child_process.js +36 -36
  16. package/src/execute/runtimes/node/node_worker_thread.js +36 -36
  17. package/src/omega/kitchen.js +12 -9
  18. package/src/omega/omega_server.js +2 -2
  19. package/src/omega/server/file_service.js +2 -2
  20. package/src/omega/url_graph/url_info_transformations.js +8 -1
  21. package/src/plugins/autoreload/client/reload.js +20 -7
  22. package/src/plugins/autoreload/jsenv_plugin_autoreload_client.js +4 -4
  23. package/src/plugins/import_meta_hot/html_hot_dependencies.js +2 -2
  24. package/src/plugins/importmap/jsenv_plugin_importmap.js +5 -3
  25. package/src/plugins/inject_globals/inject_globals.js +3 -3
  26. package/src/plugins/inline/jsenv_plugin_data_urls.js +1 -1
  27. package/src/plugins/inline/jsenv_plugin_html_inline_content.js +10 -5
  28. package/src/plugins/plugins.js +5 -5
  29. package/src/plugins/server_events/jsenv_plugin_server_events_client_injection.js +4 -4
  30. package/src/plugins/supervisor/client/script_type_module_supervisor.js +99 -0
  31. package/src/plugins/supervisor/client/supervisor.js +915 -0
  32. package/src/plugins/{html_supervisor/jsenv_plugin_html_supervisor.js → supervisor/jsenv_plugin_supervisor.js} +128 -102
  33. package/src/plugins/toolbar/client/execution/toolbar_execution.js +1 -1
  34. package/src/plugins/toolbar/jsenv_plugin_toolbar.js +4 -4
  35. package/src/plugins/transpilation/as_js_classic/jsenv_plugin_as_js_classic.js +7 -5
  36. package/src/plugins/transpilation/as_js_classic/jsenv_plugin_as_js_classic_html.js +5 -4
  37. package/src/plugins/transpilation/babel/jsenv_plugin_babel.js +13 -7
  38. package/src/plugins/transpilation/babel/new_stylesheet/babel_plugin_new_stylesheet_as_jsenv_import.js +6 -4
  39. package/src/plugins/transpilation/jsenv_plugin_transpilation.js +4 -2
  40. package/src/plugins/url_analysis/html/html_urls.js +11 -10
  41. package/src/test/coverage/babel_plugin_instrument.js +1 -35
  42. package/src/test/coverage/empty_coverage_factory.js +1 -1
  43. package/src/test/execute_plan.js +7 -3
  44. package/src/test/execute_test_plan.js +2 -1
  45. package/src/test/logs_file_execution.js +49 -8
  46. package/dist/js/html_supervisor_installer.js +0 -1091
  47. package/dist/js/html_supervisor_setup.js +0 -89
  48. package/dist/js/uneval.js +0 -804
  49. package/src/plugins/html_supervisor/client/error_formatter.js +0 -426
  50. package/src/plugins/html_supervisor/client/error_in_notification.js +0 -21
  51. package/src/plugins/html_supervisor/client/error_overlay.js +0 -191
  52. package/src/plugins/html_supervisor/client/html_supervisor_installer.js +0 -315
  53. package/src/plugins/html_supervisor/client/html_supervisor_setup.js +0 -89
  54. package/src/plugins/html_supervisor/client/perf_browser.js +0 -17
  55. package/src/plugins/html_supervisor/client/uneval_exception.js +0 -8
@@ -0,0 +1,915 @@
1
+ window.__supervisor__ = (() => {
2
+ const notImplemented = () => {
3
+ throw new Error(`window.__supervisor__.setup() not called`)
4
+ }
5
+ const executionResults = {}
6
+ const supervisor = {
7
+ reportError: notImplemented,
8
+ superviseScript: notImplemented,
9
+ reloadSupervisedScript: notImplemented,
10
+ getDocumentExecutionResult: notImplemented,
11
+ executionResults,
12
+ }
13
+
14
+ let navigationStartTime
15
+ try {
16
+ navigationStartTime = window.performance.timing.navigationStart
17
+ } catch (e) {
18
+ navigationStartTime = Date.now()
19
+ }
20
+
21
+ supervisor.setupReportException = ({
22
+ rootDirectoryUrl,
23
+ errorNotification,
24
+ errorOverlay,
25
+ errorBaseUrl,
26
+ openInEditor,
27
+ }) => {
28
+ const DYNAMIC_IMPORT_FETCH_ERROR = "dynamic_import_fetch_error"
29
+ const DYNAMIC_IMPORT_EXPORT_MISSING = "dynamic_import_export_missing"
30
+ const DYNAMIC_IMPORT_SYNTAX_ERROR = "dynamic_import_syntax_error"
31
+
32
+ const createException = ({
33
+ reason,
34
+ reportedBy,
35
+ url,
36
+ line,
37
+ column,
38
+ } = {}) => {
39
+ const exception = {
40
+ reason,
41
+ reportedBy,
42
+ isError: false, // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/throw
43
+ code: null,
44
+ message: null,
45
+ stack: null,
46
+ stackFormatIsV8: null,
47
+ stackSourcemapped: null,
48
+ originalStack: null,
49
+ meta: null,
50
+ site: {
51
+ isInline: null,
52
+ url: null,
53
+ line: null,
54
+ column: null,
55
+ originalUrl: null,
56
+ },
57
+ }
58
+
59
+ const writeBasicProperties = () => {
60
+ if (reason === undefined) {
61
+ exception.message = "undefined"
62
+ return
63
+ }
64
+ if (reason === null) {
65
+ exception.message = "null"
66
+ return
67
+ }
68
+ if (typeof reason === "string") {
69
+ exception.message = reason
70
+ return
71
+ }
72
+ if (reason instanceof Error) {
73
+ const error = reason
74
+ let message = error.message
75
+ exception.isError = true
76
+ if (Error.captureStackTrace) {
77
+ // stackTrace formatted by V8
78
+ exception.message = message
79
+ exception.stack = getErrorStackWithoutErrorMessage(error)
80
+ exception.stackFormatIsV8 = true
81
+ exception.stackSourcemapped = true
82
+ } else {
83
+ exception.message = message
84
+ exception.stack = error.stack ? ` ${error.stack}` : null
85
+ exception.stackFormatIsV8 = false
86
+ exception.stackSourcemapped = false
87
+ }
88
+ if (error.reportedBy) {
89
+ exception.reportedBy = error.reportedBy
90
+ }
91
+ if (error.url) {
92
+ Object.assign(exception.site, resolveUrlSite({ url: error.url }))
93
+ }
94
+ if (error.needsReport) {
95
+ exception.needsReport = true
96
+ }
97
+ export_missing: {
98
+ // chrome
99
+ if (message.includes("does not provide an export named")) {
100
+ exception.code = DYNAMIC_IMPORT_EXPORT_MISSING
101
+ return
102
+ }
103
+ // firefox
104
+ if (message.startsWith("import not found:")) {
105
+ exception.code = DYNAMIC_IMPORT_EXPORT_MISSING
106
+ return
107
+ }
108
+ // safari
109
+ if (message.startsWith("import binding name")) {
110
+ exception.code = DYNAMIC_IMPORT_EXPORT_MISSING
111
+ return
112
+ }
113
+ }
114
+ js_syntax_error: {
115
+ if (error.name === "SyntaxError" && typeof line === "number") {
116
+ exception.code = DYNAMIC_IMPORT_SYNTAX_ERROR
117
+ return
118
+ }
119
+ }
120
+ return
121
+ }
122
+ if (typeof reason === "object") {
123
+ exception.code = reason.code
124
+ exception.message = reason.message
125
+ exception.stack = reason.stack
126
+ if (reason.reportedBy) {
127
+ exception.reportedBy = reason.reportedBy
128
+ }
129
+ if (reason.url) {
130
+ Object.assign(exception.site, resolveUrlSite({ url: reason.url }))
131
+ }
132
+ if (reason.needsReport) {
133
+ exception.needsReport = true
134
+ }
135
+ return
136
+ }
137
+ exception.message = JSON.stringify(reason)
138
+ }
139
+ writeBasicProperties()
140
+
141
+ // first create a version of the stack with file://
142
+ // (and use it to locate exception url+line+column)
143
+ if (exception.stack) {
144
+ exception.originalStack = exception.stack
145
+ exception.stack = replaceUrls(
146
+ exception.originalStack,
147
+ (serverUrlSite) => {
148
+ const fileUrlSite = resolveUrlSite(serverUrlSite)
149
+ if (exception.site.url === null) {
150
+ Object.assign(exception.site, fileUrlSite)
151
+ }
152
+ return stringifyUrlSite(fileUrlSite)
153
+ },
154
+ )
155
+ }
156
+ // then if it fails, use url+line+column passed
157
+ if (exception.site.url === null && url) {
158
+ if (typeof line === "string") {
159
+ line = parseInt(line)
160
+ }
161
+ if (typeof column === "string") {
162
+ column = parseInt(column)
163
+ }
164
+ const fileUrlSite = resolveUrlSite({ url, line, column })
165
+ if (
166
+ fileUrlSite.isInline &&
167
+ exception.code === DYNAMIC_IMPORT_SYNTAX_ERROR
168
+ ) {
169
+ // syntax error on inline script need line-1 for some reason
170
+ if (Error.captureStackTrace) {
171
+ fileUrlSite.line--
172
+ } else {
173
+ // firefox and safari need line-2
174
+ fileUrlSite.line -= 2
175
+ }
176
+ }
177
+ Object.assign(exception.site, fileUrlSite)
178
+ }
179
+ exception.text = stringifyMessageAndStack(exception)
180
+ return exception
181
+ }
182
+
183
+ const stringifyMessageAndStack = ({ message, stack }) => {
184
+ if (message && stack) {
185
+ return `${message}\n${stack}`
186
+ }
187
+ if (stack) {
188
+ return stack
189
+ }
190
+ return message
191
+ }
192
+
193
+ const stringifyUrlSite = ({ url, line, column }) => {
194
+ return typeof line === "number" && typeof column === "number"
195
+ ? `${url}:${line}:${column}`
196
+ : typeof line === "number"
197
+ ? `${url}:${line}`
198
+ : url
199
+ }
200
+
201
+ const resolveUrlSite = ({ url, line, column }) => {
202
+ const inlineUrlMatch = url.match(
203
+ /@L([0-9]+)C([0-9]+)\-L([0-9]+)C([0-9]+)(\.[\w]+)$/,
204
+ )
205
+ if (inlineUrlMatch) {
206
+ const htmlUrl = url.slice(0, inlineUrlMatch.index)
207
+ const tagLineStart = parseInt(inlineUrlMatch[1])
208
+ const tagColumnStart = parseInt(inlineUrlMatch[2])
209
+ const tagLineEnd = parseInt(inlineUrlMatch[3])
210
+ const tagColumnEnd = parseInt(inlineUrlMatch[4])
211
+ const extension = inlineUrlMatch[5]
212
+ url = htmlUrl
213
+ line = tagLineStart + (typeof line === "number" ? line : 0)
214
+ // stackTrace formatted by V8 (chrome)
215
+ if (Error.captureStackTrace) {
216
+ line--
217
+ }
218
+ column = tagColumnStart + (typeof column === "number" ? column : 0)
219
+ const fileUrl = resolveFileUrl(url)
220
+ return {
221
+ isInline: true,
222
+ serverUrl: url,
223
+ originalUrl: `${fileUrl}@L${tagLineStart}C${tagColumnStart}-L${tagLineEnd}C${tagColumnEnd}${extension}`,
224
+ url: fileUrl,
225
+ line,
226
+ column,
227
+ }
228
+ }
229
+ return {
230
+ isInline: false,
231
+ serverUrl: url,
232
+ url: resolveFileUrl(url),
233
+ line,
234
+ column,
235
+ }
236
+ }
237
+
238
+ const getErrorStackWithoutErrorMessage = (error) => {
239
+ let stack = error.stack
240
+ const messageInStack = `${error.name}: ${error.message}`
241
+ if (stack.startsWith(messageInStack)) {
242
+ stack = stack.slice(messageInStack.length)
243
+ }
244
+ const nextLineIndex = stack.indexOf("\n")
245
+ if (nextLineIndex > -1) {
246
+ stack = stack.slice(nextLineIndex + 1)
247
+ }
248
+ return stack
249
+ }
250
+
251
+ const resolveFileUrl = (url) => {
252
+ let urlObject = new URL(url)
253
+ if (urlObject.origin === window.origin) {
254
+ urlObject = new URL(
255
+ `${urlObject.pathname.slice(1)}${urlObject.search}`,
256
+ rootDirectoryUrl,
257
+ )
258
+ }
259
+ if (urlObject.href.startsWith("file:")) {
260
+ const atFsIndex = urlObject.pathname.indexOf("/@fs/")
261
+ if (atFsIndex > -1) {
262
+ const afterAtFs = urlObject.pathname.slice(atFsIndex + "/@fs/".length)
263
+ return new URL(afterAtFs, "file:///").href
264
+ }
265
+ }
266
+ return urlObject.href
267
+ }
268
+
269
+ // `Error: yo
270
+ // at Object.execute (http://127.0.0.1:57300/build/src/__test__/file-throw.js:9:13)
271
+ // at doExec (http://127.0.0.1:3000/src/__test__/file-throw.js:452:38)
272
+ // at postOrderExec (http://127.0.0.1:3000/src/__test__/file-throw.js:448:16)
273
+ // at http://127.0.0.1:3000/src/__test__/file-throw.js:399:18`.replace(/(?:https?|ftp|file):\/\/(.*+)$/gm, (...args) => {
274
+ // debugger
275
+ // })
276
+ const replaceUrls = (source, replace) => {
277
+ return source.replace(/(?:https?|ftp|file):\/\/\S+/gm, (match) => {
278
+ let replacement = ""
279
+ const lastChar = match[match.length - 1]
280
+
281
+ // hotfix because our url regex sucks a bit
282
+ const endsWithSeparationChar = lastChar === ")" || lastChar === ":"
283
+ if (endsWithSeparationChar) {
284
+ match = match.slice(0, -1)
285
+ }
286
+
287
+ const lineAndColumnPattern = /:([0-9]+):([0-9]+)$/
288
+ const lineAndColumMatch = match.match(lineAndColumnPattern)
289
+ if (lineAndColumMatch) {
290
+ const lineAndColumnString = lineAndColumMatch[0]
291
+ const lineString = lineAndColumMatch[1]
292
+ const columnString = lineAndColumMatch[2]
293
+ replacement = replace({
294
+ url: match.slice(0, -lineAndColumnString.length),
295
+ line: lineString ? parseInt(lineString) : null,
296
+ column: columnString ? parseInt(columnString) : null,
297
+ })
298
+ } else {
299
+ const linePattern = /:([0-9]+)$/
300
+ const lineMatch = match.match(linePattern)
301
+ if (lineMatch) {
302
+ const lineString = lineMatch[0]
303
+ replacement = replace({
304
+ url: match.slice(0, -lineString.length),
305
+ line: lineString ? parseInt(lineString) : null,
306
+ })
307
+ } else {
308
+ replacement = replace({
309
+ url: match,
310
+ })
311
+ }
312
+ }
313
+ if (endsWithSeparationChar) {
314
+ return `${replacement}${lastChar}`
315
+ }
316
+ return replacement
317
+ })
318
+ }
319
+
320
+ let formatError
321
+ error_formatter: {
322
+ formatError = (exceptionInfo) => {
323
+ const errorParts = {
324
+ theme: "dark",
325
+ title: "An error occured",
326
+ text: "",
327
+ tip: "",
328
+ errorDetailsPromise: null,
329
+ }
330
+ const tips = []
331
+ tips.push("Click outside to close.")
332
+ errorParts.tip = tips.join(`\n <br />\n `)
333
+
334
+ const generateClickableText = (text) => {
335
+ const textWithHtmlLinks = makeLinksClickable(text, {
336
+ createLink: ({ url, line, column }) => {
337
+ const urlSite = resolveUrlSite({ url, line, column })
338
+ if (errorBaseUrl) {
339
+ if (urlSite.url.startsWith(rootDirectoryUrl)) {
340
+ urlSite.url = `${errorBaseUrl}${urlSite.url.slice(
341
+ rootDirectoryUrl.length,
342
+ )}`
343
+ } else {
344
+ urlSite.url = "file:///mocked_for_snapshots"
345
+ }
346
+ }
347
+ const urlWithLineAndColumn = stringifyUrlSite(urlSite)
348
+ return {
349
+ href:
350
+ urlSite.url.startsWith("file:") && openInEditor
351
+ ? `javascript:window.fetch('/__open_in_editor__/${encodeURIComponent(
352
+ urlWithLineAndColumn,
353
+ )}')`
354
+ : urlSite.url,
355
+ text: urlWithLineAndColumn,
356
+ }
357
+ },
358
+ })
359
+ return textWithHtmlLinks
360
+ }
361
+
362
+ errorParts.text = stringifyMessageAndStack({
363
+ message: exceptionInfo.message
364
+ ? generateClickableText(exceptionInfo.message)
365
+ : "",
366
+ stack: exceptionInfo.stack
367
+ ? generateClickableText(exceptionInfo.stack)
368
+ : "",
369
+ })
370
+ if (exceptionInfo.site.url) {
371
+ errorParts.errorDetailsPromise = (async () => {
372
+ try {
373
+ if (
374
+ exceptionInfo.code === DYNAMIC_IMPORT_FETCH_ERROR ||
375
+ exceptionInfo.reportedBy === "script_error_event"
376
+ ) {
377
+ const response = await window.fetch(
378
+ `/__get_error_cause__/${encodeURIComponent(
379
+ exceptionInfo.site.isInline
380
+ ? exceptionInfo.site.originalUrl
381
+ : exceptionInfo.site.url,
382
+ )}`,
383
+ )
384
+ if (response.status !== 200) {
385
+ return null
386
+ }
387
+ const causeInfo = await response.json()
388
+ if (!causeInfo) {
389
+ return null
390
+ }
391
+ const causeText =
392
+ causeInfo.code === "NOT_FOUND"
393
+ ? stringifyMessageAndStack({
394
+ message: generateClickableText(causeInfo.reason),
395
+ stack: generateClickableText(causeInfo.codeFrame),
396
+ })
397
+ : stringifyMessageAndStack({
398
+ message: generateClickableText(causeInfo.stack),
399
+ stack: generateClickableText(causeInfo.codeFrame),
400
+ })
401
+ return {
402
+ cause: causeText,
403
+ }
404
+ }
405
+ if (
406
+ exceptionInfo.site.line !== undefined &&
407
+ // code frame showing internal window.reportError is pointless
408
+ !exceptionInfo.site.url.endsWith(
409
+ `script_type_module_supervisor.js`,
410
+ )
411
+ ) {
412
+ const urlToFetch = new URL(
413
+ `/__get_code_frame__/${encodeURIComponent(
414
+ stringifyUrlSite(exceptionInfo.site),
415
+ )}`,
416
+ window.origin,
417
+ )
418
+ if (!exceptionInfo.stackSourcemapped) {
419
+ urlToFetch.searchParams.set("remap", "")
420
+ }
421
+ const response = await window.fetch(urlToFetch)
422
+ if (response.status !== 200) {
423
+ return null
424
+ }
425
+ const codeFrame = await response.text()
426
+ return {
427
+ codeFrame: generateClickableText(codeFrame),
428
+ }
429
+ }
430
+ } catch (e) {
431
+ // happens if server is closed for instance
432
+ return null
433
+ }
434
+ return null
435
+ })()
436
+ }
437
+ return errorParts
438
+ }
439
+
440
+ const makeLinksClickable = (
441
+ string,
442
+ { createLink = ({ url }) => url },
443
+ ) => {
444
+ // normalize line breaks
445
+ string = string.replace(/\n/g, "\n")
446
+ string = escapeHtml(string)
447
+ // render links
448
+ string = replaceUrls(string, ({ url, line, column }) => {
449
+ const { href, text } = createLink({ url, line, column })
450
+ return link({ href, text })
451
+ })
452
+ return string
453
+ }
454
+
455
+ const escapeHtml = (string) => {
456
+ return string
457
+ .replace(/&/g, "&amp;")
458
+ .replace(/</g, "&lt;")
459
+ .replace(/>/g, "&gt;")
460
+ .replace(/"/g, "&quot;")
461
+ .replace(/'/g, "&#039;")
462
+ }
463
+
464
+ const link = ({ href, text = href }) => `<a href="${href}">${text}</a>`
465
+ }
466
+
467
+ let displayErrorNotification
468
+ error_notification: {
469
+ const { Notification } = window
470
+ displayErrorNotification =
471
+ typeof Notification === "function"
472
+ ? ({ title, text, icon }) => {
473
+ if (Notification.permission === "granted") {
474
+ const notification = new Notification(title, {
475
+ lang: "en",
476
+ body: text,
477
+ icon,
478
+ })
479
+ notification.onclick = () => {
480
+ window.focus()
481
+ }
482
+ }
483
+ }
484
+ : () => {}
485
+ }
486
+
487
+ const JSENV_ERROR_OVERLAY_TAGNAME = "jsenv-error-overlay"
488
+ let displayJsenvErrorOverlay
489
+ error_overlay: {
490
+ displayJsenvErrorOverlay = (params) => {
491
+ let jsenvErrorOverlay = new JsenvErrorOverlay(params)
492
+ document
493
+ .querySelectorAll(JSENV_ERROR_OVERLAY_TAGNAME)
494
+ .forEach((node) => {
495
+ node.parentNode.removeChild(node)
496
+ })
497
+ document.body.appendChild(jsenvErrorOverlay)
498
+ const removeErrorOverlay = () => {
499
+ if (jsenvErrorOverlay && jsenvErrorOverlay.parentNode) {
500
+ document.body.removeChild(jsenvErrorOverlay)
501
+ jsenvErrorOverlay = null
502
+ }
503
+ }
504
+ return removeErrorOverlay
505
+ }
506
+
507
+ class JsenvErrorOverlay extends HTMLElement {
508
+ constructor({ theme, title, text, tip, errorDetailsPromise }) {
509
+ super()
510
+ this.root = this.attachShadow({ mode: "open" })
511
+ this.root.innerHTML = `
512
+ <style>
513
+ ${overlayCSS}
514
+ </style>
515
+ <div class="backdrop"></div>
516
+ <div class="overlay" data-theme=${theme}>
517
+ <h1 class="title">
518
+ ${title}
519
+ </h1>
520
+ <pre class="text">${text}</pre>
521
+ <div class="tip">
522
+ ${tip}
523
+ </div>
524
+ </div>`
525
+ this.root.querySelector(".backdrop").onclick = () => {
526
+ if (!this.parentNode) {
527
+ // not in document anymore
528
+ return
529
+ }
530
+ this.root.querySelector(".backdrop").onclick = null
531
+ this.parentNode.removeChild(this)
532
+ }
533
+ if (errorDetailsPromise) {
534
+ errorDetailsPromise.then((errorDetails) => {
535
+ if (!errorDetails || !this.parentNode) {
536
+ return
537
+ }
538
+ const { codeFrame, cause } = errorDetails
539
+ if (codeFrame) {
540
+ this.root.querySelector(".text").innerHTML += `\n\n${codeFrame}`
541
+ }
542
+ if (cause) {
543
+ const causeIndented = prefixRemainingLines(cause, " ")
544
+ this.root.querySelector(
545
+ ".text",
546
+ ).innerHTML += `\n [cause]: ${causeIndented}`
547
+ }
548
+ })
549
+ }
550
+ }
551
+ }
552
+
553
+ const prefixRemainingLines = (text, prefix) => {
554
+ const lines = text.split(/\r?\n/)
555
+ const firstLine = lines.shift()
556
+ let result = firstLine
557
+ let i = 0
558
+ while (i < lines.length) {
559
+ const line = lines[i]
560
+ i++
561
+ result += line.length ? `\n${prefix}${line}` : `\n`
562
+ }
563
+ return result
564
+ }
565
+
566
+ if (customElements && !customElements.get(JSENV_ERROR_OVERLAY_TAGNAME)) {
567
+ customElements.define(JSENV_ERROR_OVERLAY_TAGNAME, JsenvErrorOverlay)
568
+ }
569
+
570
+ const overlayCSS = `
571
+ :host {
572
+ position: fixed;
573
+ z-index: 99999;
574
+ top: 0;
575
+ left: 0;
576
+ width: 100%;
577
+ height: 100%;
578
+ overflow-y: scroll;
579
+ margin: 0;
580
+ background: rgba(0, 0, 0, 0.66);
581
+ }
582
+
583
+ .backdrop {
584
+ position: absolute;
585
+ left: 0;
586
+ right: 0;
587
+ top: 0;
588
+ bottom: 0;
589
+ }
590
+
591
+ .overlay {
592
+ position: relative;
593
+ background: rgba(0, 0, 0, 0.95);
594
+ width: 800px;
595
+ margin: 30px auto;
596
+ padding: 25px 40px;
597
+ padding-top: 0;
598
+ overflow: hidden; /* for h1 margins */
599
+ border-radius: 4px 8px;
600
+ box-shadow: 0 20px 40px rgb(0 0 0 / 30%), 0 15px 12px rgb(0 0 0 / 20%);
601
+ box-sizing: border-box;
602
+ font-family: monospace;
603
+ direction: ltr;
604
+ }
605
+
606
+ h1 {
607
+ color: red;
608
+ text-align: center;
609
+ }
610
+
611
+ pre {
612
+ overflow: auto;
613
+ max-width: 100%;
614
+ /* padding is nice + prevents scrollbar from hiding the text behind it */
615
+ /* does not work nicely on firefox though https://bugzilla.mozilla.org/show_bug.cgi?id=748518 */
616
+ padding: 20px;
617
+ }
618
+
619
+ .tip {
620
+ border-top: 1px solid #999;
621
+ padding-top: 12px;
622
+ }
623
+
624
+ [data-theme="dark"] {
625
+ color: #999;
626
+ }
627
+ [data-theme="dark"] pre {
628
+ background: #111;
629
+ border: 1px solid #333;
630
+ color: #eee;
631
+ }
632
+
633
+ [data-theme="light"] {
634
+ color: #EEEEEE;
635
+ }
636
+ [data-theme="light"] pre {
637
+ background: #1E1E1E;
638
+ border: 1px solid white;
639
+ color: #EEEEEE;
640
+ }
641
+
642
+ pre a {
643
+ color: inherit;
644
+ }`
645
+ }
646
+
647
+ supervisor.createException = createException
648
+ supervisor.reportException = (exception) => {
649
+ const { theme, title, text, tip, errorDetailsPromise } =
650
+ formatError(exception)
651
+
652
+ if (errorOverlay) {
653
+ const removeErrorOverlay = displayJsenvErrorOverlay({
654
+ theme,
655
+ title,
656
+ text,
657
+ tip,
658
+ errorDetailsPromise,
659
+ })
660
+ if (window.__reloader__) {
661
+ window.__reloader__.onstatuschange = () => {
662
+ if (window.__reloader__.status === "reloading") {
663
+ removeErrorOverlay()
664
+ }
665
+ }
666
+ }
667
+ }
668
+ if (errorNotification) {
669
+ displayErrorNotification({
670
+ title,
671
+ text,
672
+ })
673
+ }
674
+ return exception
675
+ }
676
+ window.addEventListener("error", (errorEvent) => {
677
+ if (!errorEvent.isTrusted) {
678
+ // ignore custom error event (not sent by browser)
679
+ return
680
+ }
681
+ const { error, message, filename, lineno, colno } = errorEvent
682
+ const exception = supervisor.createException({
683
+ // when error is reported within a worker error is null
684
+ // but there is a message property on errorEvent
685
+ reason: error || message,
686
+ reportedBy: "window_error_event",
687
+ url: filename,
688
+ line: lineno,
689
+ column: colno,
690
+ })
691
+ supervisor.reportException(exception)
692
+ })
693
+ window.addEventListener("unhandledrejection", (event) => {
694
+ if (event.defaultPrevented) {
695
+ return
696
+ }
697
+ const exception = supervisor.createException({
698
+ reason: event.reason,
699
+ reportedBy: "window_unhandledrejection_event",
700
+ })
701
+ supervisor.reportException(exception)
702
+ })
703
+ }
704
+
705
+ supervisor.setup = ({
706
+ rootDirectoryUrl,
707
+ logs,
708
+ measurePerf,
709
+ errorOverlay,
710
+ errorBaseUrl,
711
+ openInEditor,
712
+ }) => {
713
+ supervisor.setupReportException({
714
+ rootDirectoryUrl,
715
+ errorOverlay,
716
+ errorBaseUrl,
717
+ openInEditor,
718
+ })
719
+
720
+ const supervisedScripts = []
721
+ const scriptExecutionPromises = []
722
+ supervisor.createExecution = ({ type, src, async, execute }) => {
723
+ const execution = {
724
+ type,
725
+ src,
726
+ async,
727
+ execute,
728
+ }
729
+ execution.start = () => {
730
+ return superviseExecution(execution, { isReload: false })
731
+ }
732
+ execution.reload = () => {
733
+ return superviseExecution(execution, { isReload: true })
734
+ }
735
+ supervisedScripts.push(execution)
736
+ return execution
737
+ }
738
+ const superviseExecution = async (execution, { isReload }) => {
739
+ if (logs) {
740
+ console.group(`[jsenv] loading ${execution.type} ${execution.src}`)
741
+ }
742
+ if (measurePerf) {
743
+ performance.mark(`execution_start`)
744
+ }
745
+ const executionResult = {
746
+ status: "pending",
747
+ startTime: Date.now(),
748
+ endTime: null,
749
+ exception: null,
750
+ namespace: null,
751
+ coverage: null,
752
+ }
753
+ executionResults[execution.src] = executionResult
754
+ let resolveScriptExecutionPromise
755
+ const scriptExecutionPromise = new Promise((resolve) => {
756
+ resolveScriptExecutionPromise = () => {
757
+ executionResult.endTime = Date.now()
758
+ if (measurePerf) {
759
+ performance.measure(`execution`, `execution_start`)
760
+ }
761
+ resolve()
762
+ }
763
+ })
764
+ scriptExecutionPromise.execution = execution
765
+ scriptExecutionPromises.push(scriptExecutionPromise)
766
+ try {
767
+ const result = await execution.execute({ isReload })
768
+ executionResult.status = "completed"
769
+ executionResult.namespace = result
770
+ executionResult.coverage = window.__coverage__
771
+ if (logs) {
772
+ console.log(`${execution.type} load ended`)
773
+ console.groupEnd()
774
+ }
775
+ resolveScriptExecutionPromise()
776
+ } catch (e) {
777
+ executionResult.status = "errored"
778
+ const exception = supervisor.createException({
779
+ reason: e,
780
+ })
781
+ if (exception.needsReport) {
782
+ supervisor.reportException(exception)
783
+ }
784
+ executionResult.exception = exception
785
+ executionResult.coverage = window.__coverage__
786
+ if (logs) {
787
+ console.groupEnd()
788
+ }
789
+ resolveScriptExecutionPromise()
790
+ }
791
+ }
792
+
793
+ // respect execution order
794
+ // - wait for classic scripts to be done (non async)
795
+ // - wait module script previous execution (non async)
796
+ // see https://gist.github.com/jakub-g/385ee6b41085303a53ad92c7c8afd7a6#typemodule-vs-non-module-typetextjavascript-vs-script-nomodule
797
+ supervisor.getPreviousExecutionDonePromise = async () => {
798
+ const previousNonAsyncScriptExecutions = scriptExecutionPromises.filter(
799
+ (promise) => !promise.execution.async,
800
+ )
801
+ await Promise.all(previousNonAsyncScriptExecutions)
802
+ }
803
+ supervisor.superviseScript = async ({ src, async }) => {
804
+ const { currentScript } = document
805
+ const parentNode = currentScript.parentNode
806
+ if (!async) {
807
+ await supervisor.getPreviousExecutionDonePromise()
808
+ }
809
+ let nodeToReplace
810
+ let currentScriptClone
811
+ const execution = supervisor.createExecution({
812
+ src,
813
+ type: "js_classic",
814
+ async,
815
+ execute: async ({ isReload }) => {
816
+ const urlObject = new URL(src, window.location)
817
+ const loadPromise = new Promise((resolve, reject) => {
818
+ // do not use script.cloneNode()
819
+ // bcause https://stackoverflow.com/questions/28771542/why-dont-clonenode-script-tags-execute
820
+ currentScriptClone = document.createElement("script")
821
+ // browsers set async by default when creating script(s)
822
+ // we want an exact copy to preserves how code is executed
823
+ currentScriptClone.async = false
824
+ Array.from(currentScript.attributes).forEach((attribute) => {
825
+ currentScriptClone.setAttribute(
826
+ attribute.nodeName,
827
+ attribute.nodeValue,
828
+ )
829
+ })
830
+ if (isReload) {
831
+ urlObject.searchParams.set("hmr", Date.now())
832
+ nodeToReplace = currentScriptClone
833
+ currentScriptClone.src = urlObject.href
834
+ } else {
835
+ currentScriptClone.removeAttribute("jsenv-plugin-owner")
836
+ currentScriptClone.removeAttribute("jsenv-plugin-action")
837
+ currentScriptClone.removeAttribute("inlined-from-src")
838
+ currentScriptClone.removeAttribute("original-position")
839
+ currentScriptClone.removeAttribute("original-src-position")
840
+ nodeToReplace = currentScript
841
+ currentScriptClone.src = src
842
+ }
843
+ currentScriptClone.addEventListener("error", reject)
844
+ currentScriptClone.addEventListener("load", resolve)
845
+ parentNode.replaceChild(currentScriptClone, nodeToReplace)
846
+ })
847
+ try {
848
+ await loadPromise
849
+ } catch (e) {
850
+ // eslint-disable-next-line no-throw-literal
851
+ throw {
852
+ message: `Failed to fetch script: ${urlObject.href}`,
853
+ reportedBy: "script_error_event",
854
+ url: urlObject.href,
855
+ // window.error won't be dispatched for this error
856
+ needsReport: true,
857
+ }
858
+ }
859
+ },
860
+ })
861
+ return execution.start()
862
+ }
863
+ supervisor.reloadSupervisedScript = ({ type, src }) => {
864
+ const supervisedScript = supervisedScripts.find(
865
+ (supervisedScriptCandidate) => {
866
+ if (type && supervisedScriptCandidate.type !== type) {
867
+ return false
868
+ }
869
+ if (supervisedScriptCandidate.src !== src) {
870
+ return false
871
+ }
872
+ return true
873
+ },
874
+ )
875
+ if (supervisedScript) {
876
+ supervisedScript.reload()
877
+ }
878
+ }
879
+ supervisor.getDocumentExecutionResult = async () => {
880
+ // just to be super safe and ensure any <script type="module"> got a chance to execute
881
+ const documentReadyPromise = new Promise((resolve) => {
882
+ if (document.readyState === "complete") {
883
+ resolve()
884
+ return
885
+ }
886
+ const loadCallback = () => {
887
+ window.removeEventListener("load", loadCallback)
888
+ resolve()
889
+ }
890
+ window.addEventListener("load", loadCallback)
891
+ })
892
+ await documentReadyPromise
893
+ const waitScriptExecutions = async () => {
894
+ const numberOfScripts = scriptExecutionPromises.length
895
+ await Promise.all(scriptExecutionPromises)
896
+ // new scripts added while the other where executing
897
+ // (should happen only on webkit where
898
+ // script might be added after window load event)
899
+ if (scriptExecutionPromises.length > numberOfScripts) {
900
+ await waitScriptExecutions()
901
+ }
902
+ }
903
+ await waitScriptExecutions()
904
+
905
+ return {
906
+ status: "completed",
907
+ executionResults,
908
+ startTime: navigationStartTime,
909
+ endTime: Date.now(),
910
+ }
911
+ }
912
+ }
913
+
914
+ return supervisor
915
+ })()