@jsenv/core 33.0.2 → 34.0.1

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 (32) hide show
  1. package/dist/js/autoreload.js +1 -4
  2. package/dist/js/supervisor.js +498 -290
  3. package/dist/jsenv.js +938 -370
  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 +283 -0
  21. package/src/plugins/supervisor/jsenv_plugin_supervisor.js +48 -233
  22. package/src/plugins/transpilation/as_js_classic/convert_js_module_to_js_classic.js +67 -30
  23. package/src/plugins/transpilation/as_js_classic/jsenv_plugin_as_js_classic_html.js +1 -1
  24. package/src/plugins/transpilation/babel/jsenv_plugin_babel.js +5 -0
  25. package/src/plugins/transpilation/jsenv_plugin_top_level_await.js +1 -1
  26. package/src/test/execute_steps.js +10 -18
  27. package/src/test/execute_test_plan.js +12 -60
  28. package/src/test/logs_file_execution.js +74 -28
  29. package/dist/js/babel_plugin_transform_modules_systemjs.cjs +0 -392
  30. package/dist/js/script_type_module_supervisor.js +0 -109
  31. package/src/plugins/supervisor/client/script_type_module_supervisor.js +0 -98
  32. package/src/plugins/transpilation/as_js_classic/babel_plugin_transform_modules_systemjs.cjs +0 -608
package/dist/jsenv.js CHANGED
@@ -15,15 +15,15 @@ import { Readable, Stream, Writable } from "node:stream";
15
15
  import { Http2ServerResponse } from "node:http2";
16
16
  import { lookup } from "node:dns";
17
17
  import { SOURCEMAP, generateSourcemapFileUrl, composeTwoSourcemaps, generateSourcemapDataUrl, createMagicSource, getOriginalPosition } from "@jsenv/sourcemap";
18
- import { parseHtmlString, stringifyHtmlAst, getHtmlNodeAttribute, visitHtmlNodes, analyzeScriptNode, setHtmlNodeAttributes, parseSrcSet, getHtmlNodePosition, getHtmlNodeAttributePosition, parseCssUrls, parseJsUrls, getHtmlNodeText, setHtmlNodeText, applyBabelPlugins, injectScriptNodeAsEarlyAsPossible, createHtmlNode, findHtmlNode, removeHtmlNode, removeHtmlNodeText, injectJsImport, analyzeLinkNode, injectHtmlNode, insertHtmlNodeAfter } from "@jsenv/ast";
18
+ import { parseHtmlString, stringifyHtmlAst, getHtmlNodeAttribute, visitHtmlNodes, analyzeScriptNode, setHtmlNodeAttributes, parseSrcSet, getHtmlNodePosition, getHtmlNodeAttributePosition, parseCssUrls, parseJsUrls, getHtmlNodeText, setHtmlNodeText, removeHtmlNodeText, applyBabelPlugins, injectScriptNodeAsEarlyAsPossible, createHtmlNode, findHtmlNode, removeHtmlNode, injectJsImport, analyzeLinkNode, injectHtmlNode, insertHtmlNodeAfter } from "@jsenv/ast";
19
19
  import { createRequire } from "node:module";
20
20
  import babelParser from "@babel/parser";
21
21
  import { bundleJsModules } from "@jsenv/plugin-bundling";
22
22
  import v8, { takeCoverage } from "node:v8";
23
- import wrapAnsi from "wrap-ansi";
24
23
  import stripAnsi from "strip-ansi";
25
24
  import { createId } from "@paralleldrive/cuid2";
26
25
  import { runInNewContext } from "node:vm";
26
+ import wrapAnsi from "wrap-ansi";
27
27
  import { fork } from "node:child_process";
28
28
  import { Worker } from "node:worker_threads";
29
29
 
@@ -8131,7 +8131,9 @@ const featuresCompatMap = {
8131
8131
  samsung: "9.2"
8132
8132
  },
8133
8133
  import_meta_resolve: {
8134
- chrome: "107"
8134
+ chrome: "107",
8135
+ edge: "105",
8136
+ firefox: "106"
8135
8137
  },
8136
8138
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#browser_compatibility
8137
8139
  import_dynamic: {
@@ -8161,7 +8163,8 @@ const featuresCompatMap = {
8161
8163
  chrome: "89",
8162
8164
  opera: "76",
8163
8165
  samsung: "15",
8164
- firefox: "108"
8166
+ firefox: "108",
8167
+ safari: "16.4"
8165
8168
  },
8166
8169
  import_type_json: {
8167
8170
  chrome: "91",
@@ -10411,6 +10414,11 @@ const findOriginalDirectoryReference = (urlInfo, context) => {
10411
10414
  return ref;
10412
10415
  };
10413
10416
 
10417
+ /*
10418
+ * This plugin ensure content inlined inside HTML is cooked (inline <script> for instance)
10419
+ * For <script hot-accept> the script content will be moved to a virtual file
10420
+ * to enable hot reloading
10421
+ */
10414
10422
  const jsenvPluginHtmlInlineContent = ({
10415
10423
  analyzeConvertedScripts
10416
10424
  }) => {
@@ -10508,9 +10516,7 @@ ${e.traceMessage}`);
10508
10516
  if (!analyzeConvertedScripts && getHtmlNodeAttribute(scriptNode, "jsenv-injected-by") === "jsenv:as_js_classic_html") {
10509
10517
  return;
10510
10518
  }
10511
- if (getHtmlNodeAttribute(scriptNode, "jsenv-cooked-by") === "jsenv:supervisor" || getHtmlNodeAttribute(scriptNode, "jsenv-inlined-by") === "jsenv:supervisor" || getHtmlNodeAttribute(scriptNode, "jsenv-injected-by") === "jsenv:supervisor") {
10512
- return;
10513
- }
10519
+ const hotAccept = getHtmlNodeAttribute(scriptNode, "hot-accept") !== undefined;
10514
10520
  const {
10515
10521
  type,
10516
10522
  contentType,
@@ -10555,14 +10561,26 @@ ${e.traceMessage}`);
10555
10561
  inlineContentUrlInfo: inlineScriptUrlInfo,
10556
10562
  inlineContentReference: inlineScriptReference
10557
10563
  });
10558
- });
10559
- mutations.push(() => {
10560
- setHtmlNodeText(scriptNode, inlineScriptUrlInfo.content);
10561
- setHtmlNodeAttributes(scriptNode, {
10562
- "jsenv-cooked-by": "jsenv:html_inline_content",
10563
- ...(extension ? {
10564
- type: type === "js_module" ? "module" : undefined
10565
- } : {})
10564
+ mutations.push(() => {
10565
+ const attributes = {
10566
+ "jsenv-cooked-by": "jsenv:html_inline_content",
10567
+ // 1. <script type="jsx"> becomes <script>
10568
+ // 2. <script type="module/jsx"> becomes <script type="module">
10569
+ ...(extension ? {
10570
+ type: type === "js_module" ? "module" : undefined
10571
+ } : {})
10572
+ };
10573
+ if (hotAccept) {
10574
+ removeHtmlNodeText(scriptNode);
10575
+ setHtmlNodeAttributes(scriptNode, {
10576
+ ...attributes
10577
+ });
10578
+ } else {
10579
+ setHtmlNodeText(scriptNode, inlineScriptUrlInfo.content);
10580
+ setHtmlNodeAttributes(scriptNode, {
10581
+ ...attributes
10582
+ });
10583
+ }
10566
10584
  });
10567
10585
  });
10568
10586
  }
@@ -14735,21 +14753,7 @@ function default_1({
14735
14753
  };
14736
14754
  }
14737
14755
 
14738
- /*
14739
- * When systemjs format is used by babel, it will generated UID based on
14740
- * the import specifier:
14741
- * https://github.com/babel/babel/blob/97d1967826077f15e766778c0d64711399e9a72a/packages/babel-plugin-transform-modules-systemjs/src/index.ts#L498
14742
- * But at this stage import specifier are absolute file urls
14743
- * So without minification these specifier are long and dependent
14744
- * on where the files are on the filesystem.
14745
- * This can be mitigated by minification that will rename them.
14746
- * But to fix this issue once and for all I have copy-pasted
14747
- * "@babel/plugin-transform-modules-systemjs" to introduce
14748
- * "generateIdentifierHint" options and prevent that from hapenning
14749
- */
14750
- const TRANSFORM_MODULES_SYSTEMJS_PATH = fileURLToPath(new URL("./js/babel_plugin_transform_modules_systemjs.cjs", import.meta.url));
14751
14756
  const convertJsModuleToJsClassic = async ({
14752
- rootDirectoryUrl,
14753
14757
  systemJsInjection,
14754
14758
  systemJsClientFileUrl,
14755
14759
  urlInfo,
@@ -14774,15 +14778,8 @@ const convertJsModuleToJsClassic = async ({
14774
14778
  babelPlugins: [...(jsClassicFormat === "system" ? [
14775
14779
  // proposal-dynamic-import required with systemjs for babel8:
14776
14780
  // https://github.com/babel/babel/issues/10746
14777
- requireFromJsenv("@babel/plugin-proposal-dynamic-import"), [
14778
- // eslint-disable-next-line import/no-dynamic-require
14779
- requireFromJsenv(TRANSFORM_MODULES_SYSTEMJS_PATH), {
14780
- generateIdentifierHint: key => {
14781
- if (key.startsWith("file://")) {
14782
- return urlToRelativeUrl(key, rootDirectoryUrl);
14783
- }
14784
- return key;
14785
- }
14781
+ requireFromJsenv("@babel/plugin-proposal-dynamic-import"), requireFromJsenv("@babel/plugin-transform-modules-systemjs"), [babelPluginRelativeImports, {
14782
+ rootUrl: jsModuleUrlInfo.url
14786
14783
  }], [default_1, {
14787
14784
  asyncAwait: false,
14788
14785
  // already handled + we might not needs it at all
@@ -14791,7 +14788,9 @@ const convertJsModuleToJsClassic = async ({
14791
14788
  asyncAwait: false,
14792
14789
  // already handled + we might not needs it at all
14793
14790
  topLevelAwait: "simple"
14794
- }], babelPluginTransformImportMetaUrl, babelPluginTransformImportMetaResolve, requireFromJsenv("@babel/plugin-transform-modules-umd")])],
14791
+ }], babelPluginTransformImportMetaUrl, babelPluginTransformImportMetaResolve, requireFromJsenv("@babel/plugin-transform-modules-umd"), [babelPluginRelativeImports, {
14792
+ rootUrl: jsModuleUrlInfo.url
14793
+ }]])],
14795
14794
  urlInfo: jsModuleUrlInfo
14796
14795
  });
14797
14796
  let sourcemap = jsModuleUrlInfo.sourcemap;
@@ -14828,6 +14827,68 @@ const convertJsModuleToJsClassic = async ({
14828
14827
  };
14829
14828
  };
14830
14829
 
14830
+ /*
14831
+ * When systemjs or umd format is used by babel, it will generated UID based on
14832
+ * the import specifier:
14833
+ * https://github.com/babel/babel/blob/97d1967826077f15e766778c0d64711399e9a72a/packages/babel-plugin-transform-modules-systemjs/src/index.ts#L498
14834
+ * But at this stage import specifier are absolute file urls
14835
+ * This can be mitigated by minification that will rename them.
14836
+ * But to fix this issue once and for all there is babelPluginRelativeImports below
14837
+ */
14838
+ const babelPluginRelativeImports = babel => {
14839
+ const t = babel.types;
14840
+ const replaceSpecifierAtPath = (path, state) => {
14841
+ const specifier = path.node.value;
14842
+ if (specifier.startsWith("file://")) {
14843
+ const specifierRelative = urlToRelativeUrl(specifier, state.opts.rootUrl);
14844
+ path.replaceWith(t.stringLiteral(specifierRelative));
14845
+ }
14846
+ };
14847
+ return {
14848
+ name: "relative-imports",
14849
+ visitor: {
14850
+ CallExpression: (path, state) => {
14851
+ if (path.node.callee.type !== "Import") {
14852
+ // Some other function call, not import();
14853
+ return;
14854
+ }
14855
+ if (path.node.arguments[0].type !== "StringLiteral") {
14856
+ // Non-string argument, probably a variable or expression, e.g.
14857
+ // import(moduleId)
14858
+ // import('./' + moduleName)
14859
+ return;
14860
+ }
14861
+ const sourcePath = path.get("arguments")[0];
14862
+ if (sourcePath.node.type === "StringLiteral") {
14863
+ replaceSpecifierAtPath(sourcePath, state);
14864
+ }
14865
+ },
14866
+ ImportDeclaration: (path, state) => {
14867
+ const sourcePath = path.get("source");
14868
+ replaceSpecifierAtPath(sourcePath, state);
14869
+ },
14870
+ ExportAllDeclaration: (path, state) => {
14871
+ const sourcePath = path.get("source");
14872
+ replaceSpecifierAtPath(sourcePath, state);
14873
+ },
14874
+ ExportNamedDeclaration: (path, state) => {
14875
+ if (!path.node.source) {
14876
+ // This export has no "source", so it's probably
14877
+ // a local variable or function, e.g.
14878
+ // export { varName }
14879
+ // export const constName = ...
14880
+ // export function funcName() {}
14881
+ return;
14882
+ }
14883
+ const sourcePath = path.get("source");
14884
+ if (sourcePath.node.type === "StringLiteral") {
14885
+ replaceSpecifierAtPath(sourcePath, state);
14886
+ }
14887
+ }
14888
+ }
14889
+ };
14890
+ };
14891
+
14831
14892
  /*
14832
14893
  * - propagate ?as_js_classic to urls
14833
14894
  * - perform conversion from js module to js classic when url uses ?as_js_classic
@@ -15062,7 +15123,7 @@ const jsenvPluginAsJsClassicHtml = ({
15062
15123
  break;
15063
15124
  }
15064
15125
  } catch (e) {
15065
- if (context.dev) {
15126
+ if (context.dev && e.code !== "PARSE_ERROR") {
15066
15127
  needsSystemJs = true;
15067
15128
  // ignore cooking error, the browser will trigger it again on fetch
15068
15129
  // + disable cache for this html file because when browser will reload
@@ -17484,49 +17545,502 @@ const jsenvPluginHttpUrls = () => {
17484
17545
  };
17485
17546
 
17486
17547
  /*
17487
- * Jsenv needs to wait for all js execution inside an HTML page before killing the browser.
17488
- * A naive approach would consider execution done when "load" event is dispatched on window but:
17548
+ * ```js
17549
+ * console.log(42)
17550
+ * ```
17551
+ * becomes
17552
+ * ```js
17553
+ * window.__supervisor__.jsClassicStart('main.html@L10-L13.js')
17554
+ * try {
17555
+ * console.log(42)
17556
+ * window.__supervisor__.jsClassicEnd('main.html@L10-L13.js')
17557
+ * } catch(e) {
17558
+ * window.__supervisor__.jsClassicError('main.html@L10-L13.js', e)
17559
+ * }
17560
+ * ```
17561
+ *
17562
+ * ```js
17563
+ * import value from "./file.js"
17564
+ * console.log(value)
17565
+ * ```
17566
+ * becomes
17567
+ * ```js
17568
+ * window.__supervisor__.jsModuleStart('main.html@L10-L13.js')
17569
+ * try {
17570
+ * const value = await import("./file.js")
17571
+ * console.log(value)
17572
+ * window.__supervisor__.jsModuleEnd('main.html@L10-L13.js')
17573
+ * } catch(e) {
17574
+ * window.__supervisor__.jsModuleError('main.html@L10-L13.js', e)
17575
+ * }
17576
+ * ```
17489
17577
  *
17578
+ * -> TO KEEP IN MIND:
17579
+ * Static import can throw errors like
17580
+ * The requested module '/js_module_export_not_found/foo.js' does not provide an export named 'answerr'
17581
+ * While dynamic import will work just fine
17582
+ * and create a variable named "undefined"
17583
+ */
17584
+ const injectSupervisorIntoJs = async ({
17585
+ webServer,
17586
+ content,
17587
+ url,
17588
+ type,
17589
+ inlineSrc
17590
+ }) => {
17591
+ const babelPluginJsSupervisor = type === "js_module" ? babelPluginJsModuleSupervisor : babelPluginJsClassicSupervisor;
17592
+ const result = await applyBabelPlugins({
17593
+ urlInfo: {
17594
+ content,
17595
+ originalUrl: url,
17596
+ type
17597
+ },
17598
+ babelPlugins: [[babelPluginJsSupervisor, {
17599
+ inlineSrc
17600
+ }]]
17601
+ });
17602
+ let code = result.code;
17603
+ let map = result.map;
17604
+ const sourcemapDataUrl = generateSourcemapDataUrl(map);
17605
+ code = SOURCEMAP.writeComment({
17606
+ contentType: "text/javascript",
17607
+ content: code,
17608
+ specifier: sourcemapDataUrl
17609
+ });
17610
+ code = `${code}
17611
+ //# sourceURL=${urlToRelativeUrl(url, webServer.rootDirectoryUrl)}`;
17612
+ return code;
17613
+ };
17614
+ const babelPluginJsModuleSupervisor = babel => {
17615
+ const t = babel.types;
17616
+ return {
17617
+ name: "js-module-supervisor",
17618
+ visitor: {
17619
+ Program: (programPath, state) => {
17620
+ const {
17621
+ inlineSrc
17622
+ } = state.opts;
17623
+ if (state.file.metadata.jsExecutionInstrumented) return;
17624
+ state.file.metadata.jsExecutionInstrumented = true;
17625
+ const urlNode = t.stringLiteral(inlineSrc);
17626
+ const startCallNode = createSupervisionCall({
17627
+ t,
17628
+ urlNode,
17629
+ methodName: "jsModuleStart"
17630
+ });
17631
+ const endCallNode = createSupervisionCall({
17632
+ t,
17633
+ urlNode,
17634
+ methodName: "jsModuleEnd"
17635
+ });
17636
+ const errorCallNode = createSupervisionCall({
17637
+ t,
17638
+ urlNode,
17639
+ methodName: "jsModuleError",
17640
+ args: [t.identifier("e")]
17641
+ });
17642
+ const bodyPath = programPath.get("body");
17643
+ const importNodes = [];
17644
+ const topLevelNodes = [];
17645
+ for (const topLevelNodePath of bodyPath) {
17646
+ const topLevelNode = topLevelNodePath.node;
17647
+ if (t.isImportDeclaration(topLevelNode)) {
17648
+ importNodes.push(topLevelNode);
17649
+ } else {
17650
+ topLevelNodes.push(topLevelNode);
17651
+ }
17652
+ }
17653
+
17654
+ // replace all import nodes with dynamic imports
17655
+ const dynamicImports = [];
17656
+ importNodes.forEach(importNode => {
17657
+ const dynamicImportConversion = convertStaticImportIntoDynamicImport(importNode, t);
17658
+ if (Array.isArray(dynamicImportConversion)) {
17659
+ dynamicImports.push(...dynamicImportConversion);
17660
+ } else {
17661
+ dynamicImports.push(dynamicImportConversion);
17662
+ }
17663
+ });
17664
+ const tryCatchNode = t.tryStatement(t.blockStatement([...dynamicImports, ...topLevelNodes, endCallNode]), t.catchClause(t.identifier("e"), t.blockStatement([errorCallNode])));
17665
+ programPath.replaceWith(t.program([startCallNode, tryCatchNode]));
17666
+ }
17667
+ }
17668
+ };
17669
+ };
17670
+ const convertStaticImportIntoDynamicImport = (staticImportNode, t) => {
17671
+ const awaitExpression = t.awaitExpression(t.callExpression(t.import(), [t.stringLiteral(staticImportNode.source.value)]));
17672
+
17673
+ // import "./file.js" -> await import("./file.js")
17674
+ if (staticImportNode.specifiers.length === 0) {
17675
+ return t.expressionStatement(awaitExpression);
17676
+ }
17677
+ if (staticImportNode.specifiers.length === 1) {
17678
+ const [firstSpecifier] = staticImportNode.specifiers;
17679
+ if (firstSpecifier.type === "ImportNamespaceSpecifier") {
17680
+ return t.variableDeclaration("const", [t.variableDeclarator(t.identifier(firstSpecifier.local.name), awaitExpression)]);
17681
+ }
17682
+ }
17683
+ if (staticImportNode.specifiers.length === 2) {
17684
+ const [first, second] = staticImportNode.specifiers;
17685
+ if (first.type === "ImportDefaultSpecifier" && second.type === "ImportNamespaceSpecifier") {
17686
+ const namespaceDeclaration = t.variableDeclaration("const", [t.variableDeclarator(t.identifier(second.local.name), awaitExpression)]);
17687
+ const defaultDeclaration = t.variableDeclaration("const", [t.variableDeclarator(t.identifier(first.local.name), t.memberExpression(t.identifier(second.local.name), t.identifier("default")))]);
17688
+ return [namespaceDeclaration, defaultDeclaration];
17689
+ }
17690
+ }
17691
+
17692
+ // import { name } from "./file.js" -> const { name } = await import("./file.js")
17693
+ // import toto, { name } from "./file.js" -> const { name, default as toto } = await import("./file.js")
17694
+ const objectPattern = t.objectPattern(staticImportNode.specifiers.map(specifier => {
17695
+ if (specifier.type === "ImportDefaultSpecifier") {
17696
+ return t.objectProperty(t.identifier("default"), t.identifier(specifier.local.name), false,
17697
+ // computed
17698
+ false // shorthand
17699
+ );
17700
+ }
17701
+ // if (specifier.type === "ImportNamespaceSpecifier") {
17702
+ // return t.restElement(t.identifier(specifier.local.name))
17703
+ // }
17704
+ const isRenamed = specifier.imported.name !== specifier.local.name;
17705
+ if (isRenamed) {
17706
+ return t.objectProperty(t.identifier(specifier.imported.name), t.identifier(specifier.local.name), false,
17707
+ // computed
17708
+ false // shorthand
17709
+ );
17710
+ }
17711
+ // shorthand must be true
17712
+ return t.objectProperty(t.identifier(specifier.local.name), t.identifier(specifier.local.name), false,
17713
+ // computed
17714
+ true // shorthand
17715
+ );
17716
+ }));
17717
+
17718
+ const variableDeclarator = t.variableDeclarator(objectPattern, awaitExpression);
17719
+ const variableDeclaration = t.variableDeclaration("const", [variableDeclarator]);
17720
+ return variableDeclaration;
17721
+ };
17722
+ const babelPluginJsClassicSupervisor = babel => {
17723
+ const t = babel.types;
17724
+ return {
17725
+ name: "js-classic-supervisor",
17726
+ visitor: {
17727
+ Program: (programPath, state) => {
17728
+ const {
17729
+ inlineSrc
17730
+ } = state.opts;
17731
+ if (state.file.metadata.jsExecutionInstrumented) return;
17732
+ state.file.metadata.jsExecutionInstrumented = true;
17733
+ const urlNode = t.stringLiteral(inlineSrc);
17734
+ const startCallNode = createSupervisionCall({
17735
+ t,
17736
+ urlNode,
17737
+ methodName: "jsClassicStart"
17738
+ });
17739
+ const endCallNode = createSupervisionCall({
17740
+ t,
17741
+ urlNode,
17742
+ methodName: "jsClassicEnd"
17743
+ });
17744
+ const errorCallNode = createSupervisionCall({
17745
+ t,
17746
+ urlNode,
17747
+ methodName: "jsClassicError",
17748
+ args: [t.identifier("e")]
17749
+ });
17750
+ const topLevelNodes = programPath.node.body;
17751
+ const tryCatchNode = t.tryStatement(t.blockStatement([...topLevelNodes, endCallNode]), t.catchClause(t.identifier("e"), t.blockStatement([errorCallNode])));
17752
+ programPath.replaceWith(t.program([startCallNode, tryCatchNode]));
17753
+ }
17754
+ }
17755
+ };
17756
+ };
17757
+ const createSupervisionCall = ({
17758
+ t,
17759
+ methodName,
17760
+ urlNode,
17761
+ args = []
17762
+ }) => {
17763
+ return t.expressionStatement(t.callExpression(t.memberExpression(t.memberExpression(t.identifier("window"), t.identifier("__supervisor__")), t.identifier(methodName)), [urlNode, ...args]), [], null);
17764
+ };
17765
+
17766
+ /*
17767
+ * Jsenv needs to track js execution in order to:
17768
+ * 1. report errors
17769
+ * 2. wait for all js execution inside an HTML page before killing the browser
17770
+ *
17771
+ * A naive approach would rely on "load" events on window but:
17490
17772
  * scenario | covered by window "load"
17491
17773
  * ------------------------------------------- | -------------------------
17492
17774
  * js referenced by <script src> | yes
17493
17775
  * js inlined into <script> | yes
17494
17776
  * js referenced by <script type="module" src> | partially (not for import and top level await)
17495
17777
  * js inlined into <script type="module"> | not at all
17496
- *
17497
- * This plugin provides a way for jsenv to know when js execution is done
17498
- * As a side effect this plugin enables ability to hot reload js inlined into <script hot-accept>
17778
+ * Same for "error" event on window who is not enough
17499
17779
  *
17500
17780
  * <script src="file.js">
17501
17781
  * becomes
17502
17782
  * <script>
17503
- * window.__supervisor__.superviseScript({ src: 'file.js' })
17783
+ * window.__supervisor__.superviseScript('file.js')
17504
17784
  * </script>
17505
17785
  *
17506
17786
  * <script>
17507
17787
  * console.log(42)
17508
17788
  * </script>
17509
17789
  * becomes
17510
- * <script>
17511
- * window.__supervisor__.superviseScript({ src: 'main.html@L10-L13.js' })
17790
+ * <script inlined-from-src="main.html@L10-C5.js">
17791
+ * window.__supervisor.__superviseScript("main.html@L10-C5.js")
17512
17792
  * </script>
17513
17793
  *
17514
17794
  * <script type="module" src="module.js"></script>
17515
17795
  * becomes
17516
17796
  * <script type="module">
17517
- * import { superviseScriptTypeModule } from 'supervisor'
17518
- * superviseScriptTypeModule({ src: "module.js" })
17797
+ * window.__supervisor__.superviseScriptTypeModule('module.js')
17519
17798
  * </script>
17520
17799
  *
17521
17800
  * <script type="module">
17522
17801
  * console.log(42)
17523
17802
  * </script>
17524
17803
  * becomes
17525
- * <script type="module">
17526
- * import { superviseScriptTypeModule } from 'supervisor'
17527
- * superviseScriptTypeModule({ src: 'main.html@L10-L13.js' })
17804
+ * <script type="module" inlined-from-src="main.html@L10-C5.js">
17805
+ * window.__supervisor__.superviseScriptTypeModule('main.html@L10-C5.js')
17528
17806
  * </script>
17807
+ *
17808
+ * Why Inline scripts are converted to files dynamically?
17809
+ * -> No changes required on js source code, it's only the HTML that is modified
17810
+ * - Also allow to catch syntax errors and export missing
17529
17811
  */
17812
+ const supervisorFileUrl$1 = new URL("./js/supervisor.js", import.meta.url).href;
17813
+ const injectSupervisorIntoHTML = async ({
17814
+ content,
17815
+ url
17816
+ }, {
17817
+ supervisorScriptSrc = supervisorFileUrl$1,
17818
+ supervisorOptions,
17819
+ webServer,
17820
+ onInlineScript = () => {},
17821
+ generateInlineScriptSrc = ({
17822
+ inlineScriptUrl
17823
+ }) => urlToRelativeUrl(inlineScriptUrl, webServer.rootDirectoryUrl),
17824
+ inlineAsRemote
17825
+ }) => {
17826
+ const htmlAst = parseHtmlString(content);
17827
+ const mutations = [];
17828
+ const actions = [];
17829
+ const scriptInfos = [];
17830
+ // 1. Find inline and remote scripts
17831
+ {
17832
+ const handleInlineScript = (scriptNode, {
17833
+ type,
17834
+ extension,
17835
+ textContent
17836
+ }) => {
17837
+ const {
17838
+ line,
17839
+ column,
17840
+ lineEnd,
17841
+ columnEnd,
17842
+ isOriginal
17843
+ } = getHtmlNodePosition(scriptNode, {
17844
+ preferOriginal: true
17845
+ });
17846
+ const inlineScriptUrl = generateInlineContentUrl({
17847
+ url,
17848
+ extension: extension || ".js",
17849
+ line,
17850
+ column,
17851
+ lineEnd,
17852
+ columnEnd
17853
+ });
17854
+ const inlineScriptSrc = generateInlineScriptSrc({
17855
+ type,
17856
+ textContent,
17857
+ inlineScriptUrl,
17858
+ isOriginal,
17859
+ line,
17860
+ column
17861
+ });
17862
+ onInlineScript({
17863
+ type,
17864
+ textContent,
17865
+ url: inlineScriptUrl,
17866
+ isOriginal,
17867
+ line,
17868
+ column,
17869
+ src: inlineScriptSrc
17870
+ });
17871
+ if (inlineAsRemote) {
17872
+ // prefere la version src
17873
+ scriptInfos.push({
17874
+ type,
17875
+ src: inlineScriptSrc
17876
+ });
17877
+ const remoteJsSupervised = generateCodeToSuperviseScriptWithSrc({
17878
+ type,
17879
+ src: inlineScriptSrc
17880
+ });
17881
+ mutations.push(() => {
17882
+ setHtmlNodeText(scriptNode, remoteJsSupervised);
17883
+ setHtmlNodeAttributes(scriptNode, {
17884
+ "jsenv-cooked-by": "jsenv:supervisor",
17885
+ "src": undefined,
17886
+ "inlined-from-src": inlineScriptSrc
17887
+ });
17888
+ });
17889
+ } else {
17890
+ scriptInfos.push({
17891
+ type,
17892
+ src: inlineScriptSrc,
17893
+ isInline: true
17894
+ });
17895
+ actions.push(async () => {
17896
+ try {
17897
+ const inlineJsSupervised = await injectSupervisorIntoJs({
17898
+ webServer,
17899
+ content: textContent,
17900
+ url: inlineScriptUrl,
17901
+ type,
17902
+ inlineSrc: inlineScriptSrc
17903
+ });
17904
+ mutations.push(() => {
17905
+ setHtmlNodeText(scriptNode, inlineJsSupervised);
17906
+ setHtmlNodeAttributes(scriptNode, {
17907
+ "jsenv-cooked-by": "jsenv:supervisor"
17908
+ });
17909
+ });
17910
+ } catch (e) {
17911
+ if (e.code === "PARSE_ERROR") {
17912
+ // mutations.push(() => {
17913
+ // setHtmlNodeAttributes(scriptNode, {
17914
+ // "jsenv-cooked-by": "jsenv:supervisor",
17915
+ // })
17916
+ // })
17917
+ // on touche a rien
17918
+ return;
17919
+ }
17920
+ throw e;
17921
+ }
17922
+ });
17923
+ }
17924
+ };
17925
+ const handleScriptWithSrc = (scriptNode, {
17926
+ type,
17927
+ src
17928
+ }) => {
17929
+ scriptInfos.push({
17930
+ type,
17931
+ src
17932
+ });
17933
+ const remoteJsSupervised = generateCodeToSuperviseScriptWithSrc({
17934
+ type,
17935
+ src
17936
+ });
17937
+ mutations.push(() => {
17938
+ setHtmlNodeText(scriptNode, remoteJsSupervised);
17939
+ setHtmlNodeAttributes(scriptNode, {
17940
+ "jsenv-cooked-by": "jsenv:supervisor",
17941
+ "src": undefined,
17942
+ "inlined-from-src": src
17943
+ });
17944
+ });
17945
+ };
17946
+ visitHtmlNodes(htmlAst, {
17947
+ script: scriptNode => {
17948
+ const {
17949
+ type,
17950
+ extension
17951
+ } = analyzeScriptNode(scriptNode);
17952
+ if (type !== "js_classic" && type !== "js_module") {
17953
+ return;
17954
+ }
17955
+ if (getHtmlNodeAttribute(scriptNode, "jsenv-injected-by")) {
17956
+ return;
17957
+ }
17958
+ const noSupervisor = getHtmlNodeAttribute(scriptNode, "no-supervisor");
17959
+ if (noSupervisor !== undefined) {
17960
+ return;
17961
+ }
17962
+ const scriptNodeText = getHtmlNodeText(scriptNode);
17963
+ if (scriptNodeText) {
17964
+ handleInlineScript(scriptNode, {
17965
+ type,
17966
+ extension,
17967
+ textContent: scriptNodeText
17968
+ });
17969
+ return;
17970
+ }
17971
+ const src = getHtmlNodeAttribute(scriptNode, "src");
17972
+ if (src) {
17973
+ handleScriptWithSrc(scriptNode, {
17974
+ type,
17975
+ src
17976
+ });
17977
+ return;
17978
+ }
17979
+ }
17980
+ });
17981
+ }
17982
+ // 2. Inject supervisor js file + setup call
17983
+ {
17984
+ const setupParamsSource = stringifyParams({
17985
+ ...supervisorOptions,
17986
+ serverIsJsenvDevServer: webServer.isJsenvDevServer,
17987
+ rootDirectoryUrl: webServer.rootDirectoryUrl,
17988
+ scriptInfos
17989
+ }, " ");
17990
+ injectScriptNodeAsEarlyAsPossible(htmlAst, createHtmlNode({
17991
+ tagName: "script",
17992
+ textContent: `
17993
+ window.__supervisor__.setup({
17994
+ ${setupParamsSource}
17995
+ })
17996
+ `
17997
+ }), "jsenv:supervisor");
17998
+ const supervisorScript = createHtmlNode({
17999
+ tagName: "script",
18000
+ src: supervisorScriptSrc
18001
+ });
18002
+ injectScriptNodeAsEarlyAsPossible(htmlAst, supervisorScript, "jsenv:supervisor");
18003
+ }
18004
+ // 3. Perform actions (transforming inline script content) and html mutations
18005
+ if (actions.length > 0) {
18006
+ await Promise.all(actions.map(action => action()));
18007
+ }
18008
+ mutations.forEach(mutation => mutation());
18009
+ const htmlModified = stringifyHtmlAst(htmlAst);
18010
+ return {
18011
+ content: htmlModified
18012
+ };
18013
+ };
18014
+ const stringifyParams = (params, prefix = "") => {
18015
+ const source = JSON.stringify(params, null, prefix);
18016
+ if (prefix.length) {
18017
+ // remove leading "{\n"
18018
+ // remove leading prefix
18019
+ // remove trailing "\n}"
18020
+ return source.slice(2 + prefix.length, -2);
18021
+ }
18022
+ // remove leading "{"
18023
+ // remove trailing "}"
18024
+ return source.slice(1, -1);
18025
+ };
18026
+ const generateCodeToSuperviseScriptWithSrc = ({
18027
+ type,
18028
+ src
18029
+ }) => {
18030
+ if (type === "js_module") {
18031
+ return `
18032
+ window.__supervisor__.superviseScriptTypeModule(${JSON.stringify(src)}, (url) => import(url));
18033
+ `;
18034
+ }
18035
+ return `
18036
+ window.__supervisor__.superviseScript(${JSON.stringify(src)});
18037
+ `;
18038
+ };
18039
+
18040
+ /*
18041
+ * This plugin provides a way for jsenv to know when js execution is done
18042
+ */
18043
+ const supervisorFileUrl = new URL("./js/supervisor.js", import.meta.url).href;
17530
18044
  const jsenvPluginSupervisor = ({
17531
18045
  logs = false,
17532
18046
  measurePerf = false,
@@ -17534,8 +18048,6 @@ const jsenvPluginSupervisor = ({
17534
18048
  openInEditor = true,
17535
18049
  errorBaseUrl
17536
18050
  }) => {
17537
- const supervisorFileUrl = new URL("./js/supervisor.js", import.meta.url).href;
17538
- const scriptTypeModuleSupervisorFileUrl = new URL("./js/script_type_module_supervisor.js", import.meta.url).href;
17539
18051
  return {
17540
18052
  name: "jsenv:supervisor",
17541
18053
  appliesDuring: "dev",
@@ -17677,170 +18189,50 @@ const jsenvPluginSupervisor = ({
17677
18189
  url,
17678
18190
  content
17679
18191
  }, context) => {
17680
- const htmlAst = parseHtmlString(content);
17681
- const scriptsToSupervise = [];
17682
- const handleInlineScript = (node, htmlNodeText) => {
17683
- const {
17684
- type,
17685
- extension
17686
- } = analyzeScriptNode(node);
17687
- const {
17688
- line,
17689
- column,
17690
- lineEnd,
17691
- columnEnd,
17692
- isOriginal
17693
- } = getHtmlNodePosition(node, {
17694
- preferOriginal: true
17695
- });
17696
- let inlineScriptUrl = generateInlineContentUrl({
17697
- url,
17698
- extension: extension || ".js",
17699
- line,
17700
- column,
17701
- lineEnd,
17702
- columnEnd
17703
- });
17704
- const [inlineScriptReference] = context.referenceUtils.foundInline({
17705
- type: "script",
17706
- subtype: "inline",
17707
- expectedType: type,
17708
- isOriginalPosition: isOriginal,
17709
- specifierLine: line - 1,
17710
- specifierColumn: column,
17711
- specifier: inlineScriptUrl,
17712
- contentType: "text/javascript",
17713
- content: htmlNodeText
17714
- });
17715
- removeHtmlNodeText(node);
17716
- if (extension) {
17717
- setHtmlNodeAttributes(node, {
17718
- type: type === "js_module" ? "module" : undefined
17719
- });
17720
- }
17721
- scriptsToSupervise.push({
17722
- node,
17723
- isInline: true,
17724
- type,
17725
- src: inlineScriptReference.generatedSpecifier
17726
- });
17727
- };
17728
- const handleScriptWithSrc = (node, src) => {
17729
- const {
17730
- type
17731
- } = analyzeScriptNode(node);
17732
- const integrity = getHtmlNodeAttribute(node, "integrity");
17733
- const crossorigin = getHtmlNodeAttribute(node, "crossorigin") !== undefined;
17734
- const defer = getHtmlNodeAttribute(node, "defer") !== undefined;
17735
- const async = getHtmlNodeAttribute(node, "async") !== undefined;
17736
- scriptsToSupervise.push({
17737
- node,
17738
- type,
17739
- src,
17740
- defer,
17741
- async,
17742
- integrity,
17743
- crossorigin
17744
- });
17745
- };
17746
- visitHtmlNodes(htmlAst, {
17747
- script: node => {
17748
- const {
17749
- type
17750
- } = analyzeScriptNode(node);
17751
- if (type !== "js_classic" && type !== "js_module") {
17752
- return;
17753
- }
17754
- if (getHtmlNodeAttribute(node, "jsenv-cooked-by") || getHtmlNodeAttribute(node, "jsenv-inlined-by") || getHtmlNodeAttribute(node, "jsenv-injected-by")) {
17755
- return;
17756
- }
17757
- const noSupervisor = getHtmlNodeAttribute(node, "no-supervisor");
17758
- if (noSupervisor !== undefined) {
17759
- return;
17760
- }
17761
- const htmlNodeText = getHtmlNodeText(node);
17762
- if (htmlNodeText) {
17763
- handleInlineScript(node, htmlNodeText);
17764
- return;
17765
- }
17766
- const src = getHtmlNodeAttribute(node, "src");
17767
- if (src) {
17768
- handleScriptWithSrc(node, src);
17769
- return;
17770
- }
17771
- }
17772
- });
17773
- const [scriptTypeModuleSupervisorFileReference] = context.referenceUtils.inject({
17774
- type: "js_import",
17775
- expectedType: "js_module",
17776
- specifier: scriptTypeModuleSupervisorFileUrl
17777
- });
17778
18192
  const [supervisorFileReference] = context.referenceUtils.inject({
17779
18193
  type: "script",
17780
18194
  expectedType: "js_classic",
17781
18195
  specifier: supervisorFileUrl
17782
18196
  });
17783
- injectScriptNodeAsEarlyAsPossible(htmlAst, createHtmlNode({
17784
- tagName: "script",
17785
- textContent: `
17786
- window.__supervisor__.setup(${JSON.stringify({
17787
- rootDirectoryUrl: context.rootDirectoryUrl,
18197
+ return injectSupervisorIntoHTML({
18198
+ content,
18199
+ url
18200
+ }, {
18201
+ supervisorScriptSrc: supervisorFileReference.generatedSpecifier,
18202
+ supervisorOptions: {
17788
18203
  errorBaseUrl,
17789
18204
  logs,
17790
18205
  measurePerf,
17791
18206
  errorOverlay,
17792
18207
  openInEditor
17793
- }, null, " ")})
17794
- `
17795
- }), "jsenv:supervisor");
17796
- injectScriptNodeAsEarlyAsPossible(htmlAst, createHtmlNode({
17797
- tagName: "script",
17798
- src: supervisorFileReference.generatedSpecifier
17799
- }), "jsenv:supervisor");
17800
- scriptsToSupervise.forEach(({
17801
- node,
17802
- isInline,
17803
- type,
17804
- src,
17805
- defer,
17806
- async,
17807
- integrity,
17808
- crossorigin
17809
- }) => {
17810
- const paramsAsJson = JSON.stringify({
17811
- src,
17812
- isInline,
17813
- defer,
17814
- async,
17815
- integrity,
17816
- crossorigin
17817
- });
17818
- if (type === "js_module") {
17819
- setHtmlNodeText(node, `
17820
- import { superviseScriptTypeModule } from ${scriptTypeModuleSupervisorFileReference.generatedSpecifier}
17821
- superviseScriptTypeModule(${paramsAsJson})
17822
- `);
17823
- } else {
17824
- setHtmlNodeText(node, `
17825
- window.__supervisor__.superviseScript(${paramsAsJson})
17826
- `);
17827
- }
17828
- if (src) {
17829
- setHtmlNodeAttributes(node, {
17830
- "jsenv-inlined-by": "jsenv:supervisor",
17831
- "src": undefined,
17832
- "inlined-from-src": src
17833
- });
17834
- } else {
17835
- setHtmlNodeAttributes(node, {
17836
- "jsenv-cooked-by": "jsenv:supervisor"
18208
+ },
18209
+ webServer: {
18210
+ rootDirectoryUrl: context.rootDirectoryUrl,
18211
+ isJsenvDevServer: true
18212
+ },
18213
+ inlineAsRemote: true,
18214
+ generateInlineScriptSrc: ({
18215
+ type,
18216
+ textContent,
18217
+ inlineScriptUrl,
18218
+ isOriginal,
18219
+ line,
18220
+ column
18221
+ }) => {
18222
+ const [inlineScriptReference] = context.referenceUtils.foundInline({
18223
+ type: "script",
18224
+ subtype: "inline",
18225
+ expectedType: type,
18226
+ isOriginalPosition: isOriginal,
18227
+ specifierLine: line - 1,
18228
+ specifierColumn: column,
18229
+ specifier: inlineScriptUrl,
18230
+ contentType: "text/javascript",
18231
+ content: textContent
17837
18232
  });
18233
+ return inlineScriptReference.generatedSpecifier;
17838
18234
  }
17839
18235
  });
17840
- const htmlModified = stringifyHtmlAst(htmlAst);
17841
- return {
17842
- content: htmlModified
17843
- };
17844
18236
  }
17845
18237
  }
17846
18238
  };
@@ -19451,7 +19843,12 @@ const jsenvPluginBabel = ({
19451
19843
  map
19452
19844
  } = await applyBabelPlugins({
19453
19845
  babelPlugins,
19454
- urlInfo
19846
+ urlInfo,
19847
+ options: {
19848
+ generatorOpts: {
19849
+ retainLines: context.dev
19850
+ }
19851
+ }
19455
19852
  });
19456
19853
  return {
19457
19854
  content: code,
@@ -19539,7 +19936,7 @@ const babelPluginMetadataUsesTopLevelAwait = () => {
19539
19936
  programPath.traverse({
19540
19937
  AwaitExpression: path => {
19541
19938
  const closestFunction = path.getFunctionParent();
19542
- if (!closestFunction) {
19939
+ if (!closestFunction || closestFunction.type === "Program") {
19543
19940
  usesTopLevelAwait = true;
19544
19941
  path.stop();
19545
19942
  }
@@ -20418,9 +20815,10 @@ const jsenvPluginRibbon = ({
20418
20815
  tagName: "script",
20419
20816
  type: "module",
20420
20817
  textContent: `
20421
- import { injectRibbon} from "${ribbonClientFileReference.generatedSpecifier}"
20818
+ import { injectRibbon } from "${ribbonClientFileReference.generatedSpecifier}"
20422
20819
 
20423
- injectRibbon(${paramsJson})`
20820
+ injectRibbon(${paramsJson})
20821
+ `
20424
20822
  });
20425
20823
  injectHtmlNode(htmlAst, scriptNode, "jsenv:ribbon");
20426
20824
  return stringifyHtmlAst(htmlAst);
@@ -20468,13 +20866,13 @@ const getCorePlugins = ({
20468
20866
  return [jsenvPluginUrlAnalysis({
20469
20867
  rootDirectoryUrl,
20470
20868
  ...urlAnalysis
20471
- }), jsenvPluginTranspilation(transpilation), ...(supervisor ? [jsenvPluginSupervisor(supervisor)] : []),
20472
- // before inline as it turns inline <script> into <script src>
20473
- jsenvPluginImportmap(),
20869
+ }), jsenvPluginTranspilation(transpilation), jsenvPluginImportmap(),
20474
20870
  // before node esm to handle bare specifiers
20475
20871
  // + before node esm to handle importmap before inline content
20476
20872
  jsenvPluginInline(),
20477
20873
  // before "file urls" to resolve and load inline urls
20874
+ ...(supervisor ? [jsenvPluginSupervisor(supervisor)] : []),
20875
+ // after inline as it needs inline script to be cooked
20478
20876
  jsenvPluginFileUrls({
20479
20877
  directoryReferenceAllowed,
20480
20878
  ...fileSystemMagicRedirection
@@ -22387,7 +22785,7 @@ const createFileService = ({
22387
22785
  const {
22388
22786
  runtimeName,
22389
22787
  runtimeVersion
22390
- } = parseUserAgentHeader(request.headers["user-agent"]);
22788
+ } = parseUserAgentHeader(request.headers["user-agent"] || "");
22391
22789
  const runtimeId = `${runtimeName}@${runtimeVersion}`;
22392
22790
  const existingContext = contextCache.get(runtimeId);
22393
22791
  if (existingContext) {
@@ -22846,7 +23244,7 @@ const startDevServer = async ({
22846
23244
  stopOnExit: false,
22847
23245
  stopOnSIGINT: handleSIGINT,
22848
23246
  stopOnInternalError: false,
22849
- keepProcessAlive,
23247
+ keepProcessAlive: process.env.IMPORTED_BY_TEST_PLAN ? false : keepProcessAlive,
22850
23248
  logLevel: serverLogLevel,
22851
23249
  startLog: false,
22852
23250
  https,
@@ -22855,7 +23253,13 @@ const startDevServer = async ({
22855
23253
  hostname,
22856
23254
  port,
22857
23255
  requestWaitingMs: 60_000,
22858
- services: [jsenvServiceCORS({
23256
+ services: [{
23257
+ injectResponseHeaders: () => {
23258
+ return {
23259
+ "x-server-name": "jsenv_dev_server"
23260
+ };
23261
+ }
23262
+ }, jsenvServiceCORS({
22859
23263
  accessControlAllowRequestOrigin: true,
22860
23264
  accessControlAllowRequestMethod: true,
22861
23265
  accessControlAllowRequestHeaders: true,
@@ -22864,7 +23268,7 @@ const startDevServer = async ({
22864
23268
  timingAllowOrigin: true
22865
23269
  }), {
22866
23270
  handleRequest: request => {
22867
- if (request.pathname === "/__server_params__.json") {
23271
+ if (request.pathname === "/__params__.json") {
22868
23272
  const json = JSON.stringify({
22869
23273
  sourceDirectoryUrl
22870
23274
  });
@@ -22877,12 +23281,6 @@ const startDevServer = async ({
22877
23281
  body: json
22878
23282
  };
22879
23283
  }
22880
- if (request.pathname === "/__stop__") {
22881
- server.stop();
22882
- return {
22883
- status: 200
22884
- };
22885
- }
22886
23284
  return null;
22887
23285
  }
22888
23286
  }, ...services, {
@@ -23041,18 +23439,28 @@ const basicFetch = async (url, {
23041
23439
  headers
23042
23440
  });
23043
23441
  req.on("response", response => {
23044
- req.setTimeout(0);
23045
- let responseBody = "";
23046
- response.setEncoding("utf8");
23047
- response.on("data", chunk => {
23048
- responseBody += chunk;
23049
- });
23050
- response.on("end", () => {
23051
- req.destroy();
23052
- if (response.headers["content-type"] === "application/json") {
23053
- resolve(JSON.parse(responseBody));
23054
- } else {
23055
- resolve(responseBody);
23442
+ resolve({
23443
+ status: response.statusCode,
23444
+ headers: response.headers,
23445
+ json: () => {
23446
+ req.setTimeout(0);
23447
+ req.destroy();
23448
+ return new Promise(resolve => {
23449
+ if (response.headers["content-type"] !== "application/json") {
23450
+ console.warn("not json");
23451
+ }
23452
+ let responseBody = "";
23453
+ response.setEncoding("utf8");
23454
+ response.on("data", chunk => {
23455
+ responseBody += chunk;
23456
+ });
23457
+ response.on("end", () => {
23458
+ resolve(JSON.parse(responseBody));
23459
+ });
23460
+ response.on("error", e => {
23461
+ reject(e);
23462
+ });
23463
+ });
23056
23464
  }
23057
23465
  });
23058
23466
  });
@@ -23061,6 +23469,57 @@ const basicFetch = async (url, {
23061
23469
  });
23062
23470
  };
23063
23471
 
23472
+ const assertAndNormalizeWebServer = async webServer => {
23473
+ if (!webServer) {
23474
+ throw new TypeError(`webServer is required when running tests on browser(s)`);
23475
+ }
23476
+ const unexpectedParamNames = Object.keys(webServer).filter(key => {
23477
+ return !["origin", "moduleUrl", "rootDirectoryUrl"].includes(key);
23478
+ });
23479
+ if (unexpectedParamNames.length > 0) {
23480
+ throw new TypeError(`${unexpectedParamNames.join(",")}: there is no such param to webServer`);
23481
+ }
23482
+ let aServerIsListening = await pingServer(webServer.origin);
23483
+ if (!aServerIsListening) {
23484
+ if (!webServer.moduleUrl) {
23485
+ throw new TypeError(`webServer.moduleUrl is required as there is no server listening "${webServer.origin}"`);
23486
+ }
23487
+ try {
23488
+ process.env.IMPORTED_BY_TEST_PLAN = "1";
23489
+ await import(webServer.moduleUrl);
23490
+ delete process.env.IMPORTED_BY_TEST_PLAN;
23491
+ } catch (e) {
23492
+ if (e.code === "ERR_MODULE_NOT_FOUND") {
23493
+ throw new Error(`webServer.moduleUrl does not lead to a file at "${webServer.moduleUrl}"`);
23494
+ }
23495
+ throw e;
23496
+ }
23497
+ aServerIsListening = await pingServer(webServer.origin);
23498
+ if (!aServerIsListening) {
23499
+ throw new Error(`webServer.moduleUrl did not start a server listening at "${webServer.origin}", check file at "${webServer.moduleUrl}"`);
23500
+ }
23501
+ }
23502
+ const {
23503
+ headers
23504
+ } = await basicFetch(webServer.origin);
23505
+ if (headers["x-server-name"] === "jsenv_dev_server") {
23506
+ webServer.isJsenvDevServer = true;
23507
+ const {
23508
+ json
23509
+ } = await basicFetch(`${webServer.origin}/__params__.json`, {
23510
+ rejectUnauthorized: false
23511
+ });
23512
+ if (webServer.rootDirectoryUrl === undefined) {
23513
+ const jsenvDevServerParams = await json();
23514
+ webServer.rootDirectoryUrl = jsenvDevServerParams.sourceDirectoryUrl;
23515
+ } else {
23516
+ webServer.rootDirectoryUrl = assertAndNormalizeDirectoryUrl(webServer.rootDirectoryUrl, "webServer.rootDirectoryUrl");
23517
+ }
23518
+ } else {
23519
+ webServer.rootDirectoryUrl = assertAndNormalizeDirectoryUrl(webServer.rootDirectoryUrl, "webServer.rootDirectoryUrl");
23520
+ }
23521
+ };
23522
+
23064
23523
  const generateCoverageJsonFile = async ({
23065
23524
  coverage,
23066
23525
  coverageJsonFileUrl,
@@ -23887,29 +24346,49 @@ const createExecutionLog = ({
23887
24346
  timeEllapsed,
23888
24347
  memoryHeap
23889
24348
  });
24349
+ let log;
23890
24350
  if (completedExecutionLogAbbreviation && status === "completed") {
23891
- return `${description}${summary}`;
24351
+ log = `${description}${summary}`;
24352
+ } else {
24353
+ const {
24354
+ consoleCalls = [],
24355
+ errors = []
24356
+ } = executionResult;
24357
+ const consoleOutput = formatConsoleCalls(consoleCalls);
24358
+ const errorsOutput = formatErrors(errors);
24359
+ log = formatExecution({
24360
+ label: `${description}${summary}`,
24361
+ details: {
24362
+ file: fileRelativeUrl,
24363
+ ...(logRuntime ? {
24364
+ runtime: `${runtimeName}/${runtimeVersion}`
24365
+ } : {}),
24366
+ ...(logEachDuration ? {
24367
+ duration: status === "executing" ? msAsEllapsedTime(Date.now() - startMs) : msAsDuration(endMs - startMs)
24368
+ } : {})
24369
+ },
24370
+ consoleOutput,
24371
+ errorsOutput
24372
+ });
23892
24373
  }
23893
24374
  const {
23894
- consoleCalls = [],
23895
- errors = []
23896
- } = executionResult;
23897
- const consoleOutput = formatConsoleCalls(consoleCalls);
23898
- const errorsOutput = formatErrors(errors);
23899
- return formatExecution({
23900
- label: `${description}${summary}`,
23901
- details: {
23902
- file: fileRelativeUrl,
23903
- ...(logRuntime ? {
23904
- runtime: `${runtimeName}/${runtimeVersion}`
23905
- } : {}),
23906
- ...(logEachDuration ? {
23907
- duration: status === "executing" ? msAsEllapsedTime(Date.now() - startMs) : msAsDuration(endMs - startMs)
23908
- } : {})
23909
- },
23910
- consoleOutput,
23911
- errorsOutput
24375
+ columns = 80
24376
+ } = process.stdout;
24377
+ log = wrapAnsi(log, columns, {
24378
+ trim: false,
24379
+ hard: true,
24380
+ wordWrap: false
23912
24381
  });
24382
+ if (endMs) {
24383
+ if (completedExecutionLogAbbreviation) {
24384
+ return `${log}\n`;
24385
+ }
24386
+ if (executionIndex === counters.total - 1) {
24387
+ return `${log}\n`;
24388
+ }
24389
+ return `${log}\n\n`;
24390
+ }
24391
+ return log;
23913
24392
  };
23914
24393
  const formatErrors = errors => {
23915
24394
  if (errors.length === 0) {
@@ -24022,39 +24501,51 @@ const descriptionFormatters = {
24022
24501
  index,
24023
24502
  total
24024
24503
  }) => {
24025
- return ANSI.color(`executing ${index + 1} of ${total}`, EXECUTION_COLORS.executing);
24504
+ return ANSI.color(`executing ${padNumber(index, total)} of ${total}`, EXECUTION_COLORS.executing);
24026
24505
  },
24027
24506
  aborted: ({
24028
24507
  index,
24029
24508
  total
24030
24509
  }) => {
24031
- return ANSI.color(`${UNICODE.FAILURE_RAW} execution ${index + 1} of ${total} aborted`, EXECUTION_COLORS.aborted);
24510
+ return ANSI.color(`${UNICODE.FAILURE_RAW} execution ${padNumber(index, total)} of ${total} aborted`, EXECUTION_COLORS.aborted);
24032
24511
  },
24033
24512
  timedout: ({
24034
24513
  index,
24035
24514
  total,
24036
24515
  executionParams
24037
24516
  }) => {
24038
- return ANSI.color(`${UNICODE.FAILURE_RAW} execution ${index + 1} of ${total} timeout after ${executionParams.allocatedMs}ms`, EXECUTION_COLORS.timedout);
24517
+ return ANSI.color(`${UNICODE.FAILURE_RAW} execution ${padNumber(index, total)} of ${total} timeout after ${executionParams.allocatedMs}ms`, EXECUTION_COLORS.timedout);
24039
24518
  },
24040
24519
  failed: ({
24041
24520
  index,
24042
24521
  total
24043
24522
  }) => {
24044
- return ANSI.color(`${UNICODE.FAILURE_RAW} execution ${index + 1} of ${total} failed`, EXECUTION_COLORS.failed);
24523
+ return ANSI.color(`${UNICODE.FAILURE_RAW} execution ${padNumber(index, total)} of ${total} failed`, EXECUTION_COLORS.failed);
24045
24524
  },
24046
24525
  completed: ({
24047
24526
  index,
24048
24527
  total
24049
24528
  }) => {
24050
- return ANSI.color(`${UNICODE.OK_RAW} execution ${index + 1} of ${total} completed`, EXECUTION_COLORS.completed);
24529
+ return ANSI.color(`${UNICODE.OK_RAW} execution ${padNumber(index, total)} of ${total} completed`, EXECUTION_COLORS.completed);
24051
24530
  },
24052
24531
  cancelled: ({
24053
24532
  index,
24054
24533
  total
24055
24534
  }) => {
24056
- return ANSI.color(`${UNICODE.FAILURE_RAW} execution ${index + 1} of ${total} cancelled`, EXECUTION_COLORS.cancelled);
24535
+ return ANSI.color(`${UNICODE.FAILURE_RAW} execution ${padNumber(index, total)} of ${total} cancelled`, EXECUTION_COLORS.cancelled);
24536
+ }
24537
+ };
24538
+ const padNumber = (index, total) => {
24539
+ const number = index + 1;
24540
+ const numberWidth = String(number).length;
24541
+ const totalWith = String(total).length;
24542
+ let missingWidth = totalWith - numberWidth;
24543
+ let padded = "";
24544
+ while (missingWidth--) {
24545
+ padded += "0";
24057
24546
  }
24547
+ padded += number;
24548
+ return padded;
24058
24549
  };
24059
24550
  const formatConsoleCalls = consoleCalls => {
24060
24551
  if (consoleCalls.length === 0) {
@@ -24200,8 +24691,7 @@ const executeSteps = async (executionSteps, {
24200
24691
  completedExecutionLogMerging,
24201
24692
  completedExecutionLogAbbreviation,
24202
24693
  rootDirectoryUrl,
24203
- devServerOrigin,
24204
- sourceDirectoryUrl,
24694
+ webServer,
24205
24695
  keepRunning,
24206
24696
  defaultMsAllocatedPerExecution,
24207
24697
  maxExecutionsInParallel,
@@ -24280,8 +24770,7 @@ const executeSteps = async (executionSteps, {
24280
24770
  }
24281
24771
  let runtimeParams = {
24282
24772
  rootDirectoryUrl,
24283
- devServerOrigin,
24284
- sourceDirectoryUrl,
24773
+ webServer,
24285
24774
  coverageEnabled,
24286
24775
  coverageConfig,
24287
24776
  coverageMethodForBrowsers,
@@ -24425,7 +24914,7 @@ const executeSteps = async (executionSteps, {
24425
24914
  global.gc();
24426
24915
  }
24427
24916
  if (executionLogsEnabled) {
24428
- let log = createExecutionLog(afterExecutionInfo, {
24917
+ const log = createExecutionLog(afterExecutionInfo, {
24429
24918
  completedExecutionLogAbbreviation,
24430
24919
  counters,
24431
24920
  logRuntime,
@@ -24437,18 +24926,6 @@ const executeSteps = async (executionSteps, {
24437
24926
  memoryHeap: memoryUsage().heapUsed
24438
24927
  } : {})
24439
24928
  });
24440
- log = `${log}
24441
-
24442
- `;
24443
- const {
24444
- columns = 80
24445
- } = process.stdout;
24446
- log = wrapAnsi(log, columns, {
24447
- trim: false,
24448
- hard: true,
24449
- wordWrap: false
24450
- });
24451
-
24452
24929
  // replace spinner with this execution result
24453
24930
  if (spinner) spinner.stop();
24454
24931
  executionLog.write(log);
@@ -24466,7 +24943,12 @@ const executeSteps = async (executionSteps, {
24466
24943
  });
24467
24944
  }
24468
24945
  }
24469
- if (failFast && executionResult.status !== "completed" && counters.done < counters.total) {
24946
+ const isLastExecutionLog = executionIndex === executionSteps.length - 1;
24947
+ const cancelRemaining = failFast && executionResult.status !== "completed" && counters.done < counters.total;
24948
+ if (isLastExecutionLog) {
24949
+ executionLog.write("\n");
24950
+ }
24951
+ if (cancelRemaining) {
24470
24952
  logger.info(`"failFast" enabled -> cancel remaining executions`);
24471
24953
  failFastAbortController.abort();
24472
24954
  }
@@ -24571,7 +25053,7 @@ const executeInParallel = async ({
24571
25053
  * Execute a list of files and log how it goes.
24572
25054
  * @param {Object} testPlanParameters
24573
25055
  * @param {string|url} testPlanParameters.rootDirectoryUrl Directory containing test files;
24574
- * @param {string|url} [testPlanParameters.devServerOrigin=undefined] Jsenv dev server origin; required when executing test on browsers
25056
+ * @param {Object} [testPlanParameters.webServer] Web server info; required when executing test on browsers
24575
25057
  * @param {Object} testPlanParameters.testPlan Object associating files with runtimes where they will be executed
24576
25058
  * @param {boolean} [testPlanParameters.completedExecutionLogAbbreviation=false] Abbreviate completed execution information to shorten terminal output
24577
25059
  * @param {boolean} [testPlanParameters.completedExecutionLogMerging=false] Merge completed execution logs to shorten terminal output
@@ -24598,8 +25080,7 @@ const executeTestPlan = async ({
24598
25080
  completedExecutionLogAbbreviation = false,
24599
25081
  completedExecutionLogMerging = false,
24600
25082
  rootDirectoryUrl,
24601
- devServerModuleUrl,
24602
- devServerOrigin,
25083
+ webServer,
24603
25084
  testPlan,
24604
25085
  updateProcessExitCode = true,
24605
25086
  maxExecutionsInParallel = 1,
@@ -24644,8 +25125,6 @@ const executeTestPlan = async ({
24644
25125
  }) => {
24645
25126
  let someNeedsServer = false;
24646
25127
  let someNodeRuntime = false;
24647
- let stopDevServerNeeded = false;
24648
- let sourceDirectoryUrl;
24649
25128
  const runtimes = {};
24650
25129
  // param validation
24651
25130
  {
@@ -24680,34 +25159,7 @@ const executeTestPlan = async ({
24680
25159
  });
24681
25160
  });
24682
25161
  if (someNeedsServer) {
24683
- if (!devServerOrigin) {
24684
- throw new TypeError(`devServerOrigin is required when running tests on browser(s)`);
24685
- }
24686
- let devServerStarted = await pingServer(devServerOrigin);
24687
- if (!devServerStarted) {
24688
- if (!devServerModuleUrl) {
24689
- throw new TypeError(`devServerModuleUrl is required when dev server is not started in order to run tests on browser(s)`);
24690
- }
24691
- try {
24692
- process.env.IMPORTED_BY_TEST_PLAN = "1";
24693
- await import(devServerModuleUrl);
24694
- delete process.env.IMPORTED_BY_TEST_PLAN;
24695
- } catch (e) {
24696
- if (e.code === "ERR_MODULE_NOT_FOUND") {
24697
- throw new Error(`Cannot find file responsible to start dev server at "${devServerModuleUrl}"`);
24698
- }
24699
- throw e;
24700
- }
24701
- devServerStarted = await pingServer(devServerOrigin);
24702
- if (!devServerStarted) {
24703
- throw new Error(`dev server not started after importing "${devServerModuleUrl}", ensure this module file is starting a server at "${devServerOrigin}"`);
24704
- }
24705
- stopDevServerNeeded = true;
24706
- }
24707
- const devServerParams = await basicFetch(`${devServerOrigin}/__server_params__.json`, {
24708
- rejectUnauthorized: false
24709
- });
24710
- sourceDirectoryUrl = devServerParams.sourceDirectoryUrl;
25162
+ await assertAndNormalizeWebServer(webServer);
24711
25163
  }
24712
25164
  if (coverageEnabled) {
24713
25165
  if (typeof coverageConfig !== "object") {
@@ -24785,6 +25237,8 @@ const executeTestPlan = async ({
24785
25237
  }
24786
25238
  }
24787
25239
  testPlan = {
25240
+ "file:///**/node_modules/": null,
25241
+ "**/*./": null,
24788
25242
  ...testPlan,
24789
25243
  "**/.jsenv/": null
24790
25244
  };
@@ -24809,8 +25263,7 @@ const executeTestPlan = async ({
24809
25263
  completedExecutionLogMerging,
24810
25264
  completedExecutionLogAbbreviation,
24811
25265
  rootDirectoryUrl,
24812
- devServerOrigin,
24813
- sourceDirectoryUrl,
25266
+ webServer,
24814
25267
  maxExecutionsInParallel,
24815
25268
  defaultMsAllocatedPerExecution,
24816
25269
  failFast,
@@ -24825,17 +25278,6 @@ const executeTestPlan = async ({
24825
25278
  coverageV8ConflictWarning,
24826
25279
  coverageTempDirectoryUrl
24827
25280
  });
24828
- if (stopDevServerNeeded) {
24829
- // we are expecting ECONNRESET because server will be stopped by the request
24830
- basicFetch(`${devServerOrigin}/__stop__`, {
24831
- rejectUnauthorized: false
24832
- }).catch(e => {
24833
- if (e.code === "ECONNRESET") {
24834
- return;
24835
- }
24836
- throw e;
24837
- });
24838
- }
24839
25281
  if (updateProcessExitCode && result.planSummary.counters.total !== result.planSummary.counters.completed) {
24840
25282
  process.exitCode = 1;
24841
25283
  }
@@ -24884,7 +25326,7 @@ const createRuntimeFromPlaywright = ({
24884
25326
  browserName,
24885
25327
  browserVersion,
24886
25328
  coveragePlaywrightAPIAvailable = false,
24887
- ignoreErrorHook = () => false,
25329
+ shouldIgnoreError = () => false,
24888
25330
  transformErrorHook = error => error,
24889
25331
  isolatedTab = false
24890
25332
  }) => {
@@ -24898,9 +25340,8 @@ const createRuntimeFromPlaywright = ({
24898
25340
  signal = new AbortController().signal,
24899
25341
  logger,
24900
25342
  rootDirectoryUrl,
25343
+ webServer,
24901
25344
  fileRelativeUrl,
24902
- devServerOrigin,
24903
- sourceDirectoryUrl,
24904
25345
  // measurePerformance,
24905
25346
  collectPerformance,
24906
25347
  coverageEnabled = false,
@@ -24915,6 +25356,19 @@ const createRuntimeFromPlaywright = ({
24915
25356
  playwrightLaunchOptions = {},
24916
25357
  ignoreHTTPSErrors = true
24917
25358
  }) => {
25359
+ const fileUrl = new URL(fileRelativeUrl, rootDirectoryUrl).href;
25360
+ if (!urlIsInsideOf(fileUrl, webServer.rootDirectoryUrl)) {
25361
+ throw new Error(`Cannot execute file that is outside web server root directory
25362
+ --- file ---
25363
+ ${fileUrl}
25364
+ --- web server root directory url ---
25365
+ ${webServer.rootDirectoryUrl}`);
25366
+ }
25367
+ const fileServerUrl = moveUrl({
25368
+ url: fileUrl,
25369
+ from: webServer.rootDirectoryUrl,
25370
+ to: `${webServer.origin}/`
25371
+ });
24918
25372
  const cleanupCallbackList = createCallbackListNotifiedOnce();
24919
25373
  const cleanup = memoize(async reason => {
24920
25374
  await cleanupCallbackList.notify({
@@ -24978,6 +25432,13 @@ const createRuntimeFromPlaywright = ({
24978
25432
  } : {})
24979
25433
  }
24980
25434
  });
25435
+ if (!webServer.isJsenvDevServer) {
25436
+ await initJsExecutionMiddleware(page, {
25437
+ webServer,
25438
+ fileUrl,
25439
+ fileServerUrl
25440
+ });
25441
+ }
24981
25442
  const closePage = async () => {
24982
25443
  try {
24983
25444
  await page.close();
@@ -25006,8 +25467,8 @@ const createRuntimeFromPlaywright = ({
25006
25467
  const v8CoveragesWithFsUrls = v8CoveragesWithWebUrls.map(v8CoveragesWithWebUrl => {
25007
25468
  const fsUrl = moveUrl({
25008
25469
  url: v8CoveragesWithWebUrl.url,
25009
- from: `${devServerOrigin}/`,
25010
- to: sourceDirectoryUrl
25470
+ from: `${webServer.origin}/`,
25471
+ to: webServer.rootDirectoryUrl
25011
25472
  });
25012
25473
  return {
25013
25474
  ...v8CoveragesWithWebUrl,
@@ -25069,19 +25530,7 @@ const createRuntimeFromPlaywright = ({
25069
25530
  result.performance = performance;
25070
25531
  });
25071
25532
  }
25072
- const fileUrl = new URL(fileRelativeUrl, rootDirectoryUrl).href;
25073
- if (!urlIsInsideOf(fileUrl, sourceDirectoryUrl)) {
25074
- throw new Error(`Cannot execute file that is outside source directory
25075
- --- file ---
25076
- ${fileUrl}
25077
- --- source directory ---
25078
- ${sourceDirectoryUrl}`);
25079
- }
25080
- const fileDevServerUrl = moveUrl({
25081
- url: fileUrl,
25082
- from: sourceDirectoryUrl,
25083
- to: `${devServerOrigin}/`
25084
- });
25533
+
25085
25534
  // https://github.com/GoogleChrome/puppeteer/blob/v1.4.0/docs/api.md#event-console
25086
25535
  const removeConsoleListener = registerEvent({
25087
25536
  object: page,
@@ -25109,7 +25558,7 @@ ${sourceDirectoryUrl}`);
25109
25558
  object: page,
25110
25559
  eventType: "error",
25111
25560
  callback: error => {
25112
- if (ignoreErrorHook(error)) {
25561
+ if (shouldIgnoreError(error, "error")) {
25113
25562
  return;
25114
25563
  }
25115
25564
  cb(transformErrorHook(error));
@@ -25122,7 +25571,10 @@ ${sourceDirectoryUrl}`);
25122
25571
  // object: page,
25123
25572
  // eventType: "pageerror",
25124
25573
  // callback: (error) => {
25125
- // if (ignoreErrorHook(error)) {
25574
+ // if (
25575
+ // webServer.isJsenvDevServer ||
25576
+ // shouldIgnoreError(error, "pageerror")
25577
+ // ) {
25126
25578
  // return
25127
25579
  // }
25128
25580
  // result.errors.push(transformErrorHook(error))
@@ -25164,16 +25616,28 @@ ${sourceDirectoryUrl}`);
25164
25616
  },
25165
25617
  response: async cb => {
25166
25618
  try {
25167
- await page.goto(fileDevServerUrl, {
25619
+ await page.goto(fileServerUrl, {
25168
25620
  timeout: 0
25169
25621
  });
25170
25622
  const returnValue = await page.evaluate( /* eslint-disable no-undef */
25171
25623
  /* istanbul ignore next */
25172
- () => {
25624
+ async () => {
25625
+ let startTime;
25626
+ try {
25627
+ startTime = window.performance.timing.navigationStart;
25628
+ } catch (e) {
25629
+ startTime = Date.now();
25630
+ }
25173
25631
  if (!window.__supervisor__) {
25174
- throw new Error(`window.__supervisor__ not found`);
25632
+ throw new Error("window.__supervisor__ is undefined");
25175
25633
  }
25176
- return window.__supervisor__.getDocumentExecutionResult();
25634
+ const executionResultFromJsenvSupervisor = await window.__supervisor__.getDocumentExecutionResult();
25635
+ return {
25636
+ type: "window_supervisor",
25637
+ startTime,
25638
+ endTime: Date.now(),
25639
+ executionResults: executionResultFromJsenvSupervisor.executionResults
25640
+ };
25177
25641
  }
25178
25642
  /* eslint-enable no-undef */);
25179
25643
 
@@ -25196,12 +25660,18 @@ ${sourceDirectoryUrl}`);
25196
25660
  result.errors.push(error);
25197
25661
  return;
25198
25662
  }
25663
+ if (winner.name === "pageerror") {
25664
+ let error = winner.data;
25665
+ result.status = "failed";
25666
+ result.errors.push(error);
25667
+ return;
25668
+ }
25199
25669
  if (winner.name === "closed") {
25200
25670
  result.status = "failed";
25201
25671
  result.errors.push(isBrowserDedicatedToExecution ? new Error(`browser disconnected during execution`) : new Error(`page closed during execution`));
25202
25672
  return;
25203
25673
  }
25204
- // winner.name = 'response'
25674
+ // winner.name === "response"
25205
25675
  const {
25206
25676
  executionResults
25207
25677
  } = winner.data;
@@ -25211,10 +25681,17 @@ ${sourceDirectoryUrl}`);
25211
25681
  const executionResult = executionResults[key];
25212
25682
  if (executionResult.status === "failed") {
25213
25683
  result.status = "failed";
25214
- result.errors.push({
25215
- ...executionResult.exception,
25216
- stack: executionResult.exception.text
25217
- });
25684
+ if (executionResult.exception) {
25685
+ result.errors.push({
25686
+ ...executionResult.exception,
25687
+ stack: executionResult.exception.text
25688
+ });
25689
+ } else {
25690
+ result.errors.push({
25691
+ ...executionResult.error,
25692
+ stack: executionResult.error.stack
25693
+ });
25694
+ }
25218
25695
  }
25219
25696
  });
25220
25697
  };
@@ -25243,7 +25720,7 @@ ${sourceDirectoryUrl}`);
25243
25720
  browserName,
25244
25721
  browserVersion,
25245
25722
  coveragePlaywrightAPIAvailable,
25246
- ignoreErrorHook,
25723
+ shouldIgnoreError,
25247
25724
  transformErrorHook,
25248
25725
  isolatedTab: true
25249
25726
  });
@@ -25361,6 +25838,106 @@ const extractTextFromConsoleMessage = consoleMessage => {
25361
25838
  // return text
25362
25839
  };
25363
25840
 
25841
+ const initJsExecutionMiddleware = async (page, {
25842
+ webServer,
25843
+ fileUrl,
25844
+ fileServerUrl
25845
+ }) => {
25846
+ const inlineScriptContents = new Map();
25847
+ const interceptHtmlToExecute = async ({
25848
+ route
25849
+ }) => {
25850
+ // Fetch original response.
25851
+ const response = await route.fetch();
25852
+ // Add a prefix to the title.
25853
+ const originalBody = await response.text();
25854
+ const injectionResult = await injectSupervisorIntoHTML({
25855
+ content: originalBody,
25856
+ url: fileUrl
25857
+ }, {
25858
+ supervisorScriptSrc: `/@fs/${supervisorFileUrl$1.slice("file:///".length)}`,
25859
+ supervisorOptions: {},
25860
+ inlineAsRemote: true,
25861
+ webServer,
25862
+ onInlineScript: ({
25863
+ src,
25864
+ textContent
25865
+ }) => {
25866
+ const inlineScriptWebUrl = new URL(src, `${webServer.origin}/`).href;
25867
+ inlineScriptContents.set(inlineScriptWebUrl, textContent);
25868
+ }
25869
+ });
25870
+ route.fulfill({
25871
+ response,
25872
+ body: injectionResult.content,
25873
+ headers: {
25874
+ ...response.headers(),
25875
+ "content-length": Buffer.byteLength(injectionResult.content)
25876
+ }
25877
+ });
25878
+ };
25879
+ const interceptInlineScript = ({
25880
+ url,
25881
+ route
25882
+ }) => {
25883
+ const inlineScriptContent = inlineScriptContents.get(url);
25884
+ route.fulfill({
25885
+ status: 200,
25886
+ body: inlineScriptContent,
25887
+ headers: {
25888
+ "content-type": "text/javascript",
25889
+ "content-length": Buffer.byteLength(inlineScriptContent)
25890
+ }
25891
+ });
25892
+ };
25893
+ const interceptFileSystemUrl = ({
25894
+ url,
25895
+ route
25896
+ }) => {
25897
+ const relativeUrl = url.slice(webServer.origin.length);
25898
+ const fsPath = relativeUrl.slice("/@fs/".length);
25899
+ const fsUrl = `file:///${fsPath}`;
25900
+ const fileContent = readFileSync$1(new URL(fsUrl), "utf8");
25901
+ route.fulfill({
25902
+ status: 200,
25903
+ body: fileContent,
25904
+ headers: {
25905
+ "content-type": "text/javascript",
25906
+ "content-length": Buffer.byteLength(fileContent)
25907
+ }
25908
+ });
25909
+ };
25910
+ await page.route("**", async route => {
25911
+ const request = route.request();
25912
+ const url = request.url();
25913
+ if (url === fileServerUrl && urlToExtension$1(url) === ".html") {
25914
+ interceptHtmlToExecute({
25915
+ url,
25916
+ request,
25917
+ route
25918
+ });
25919
+ return;
25920
+ }
25921
+ if (inlineScriptContents.has(url)) {
25922
+ interceptInlineScript({
25923
+ url,
25924
+ request,
25925
+ route
25926
+ });
25927
+ return;
25928
+ }
25929
+ const fsServerUrl = new URL("/@fs/", webServer.origin);
25930
+ if (url.startsWith(fsServerUrl)) {
25931
+ interceptFileSystemUrl({
25932
+ url,
25933
+ request,
25934
+ route
25935
+ });
25936
+ return;
25937
+ }
25938
+ route.fallback();
25939
+ });
25940
+ };
25364
25941
  const registerEvent = ({
25365
25942
  object,
25366
25943
  eventType,
@@ -25394,7 +25971,7 @@ const webkit = createRuntimeFromPlaywright({
25394
25971
  // browserVersion will be set by "browser._initializer.version"
25395
25972
  // see also https://github.com/microsoft/playwright/releases
25396
25973
  browserVersion: "unset",
25397
- ignoreErrorHook: error => {
25974
+ shouldIgnoreError: error => {
25398
25975
  // we catch error during execution but safari throw unhandled rejection
25399
25976
  // in a non-deterministic way.
25400
25977
  // I suppose it's due to some race condition to decide if the promise is catched or not
@@ -26321,8 +26898,7 @@ const startBuildServer = async ({
26321
26898
  stopOnExit: false,
26322
26899
  stopOnSIGINT: false,
26323
26900
  stopOnInternalError: false,
26324
- // the worker should be kept alive by the parent otherwise
26325
- keepProcessAlive,
26901
+ keepProcessAlive: process.env.IMPORTED_BY_TEST_PLAN ? false : keepProcessAlive,
26326
26902
  logLevel: serverLogLevel,
26327
26903
  startLog: false,
26328
26904
  https,
@@ -26406,8 +26982,7 @@ const execute = async ({
26406
26982
  handleSIGINT = true,
26407
26983
  logLevel,
26408
26984
  rootDirectoryUrl,
26409
- sourceDirectoryUrl = rootDirectoryUrl,
26410
- devServerOrigin,
26985
+ webServer,
26411
26986
  fileRelativeUrl,
26412
26987
  allocatedMs,
26413
26988
  mirrorConsole = true,
@@ -26433,23 +27008,16 @@ const execute = async ({
26433
27008
  }, abort);
26434
27009
  });
26435
27010
  }
27011
+ if (runtime.type === "browser") {
27012
+ await assertAndNormalizeWebServer(webServer);
27013
+ }
26436
27014
  let resultTransformer = result => result;
26437
27015
  runtimeParams = {
26438
27016
  rootDirectoryUrl,
26439
- sourceDirectoryUrl,
26440
- devServerOrigin,
27017
+ webServer,
26441
27018
  fileRelativeUrl,
26442
27019
  ...runtimeParams
26443
27020
  };
26444
- if (runtime.type === "browser") {
26445
- if (!devServerOrigin) {
26446
- throw new TypeError(`devServerOrigin is required to execute file on a browser`);
26447
- }
26448
- const devServerStarted = await pingServer(devServerOrigin);
26449
- if (!devServerStarted) {
26450
- throw new Error(`no server listening at ${devServerOrigin}. It is required to execute file`);
26451
- }
26452
- }
26453
27021
  let result = await run({
26454
27022
  signal: executeOperation.signal,
26455
27023
  logger,