@jsenv/core 33.0.1 → 34.0.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 (29) hide show
  1. package/dist/js/autoreload.js +1 -4
  2. package/dist/js/supervisor.js +498 -290
  3. package/dist/jsenv.js +874 -347
  4. package/package.json +2 -3
  5. package/src/basic_fetch.js +23 -13
  6. package/src/build/start_build_server.js +3 -2
  7. package/src/dev/file_service.js +1 -1
  8. package/src/dev/start_dev_server.js +9 -6
  9. package/src/execute/execute.js +7 -18
  10. package/src/execute/runtimes/browsers/from_playwright.js +168 -32
  11. package/src/execute/runtimes/browsers/webkit.js +1 -1
  12. package/src/execute/web_server_param.js +68 -0
  13. package/src/kitchen/compat/features_compatibility.js +3 -0
  14. package/src/plugins/autoreload/client/reload.js +1 -4
  15. package/src/plugins/inline/jsenv_plugin_html_inline_content.js +30 -18
  16. package/src/plugins/plugins.js +1 -1
  17. package/src/plugins/ribbon/jsenv_plugin_ribbon.js +3 -2
  18. package/src/plugins/supervisor/client/supervisor.js +467 -287
  19. package/src/plugins/supervisor/html_supervisor_injection.js +281 -0
  20. package/src/plugins/supervisor/js_supervisor_injection.js +281 -0
  21. package/src/plugins/supervisor/jsenv_plugin_supervisor.js +48 -233
  22. package/src/plugins/transpilation/as_js_classic/jsenv_plugin_as_js_classic_html.js +1 -1
  23. package/src/plugins/transpilation/babel/jsenv_plugin_babel.js +5 -0
  24. package/src/plugins/transpilation/jsenv_plugin_top_level_await.js +1 -1
  25. package/src/test/execute_steps.js +10 -18
  26. package/src/test/execute_test_plan.js +16 -61
  27. package/src/test/logs_file_execution.js +74 -28
  28. package/dist/js/script_type_module_supervisor.js +0 -109
  29. package/src/plugins/supervisor/client/script_type_module_supervisor.js +0 -98
@@ -2,24 +2,413 @@ window.__supervisor__ = (() => {
2
2
  const notImplemented = () => {
3
3
  throw new Error(`window.__supervisor__.setup() not called`)
4
4
  }
5
- const executionResults = {}
6
5
  const supervisor = {
7
- reportError: notImplemented,
6
+ reportException: notImplemented,
8
7
  superviseScript: notImplemented,
8
+ superviseScriptTypeModule: notImplemented,
9
9
  reloadSupervisedScript: notImplemented,
10
10
  getDocumentExecutionResult: notImplemented,
11
- executionResults,
12
11
  }
13
12
 
14
- let navigationStartTime
13
+ const executionResults = {}
14
+ let documentExecutionStartTime
15
15
  try {
16
- navigationStartTime = window.performance.timing.navigationStart
16
+ documentExecutionStartTime = window.performance.timing.navigationStart
17
17
  } catch (e) {
18
- navigationStartTime = Date.now()
18
+ documentExecutionStartTime = Date.now()
19
+ }
20
+ let documentExecutionEndTime
21
+ supervisor.setup = ({
22
+ rootDirectoryUrl,
23
+ scriptInfos,
24
+ serverIsJsenvDevServer,
25
+ logs,
26
+ errorOverlay,
27
+ errorBaseUrl,
28
+ openInEditor,
29
+ }) => {
30
+ const executions = {}
31
+ const promises = []
32
+ let remainingScriptCount = scriptInfos.length
33
+
34
+ // respect execution order
35
+ // - wait for classic scripts to be done (non async)
36
+ // - wait module script previous execution (non async)
37
+ // see https://gist.github.com/jakub-g/385ee6b41085303a53ad92c7c8afd7a6#typemodule-vs-non-module-typetextjavascript-vs-script-nomodule
38
+ const executionQueue = []
39
+ let executing = false
40
+ const addToExecutionQueue = async (execution) => {
41
+ if (execution.async) {
42
+ execution.execute()
43
+ return
44
+ }
45
+ if (executing) {
46
+ executionQueue.push(execution)
47
+ return
48
+ }
49
+ execThenDequeue(execution)
50
+ }
51
+ const execThenDequeue = async (execution) => {
52
+ executing = true
53
+ // start next js module execution as soon as current js module starts to execute
54
+ // (do not wait in case of top level await)
55
+ let resolveExecutingPromise
56
+ const executingPromise = new Promise((resolve) => {
57
+ resolveExecutingPromise = resolve
58
+ })
59
+ const promise = execution.execute({
60
+ onExecuting: () => resolveExecutingPromise(),
61
+ })
62
+ await Promise.race([promise, executingPromise])
63
+ executing = false
64
+ if (executionQueue.length) {
65
+ const nextExecution = executionQueue.shift()
66
+ execThenDequeue(nextExecution)
67
+ }
68
+ }
69
+
70
+ const asExecutionId = (src) => {
71
+ const url = new URL(src, window.location).href
72
+ if (url.startsWith(window.location.origin)) {
73
+ return src
74
+ }
75
+ return url
76
+ }
77
+
78
+ const createExecutionController = (src, type) => {
79
+ const result = {
80
+ status: "pending",
81
+ duration: null,
82
+ coverage: null,
83
+ exception: null,
84
+ value: null,
85
+ }
86
+
87
+ let resolve
88
+ const promise = new Promise((_resolve) => {
89
+ resolve = _resolve
90
+ })
91
+ promises.push(promise)
92
+ executionResults[src] = result
93
+
94
+ const start = () => {
95
+ result.duration = null
96
+ result.coverage = null
97
+ result.status = "started"
98
+ result.exception = null
99
+ if (logs) {
100
+ console.group(`[jsenv] ${src} execution started (${type})`)
101
+ }
102
+ }
103
+ const end = () => {
104
+ const now = Date.now()
105
+ remainingScriptCount--
106
+ result.duration = now - documentExecutionStartTime
107
+ result.coverage = window.__coverage__
108
+ if (logs) {
109
+ console.log(`execution ${result.status}`)
110
+ console.groupEnd()
111
+ }
112
+ if (remainingScriptCount === 0) {
113
+ documentExecutionEndTime = now
114
+ }
115
+ resolve()
116
+ }
117
+ const complete = () => {
118
+ result.status = "completed"
119
+ end()
120
+ }
121
+ const fail = (error, info) => {
122
+ result.status = "failed"
123
+ const exception = supervisor.createException(error, info)
124
+ result.exception = exception
125
+ end()
126
+ }
127
+
128
+ return { result, start, complete, fail }
129
+ }
130
+
131
+ const prepareJsClassicRemoteExecution = (src) => {
132
+ const urlObject = new URL(src, window.location)
133
+ const url = urlObject.href
134
+ const { result, start, complete, fail } = createExecutionController(
135
+ src,
136
+ "js_classic",
137
+ )
138
+
139
+ let parentNode
140
+ let currentScript
141
+ let nodeToReplace
142
+ let currentScriptClone
143
+ const init = () => {
144
+ currentScript = document.currentScript
145
+ parentNode = currentScript.parentNode
146
+ executions[src].async = currentScript.async
147
+ }
148
+ const execute = async ({ isReload } = {}) => {
149
+ start()
150
+ currentScriptClone = prepareScriptToLoad(currentScript)
151
+ if (isReload) {
152
+ urlObject.searchParams.set("hmr", Date.now())
153
+ nodeToReplace = currentScriptClone
154
+ currentScriptClone.src = urlObject.href
155
+ } else {
156
+ nodeToReplace = currentScript
157
+ currentScriptClone.src = url
158
+ }
159
+ const scriptLoadPromise = getScriptLoadPromise(currentScriptClone)
160
+ parentNode.replaceChild(currentScriptClone, nodeToReplace)
161
+ const { detectedBy, failed, error } = await scriptLoadPromise
162
+ if (failed) {
163
+ if (detectedBy === "script_error_event") {
164
+ // window.error won't be dispatched for this error
165
+ reportErrorBackToBrowser(error)
166
+ }
167
+ fail(error, {
168
+ message: `Error while loading script: ${urlObject.href}`,
169
+ reportedBy: "script_error_event",
170
+ url: urlObject.href,
171
+ })
172
+ if (detectedBy === "script_error_event") {
173
+ supervisor.reportException(result.exception)
174
+ }
175
+ } else {
176
+ complete()
177
+ }
178
+ return result
179
+ }
180
+ executions[src] = { init, execute }
181
+ }
182
+ const prepareJsClassicInlineExecution = (src) => {
183
+ const { start, complete, fail } = createExecutionController(
184
+ src,
185
+ "js_classic",
186
+ )
187
+ const end = complete
188
+ const error = (e) => {
189
+ reportErrorBackToBrowser(e) // supervision shallowed the error, report back to browser
190
+ fail(e)
191
+ }
192
+ executions[src] = { isInline: true, start, end, error }
193
+ }
194
+
195
+ const isWebkitOrSafari =
196
+ typeof window.webkitConvertPointFromNodeToPage === "function"
197
+ // https://twitter.com/damienmaillard/status/1554752482273787906
198
+ const prepareJsModuleExecutionWithDynamicImport = (src) => {
199
+ const urlObject = new URL(src, window.location)
200
+ const { result, start, complete, fail } = createExecutionController(
201
+ src,
202
+ "js_classic",
203
+ )
204
+
205
+ let importFn
206
+ let currentScript
207
+ const init = (_importFn) => {
208
+ importFn = _importFn
209
+ currentScript = document.querySelector(
210
+ `script[type="module"][inlined-from-src="${src}"]`,
211
+ )
212
+ executions[src].async = currentScript.async
213
+ }
214
+ const execute = async ({ isReload } = {}) => {
215
+ start()
216
+ if (isReload) {
217
+ urlObject.searchParams.set("hmr", Date.now())
218
+ }
219
+ try {
220
+ const namespace = await importFn(urlObject.href)
221
+ complete(namespace)
222
+ return result
223
+ } catch (e) {
224
+ fail(e, {
225
+ message: `Error while importing module: ${urlObject.href}`,
226
+ reportedBy: "dynamic_import",
227
+ url: urlObject.href,
228
+ })
229
+ if (isWebkitOrSafari) {
230
+ supervisor.reportException(result.exception)
231
+ }
232
+ return result
233
+ }
234
+ }
235
+ executions[src] = { init, execute }
236
+ }
237
+ const prepareJsModuleExecutionWithScriptThenDynamicImport = (src) => {
238
+ const urlObject = new URL(src, window.location)
239
+ const { result, start, complete, fail } = createExecutionController(
240
+ src,
241
+ "js_module",
242
+ )
243
+
244
+ let importFn
245
+ let currentScript
246
+ let parentNode
247
+ let nodeToReplace
248
+ let currentScriptClone
249
+ const init = (_importFn) => {
250
+ importFn = _importFn
251
+ currentScript = document.querySelector(
252
+ `script[type="module"][inlined-from-src="${src}"]`,
253
+ )
254
+ parentNode = currentScript.parentNode
255
+ executions[src].async = currentScript.async
256
+ }
257
+ const execute = async ({ isReload, onExecuting = () => {} } = {}) => {
258
+ start()
259
+ currentScriptClone = prepareScriptToLoad(currentScript)
260
+ if (isReload) {
261
+ urlObject.searchParams.set("hmr", Date.now())
262
+ nodeToReplace = currentScriptClone
263
+ currentScriptClone.src = urlObject.href
264
+ } else {
265
+ nodeToReplace = currentScript
266
+ currentScriptClone.src = urlObject.href
267
+ }
268
+ const scriptLoadResultPromise = getScriptLoadPromise(currentScriptClone)
269
+ parentNode.replaceChild(currentScriptClone, nodeToReplace)
270
+ const { detectedBy, failed, error } = await scriptLoadResultPromise
271
+
272
+ if (failed) {
273
+ // if (detectedBy === "script_error_event") {
274
+ // reportErrorBackToBrowser(error)
275
+ // }
276
+ fail(error, {
277
+ message: `Error while loading module: ${urlObject.href}`,
278
+ reportedBy: "script_error_event",
279
+ url: urlObject.href,
280
+ })
281
+ if (detectedBy === "script_error_event") {
282
+ supervisor.reportException(result.exception)
283
+ }
284
+ return result
285
+ }
286
+
287
+ onExecuting()
288
+ result.status = "executing"
289
+ if (logs) {
290
+ console.log(`load ended`)
291
+ }
292
+ try {
293
+ const namespace = await importFn(urlObject.href)
294
+ complete(namespace)
295
+ return result
296
+ } catch (e) {
297
+ fail(e, {
298
+ message: `Error while importing module: ${urlObject.href}`,
299
+ reportedBy: "dynamic_import",
300
+ url: urlObject.href,
301
+ })
302
+ return result
303
+ }
304
+ }
305
+ executions[src] = { init, execute }
306
+ }
307
+ const prepareJsModuleRemoteExecution = isWebkitOrSafari
308
+ ? prepareJsModuleExecutionWithDynamicImport
309
+ : prepareJsModuleExecutionWithScriptThenDynamicImport
310
+ const prepareJsModuleInlineExecution = (src) => {
311
+ const { start, complete, fail } = createExecutionController(
312
+ src,
313
+ "js_module",
314
+ )
315
+ const end = complete
316
+ const error = (e) => {
317
+ // supervision shallowed the error, report back to browser
318
+ reportErrorBackToBrowser(e)
319
+ fail(e)
320
+ }
321
+ executions[src] = { isInline: true, start, end, error }
322
+ }
323
+
324
+ supervisor.setupReportException({
325
+ logs,
326
+ serverIsJsenvDevServer,
327
+ rootDirectoryUrl,
328
+ errorOverlay,
329
+ errorBaseUrl,
330
+ openInEditor,
331
+ })
332
+
333
+ scriptInfos.forEach((scriptInfo) => {
334
+ const { type, src, isInline } = scriptInfo
335
+ if (type === "js_module") {
336
+ if (isInline) {
337
+ prepareJsModuleInlineExecution(src)
338
+ } else {
339
+ prepareJsModuleRemoteExecution(src)
340
+ }
341
+ } else if (isInline) {
342
+ prepareJsClassicInlineExecution(src)
343
+ } else {
344
+ prepareJsClassicRemoteExecution(src)
345
+ }
346
+ })
347
+
348
+ // js classic
349
+ supervisor.jsClassicStart = (inlineSrc) => {
350
+ executions[inlineSrc].start()
351
+ }
352
+ supervisor.jsClassicEnd = (inlineSrc) => {
353
+ executions[inlineSrc].end()
354
+ }
355
+ supervisor.jsClassicError = (inlineSrc, e) => {
356
+ executions[inlineSrc].error(e)
357
+ }
358
+ supervisor.superviseScript = (src) => {
359
+ const execution = executions[asExecutionId(src)]
360
+ execution.init()
361
+ return addToExecutionQueue(execution)
362
+ }
363
+ // js module
364
+ supervisor.jsModuleStart = (inlineSrc) => {
365
+ executions[inlineSrc].start()
366
+ }
367
+ supervisor.jsModuleEnd = (inlineSrc) => {
368
+ executions[inlineSrc].end()
369
+ }
370
+ supervisor.jsModuleError = (inlineSrc, e) => {
371
+ executions[inlineSrc].error(e)
372
+ }
373
+ supervisor.superviseScriptTypeModule = (src, importFn) => {
374
+ const execution = executions[asExecutionId(src)]
375
+ execution.init(importFn)
376
+ return addToExecutionQueue(execution)
377
+ }
378
+
379
+ supervisor.reloadSupervisedScript = (src) => {
380
+ const execution = executions[src]
381
+ if (!execution) {
382
+ throw new Error(`no execution for ${src}`)
383
+ }
384
+ if (execution.isInline) {
385
+ throw new Error(`cannot reload inline script ${src}`)
386
+ }
387
+ return execution.execute({ isReload: true })
388
+ }
389
+
390
+ supervisor.getDocumentExecutionResult = async () => {
391
+ await Promise.all(promises)
392
+ return {
393
+ startTime: documentExecutionStartTime,
394
+ endTime: documentExecutionEndTime,
395
+ status: "completed",
396
+ executionResults,
397
+ }
398
+ }
399
+ }
400
+ const reportErrorBackToBrowser = (error) => {
401
+ if (typeof window.reportError === "function") {
402
+ window.reportError(error)
403
+ } else {
404
+ console.error(error)
405
+ }
19
406
  }
20
407
 
21
408
  supervisor.setupReportException = ({
409
+ logs,
22
410
  rootDirectoryUrl,
411
+ serverIsJsenvDevServer,
23
412
  errorNotification,
24
413
  errorOverlay,
25
414
  errorBaseUrl,
@@ -29,17 +418,14 @@ window.__supervisor__ = (() => {
29
418
  const DYNAMIC_IMPORT_EXPORT_MISSING = "dynamic_import_export_missing"
30
419
  const DYNAMIC_IMPORT_SYNTAX_ERROR = "dynamic_import_syntax_error"
31
420
 
32
- const createException = ({
33
- reason,
34
- reportedBy,
35
- url,
36
- line,
37
- column,
38
- } = {}) => {
421
+ const createException = (
422
+ reason, // can be error, string, object
423
+ { message, reportedBy, url, line, column } = {},
424
+ ) => {
39
425
  const exception = {
40
426
  reason,
41
- reportedBy,
42
427
  isError: false, // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/throw
428
+ reportedBy,
43
429
  code: null,
44
430
  message: null,
45
431
  stack: null,
@@ -91,9 +477,6 @@ window.__supervisor__ = (() => {
91
477
  if (error.url) {
92
478
  Object.assign(exception.site, resolveUrlSite({ url: error.url }))
93
479
  }
94
- if (error.needsReport) {
95
- exception.needsReport = true
96
- }
97
480
  export_missing: {
98
481
  // chrome
99
482
  if (message.includes("does not provide an export named")) {
@@ -113,6 +496,10 @@ window.__supervisor__ = (() => {
113
496
  exception.code = DYNAMIC_IMPORT_EXPORT_MISSING
114
497
  return
115
498
  }
499
+ if (message.includes("Importing a module script failed")) {
500
+ exception.code = DYNAMIC_IMPORT_FETCH_ERROR
501
+ return
502
+ }
116
503
  }
117
504
  js_syntax_error: {
118
505
  if (error.name === "SyntaxError" && typeof line === "number") {
@@ -123,8 +510,9 @@ window.__supervisor__ = (() => {
123
510
  return
124
511
  }
125
512
  if (typeof reason === "object") {
513
+ // happens when reason is an Event for instance
126
514
  exception.code = reason.code
127
- exception.message = reason.message
515
+ exception.message = reason.message || message
128
516
  exception.stack = reason.stack
129
517
  if (reason.reportedBy) {
130
518
  exception.reportedBy = reason.reportedBy
@@ -132,9 +520,6 @@ window.__supervisor__ = (() => {
132
520
  if (reason.url) {
133
521
  Object.assign(exception.site, resolveUrlSite({ url: reason.url }))
134
522
  }
135
- if (reason.needsReport) {
136
- exception.needsReport = true
137
- }
138
523
  return
139
524
  }
140
525
  exception.message = JSON.stringify(reason)
@@ -170,12 +555,7 @@ window.__supervisor__ = (() => {
170
555
  exception.code === DYNAMIC_IMPORT_SYNTAX_ERROR
171
556
  ) {
172
557
  // syntax error on inline script need line-1 for some reason
173
- if (Error.captureStackTrace) {
174
- fileUrlSite.line--
175
- } else {
176
- // firefox and safari need line-2
177
- fileUrlSite.line -= 2
178
- }
558
+ fileUrlSite.line = fileUrlSite.line - 1
179
559
  }
180
560
  Object.assign(exception.site, fileUrlSite)
181
561
  }
@@ -216,10 +596,7 @@ window.__supervisor__ = (() => {
216
596
  const extension = inlineUrlMatch[5]
217
597
  url = htmlUrl
218
598
  line = tagLineStart + (typeof line === "number" ? line : 0)
219
- // stackTrace formatted by V8 (chrome)
220
- if (Error.captureStackTrace) {
221
- line--
222
- }
599
+ line = line - 1 // sauf pour les erreur de syntaxe
223
600
  column = tagColumnStart + (typeof column === "number" ? column : 0)
224
601
  const fileUrl = resolveFileUrl(url)
225
602
  return {
@@ -255,7 +632,7 @@ window.__supervisor__ = (() => {
255
632
  }
256
633
 
257
634
  const resolveFileUrl = (url) => {
258
- let urlObject = new URL(url)
635
+ let urlObject = new URL(url, window.origin)
259
636
  if (urlObject.origin === window.origin) {
260
637
  urlObject = new URL(
261
638
  `${urlObject.pathname.slice(1)}${urlObject.search}`,
@@ -375,6 +752,9 @@ window.__supervisor__ = (() => {
375
752
  })
376
753
  if (exceptionInfo.site.url) {
377
754
  errorParts.errorDetailsPromise = (async () => {
755
+ if (!serverIsJsenvDevServer) {
756
+ return null
757
+ }
378
758
  try {
379
759
  if (
380
760
  exceptionInfo.code === DYNAMIC_IMPORT_FETCH_ERROR ||
@@ -408,13 +788,7 @@ window.__supervisor__ = (() => {
408
788
  cause: causeText,
409
789
  }
410
790
  }
411
- if (
412
- exceptionInfo.site.line !== undefined &&
413
- // code frame showing internal window.reportError is pointless
414
- !exceptionInfo.site.url.endsWith(
415
- `script_type_module_supervisor.js`,
416
- )
417
- ) {
791
+ if (exceptionInfo.site.line !== undefined) {
418
792
  const urlToFetch = new URL(
419
793
  `/__get_code_frame__/${encodeURIComponent(
420
794
  stringifyUrlSite(exceptionInfo.site),
@@ -494,6 +868,9 @@ window.__supervisor__ = (() => {
494
868
  let displayJsenvErrorOverlay
495
869
  error_overlay: {
496
870
  displayJsenvErrorOverlay = (params) => {
871
+ if (logs) {
872
+ console.log("display jsenv error overlay", params)
873
+ }
497
874
  let jsenvErrorOverlay = new JsenvErrorOverlay(params)
498
875
  document
499
876
  .querySelectorAll(JSENV_ERROR_OVERLAY_TAGNAME)
@@ -684,13 +1061,18 @@ window.__supervisor__ = (() => {
684
1061
  window.addEventListener("error", (errorEvent) => {
685
1062
  if (!errorEvent.isTrusted) {
686
1063
  // ignore custom error event (not sent by browser)
1064
+ if (logs) {
1065
+ console.log("ignore non trusted error event", errorEvent)
1066
+ }
687
1067
  return
688
1068
  }
1069
+ if (logs) {
1070
+ console.log('window "error" event received', errorEvent)
1071
+ }
689
1072
  const { error, message, filename, lineno, colno } = errorEvent
690
- const exception = supervisor.createException({
1073
+ const exception = supervisor.createException(error || message, {
691
1074
  // when error is reported within a worker error is null
692
1075
  // but there is a message property on errorEvent
693
- reason: error || message,
694
1076
  reportedBy: "window_error_event",
695
1077
  url: filename,
696
1078
  line: lineno,
@@ -702,266 +1084,64 @@ window.__supervisor__ = (() => {
702
1084
  if (event.defaultPrevented) {
703
1085
  return
704
1086
  }
705
- const exception = supervisor.createException({
706
- reason: event.reason,
1087
+ const exception = supervisor.createException(event.reason, {
707
1088
  reportedBy: "window_unhandledrejection_event",
708
1089
  })
709
1090
  supervisor.reportException(exception)
710
1091
  })
711
1092
  }
712
1093
 
713
- supervisor.setup = ({
714
- rootDirectoryUrl,
715
- logs,
716
- errorOverlay,
717
- errorBaseUrl,
718
- openInEditor,
719
- }) => {
720
- supervisor.setupReportException({
721
- rootDirectoryUrl,
722
- errorOverlay,
723
- errorBaseUrl,
724
- openInEditor,
1094
+ const prepareScriptToLoad = (script) => {
1095
+ // do not use script.cloneNode()
1096
+ // bcause https://stackoverflow.com/questions/28771542/why-dont-clonenode-script-tags-execute
1097
+ const scriptClone = document.createElement("script")
1098
+ // browsers set async by default when creating script(s)
1099
+ // we want an exact copy to preserves how code is executed
1100
+ scriptClone.async = false
1101
+ Array.from(script.attributes).forEach((attribute) => {
1102
+ scriptClone.setAttribute(attribute.nodeName, attribute.nodeValue)
725
1103
  })
1104
+ scriptClone.removeAttribute("jsenv-cooked-by")
1105
+ scriptClone.removeAttribute("jsenv-inlined-by")
1106
+ scriptClone.removeAttribute("jsenv-injected-by")
1107
+ scriptClone.removeAttribute("inlined-from-src")
1108
+ scriptClone.removeAttribute("original-position")
1109
+ scriptClone.removeAttribute("original-src-position")
726
1110
 
727
- const supervisedScripts = []
728
- const pendingPromises = []
729
- // respect execution order
730
- // - wait for classic scripts to be done (non async)
731
- // - wait module script previous execution (non async)
732
- // see https://gist.github.com/jakub-g/385ee6b41085303a53ad92c7c8afd7a6#typemodule-vs-non-module-typetextjavascript-vs-script-nomodule
733
- const executionQueue = []
734
- let executing = false
735
- const addToExecutionQueue = async (execution) => {
736
- if (execution.async) {
737
- execution.start()
738
- return
739
- }
740
- if (executing) {
741
- executionQueue.push(execution)
742
- return
743
- }
744
- startThenDequeue(execution)
745
- }
746
- const startThenDequeue = async (execution) => {
747
- executing = true
748
- const promise = execution.start()
749
- await promise
750
- executing = false
751
- if (executionQueue.length) {
752
- const nextExecution = executionQueue.shift()
753
- startThenDequeue(nextExecution)
754
- }
755
- }
756
- supervisor.addExecution = ({ type, src, async, execute }) => {
757
- const execution = {
758
- type,
759
- src,
760
- async,
761
- execute,
762
- }
763
- execution.start = () => {
764
- return superviseExecution(execution, { isReload: false })
765
- }
766
- execution.reload = () => {
767
- return superviseExecution(execution, { isReload: true })
768
- }
769
- supervisedScripts.push(execution)
770
- return addToExecutionQueue(execution)
771
- }
772
- const superviseExecution = async (execution, { isReload }) => {
773
- if (logs) {
774
- console.group(`[jsenv] loading ${execution.type} ${execution.src}`)
775
- }
776
- const executionResult = {
777
- status: "pending",
778
- loadDuration: null,
779
- executionDuration: null,
780
- duration: null,
781
- exception: null,
782
- namespace: null,
783
- coverage: null,
784
- }
785
- executionResults[execution.src] = executionResult
786
-
787
- const monitorScriptLoad = () => {
788
- const loadStartTime = Date.now()
789
- let resolveScriptLoadPromise
790
- const scriptLoadPromise = new Promise((resolve) => {
791
- resolveScriptLoadPromise = resolve
792
- })
793
- pendingPromises.push(scriptLoadPromise)
794
- return () => {
795
- const loadEndTime = Date.now()
796
- executionResult.loadDuration = loadEndTime - loadStartTime
797
- resolveScriptLoadPromise()
798
- }
799
- }
800
- const monitorScriptExecution = () => {
801
- const executionStartTime = Date.now()
802
- let resolveExecutionPromise
803
- const executionPromise = new Promise((resolve) => {
804
- resolveExecutionPromise = resolve
805
- })
806
- pendingPromises.push(executionPromise)
807
- return () => {
808
- executionResult.coverage = window.__coverage__
809
- executionResult.executionDuration = Date.now() - executionStartTime
810
- executionResult.duration =
811
- executionResult.loadDuration + executionResult.executionDuration
812
- resolveExecutionPromise()
813
- }
814
- }
1111
+ return scriptClone
1112
+ }
815
1113
 
816
- const onError = (e) => {
817
- executionResult.status = "failed"
818
- const exception = supervisor.createException({ reason: e })
819
- if (exception.needsReport) {
820
- supervisor.reportException(exception)
1114
+ const getScriptLoadPromise = async (script) => {
1115
+ return new Promise((resolve) => {
1116
+ const windowErrorEventCallback = (errorEvent) => {
1117
+ if (errorEvent.filename === script.src) {
1118
+ removeWindowErrorEventCallback()
1119
+ resolve({
1120
+ detectedBy: "window_error_event",
1121
+ failed: true,
1122
+ error: errorEvent,
1123
+ })
821
1124
  }
822
- executionResult.exception = exception
823
1125
  }
824
-
825
- const scriptLoadDone = monitorScriptLoad()
826
- try {
827
- const result = await execution.execute({ isReload })
828
- if (logs) {
829
- console.log(`${execution.type} load ended`)
830
- console.groupEnd()
831
- }
832
- executionResult.status = "loaded"
833
- scriptLoadDone()
834
-
835
- const scriptExecutionDone = monitorScriptExecution()
836
- if (execution.type === "js_classic") {
837
- executionResult.status = "completed"
838
- scriptExecutionDone()
839
- } else if (execution.type === "js_module") {
840
- result.executionPromise.then(
841
- (namespace) => {
842
- executionResult.status = "completed"
843
- executionResult.namespace = namespace
844
- scriptExecutionDone()
845
- },
846
- (e) => {
847
- onError(e)
848
- scriptExecutionDone()
849
- },
850
- )
851
- }
852
- } catch (e) {
853
- if (logs) {
854
- console.groupEnd()
855
- }
856
- onError(e)
857
- scriptLoadDone()
1126
+ const removeWindowErrorEventCallback = () => {
1127
+ window.removeEventListener("error", windowErrorEventCallback)
858
1128
  }
859
- }
860
-
861
- supervisor.superviseScript = async ({ src, async }) => {
862
- const { currentScript } = document
863
- const parentNode = currentScript.parentNode
864
- let nodeToReplace
865
- let currentScriptClone
866
- return supervisor.addExecution({
867
- src,
868
- type: "js_classic",
869
- async,
870
- execute: async ({ isReload }) => {
871
- const urlObject = new URL(src, window.location)
872
- const loadPromise = new Promise((resolve, reject) => {
873
- // do not use script.cloneNode()
874
- // bcause https://stackoverflow.com/questions/28771542/why-dont-clonenode-script-tags-execute
875
- currentScriptClone = document.createElement("script")
876
- // browsers set async by default when creating script(s)
877
- // we want an exact copy to preserves how code is executed
878
- currentScriptClone.async = false
879
- Array.from(currentScript.attributes).forEach((attribute) => {
880
- currentScriptClone.setAttribute(
881
- attribute.nodeName,
882
- attribute.nodeValue,
883
- )
884
- })
885
- if (isReload) {
886
- urlObject.searchParams.set("hmr", Date.now())
887
- nodeToReplace = currentScriptClone
888
- currentScriptClone.src = urlObject.href
889
- } else {
890
- currentScriptClone.removeAttribute("jsenv-cooked-by")
891
- currentScriptClone.removeAttribute("jsenv-inlined-by")
892
- currentScriptClone.removeAttribute("jsenv-injected-by")
893
- currentScriptClone.removeAttribute("inlined-from-src")
894
- currentScriptClone.removeAttribute("original-position")
895
- currentScriptClone.removeAttribute("original-src-position")
896
- nodeToReplace = currentScript
897
- currentScriptClone.src = src
898
- }
899
- currentScriptClone.addEventListener("error", reject)
900
- currentScriptClone.addEventListener("load", resolve)
901
- parentNode.replaceChild(currentScriptClone, nodeToReplace)
902
- })
903
- try {
904
- await loadPromise
905
- } catch (e) {
906
- // eslint-disable-next-line no-throw-literal
907
- throw {
908
- message: `Failed to fetch script: ${urlObject.href}`,
909
- reportedBy: "script_error_event",
910
- url: urlObject.href,
911
- // window.error won't be dispatched for this error
912
- needsReport: true,
913
- }
914
- }
915
- },
1129
+ window.addEventListener("error", windowErrorEventCallback)
1130
+ script.addEventListener("error", (errorEvent) => {
1131
+ removeWindowErrorEventCallback()
1132
+ resolve({
1133
+ detectedBy: "script_error_event",
1134
+ failed: true,
1135
+ error: errorEvent,
1136
+ })
916
1137
  })
917
- }
918
- supervisor.reloadSupervisedScript = ({ type, src }) => {
919
- const supervisedScript = supervisedScripts.find(
920
- (supervisedScriptCandidate) => {
921
- if (type && supervisedScriptCandidate.type !== type) {
922
- return false
923
- }
924
- return supervisedScriptCandidate.src === src
925
- },
926
- )
927
- if (supervisedScript) {
928
- supervisedScript.reload()
929
- }
930
- }
931
- supervisor.getDocumentExecutionResult = async () => {
932
- // just to be super safe and ensure any <script type="module"> got a chance to execute
933
- const documentReadyPromise = new Promise((resolve) => {
934
- if (document.readyState === "complete") {
935
- resolve()
936
- return
937
- }
938
- const loadCallback = () => {
939
- window.removeEventListener("load", loadCallback)
940
- resolve()
941
- }
942
- window.addEventListener("load", loadCallback)
1138
+ script.addEventListener("load", () => {
1139
+ removeWindowErrorEventCallback()
1140
+ resolve({
1141
+ detectedBy: "script_load_event",
1142
+ })
943
1143
  })
944
- await documentReadyPromise
945
- const waitScriptExecutions = async () => {
946
- const numberOfPromises = pendingPromises.length
947
- await Promise.all(pendingPromises)
948
- // new scripts added while the other where executing
949
- // (should happen only on webkit where
950
- // script might be added after window load event)
951
- await new Promise((resolve) => setTimeout(resolve))
952
- if (pendingPromises.length > numberOfPromises) {
953
- await waitScriptExecutions()
954
- }
955
- }
956
- await waitScriptExecutions()
957
-
958
- return {
959
- status: "completed",
960
- executionResults,
961
- startTime: navigationStartTime,
962
- endTime: Date.now(),
963
- }
964
- }
1144
+ })
965
1145
  }
966
1146
 
967
1147
  return supervisor