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