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