@jsenv/core 34.0.3 → 34.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/jsenv.js CHANGED
@@ -18947,54 +18947,6 @@ const splitFileExtension$1 = filename => {
18947
18947
  return [filename.slice(0, dotLastIndex), filename.slice(dotLastIndex)];
18948
18948
  };
18949
18949
 
18950
- // https://github.com/istanbuljs/babel-plugin-istanbul/blob/321740f7b25d803f881466ea819d870f7ed6a254/src/index.js
18951
-
18952
- const babelPluginInstrument = (api, {
18953
- useInlineSourceMaps = false
18954
- }) => {
18955
- const {
18956
- programVisitor
18957
- } = requireFromJsenv("istanbul-lib-instrument");
18958
- const {
18959
- types
18960
- } = api;
18961
- return {
18962
- name: "transform-instrument",
18963
- visitor: {
18964
- Program: {
18965
- enter(path) {
18966
- const {
18967
- file
18968
- } = this;
18969
- const {
18970
- opts
18971
- } = file;
18972
- let inputSourceMap;
18973
- if (useInlineSourceMaps) {
18974
- // https://github.com/istanbuljs/babel-plugin-istanbul/commit/a9e15643d249a2985e4387e4308022053b2cd0ad#diff-1fdf421c05c1140f6d71444ea2b27638R65
18975
- inputSourceMap = opts.inputSourceMap || file.inputMap ? file.inputMap.sourcemap : null;
18976
- } else {
18977
- inputSourceMap = opts.inputSourceMap;
18978
- }
18979
- this.__dv__ = programVisitor(types, opts.filenameRelative || opts.filename, {
18980
- coverageVariable: "__coverage__",
18981
- inputSourceMap
18982
- });
18983
- this.__dv__.enter(path);
18984
- },
18985
- exit(path) {
18986
- if (!this.__dv__) {
18987
- return;
18988
- }
18989
- const object = this.__dv__.exit(path);
18990
- // object got two properties: fileCoverage and sourceMappingURL
18991
- this.file.metadata.coverage = object.fileCoverage;
18992
- }
18993
- }
18994
- }
18995
- };
18996
- };
18997
-
18998
18950
  /*
18999
18951
  * Generated helpers
19000
18952
  * - https://github.com/babel/babel/commits/main/packages/babel-helpers/src/helpers.ts
@@ -19804,21 +19756,6 @@ const jsenvPluginBabel = ({
19804
19756
  isJsModule,
19805
19757
  getImportSpecifier
19806
19758
  });
19807
- if (context.dev) {
19808
- const requestHeaders = context.request.headers;
19809
- if (requestHeaders["x-coverage-instanbul"]) {
19810
- const coverageConfig = JSON.parse(requestHeaders["x-coverage-instanbul"]);
19811
- const associations = URL_META.resolveAssociations({
19812
- cover: coverageConfig
19813
- }, context.rootDirectoryUrl);
19814
- if (URL_META.applyAssociations({
19815
- url: urlInfo.url,
19816
- associations
19817
- }).cover) {
19818
- babelPluginStructure["transform-instrument"] = [babelPluginInstrument];
19819
- }
19820
- }
19821
- }
19822
19759
  if (getCustomBabelPlugins) {
19823
19760
  Object.assign(babelPluginStructure, getCustomBabelPlugins(context));
19824
19761
  }
@@ -23921,6 +23858,54 @@ const listRelativeFileUrlToCover = async ({
23921
23858
  }) => relativeUrl);
23922
23859
  };
23923
23860
 
23861
+ // https://github.com/istanbuljs/babel-plugin-istanbul/blob/321740f7b25d803f881466ea819d870f7ed6a254/src/index.js
23862
+
23863
+ const babelPluginInstrument = (api, {
23864
+ useInlineSourceMaps = false
23865
+ }) => {
23866
+ const {
23867
+ programVisitor
23868
+ } = requireFromJsenv("istanbul-lib-instrument");
23869
+ const {
23870
+ types
23871
+ } = api;
23872
+ return {
23873
+ name: "transform-instrument",
23874
+ visitor: {
23875
+ Program: {
23876
+ enter(path) {
23877
+ const {
23878
+ file
23879
+ } = this;
23880
+ const {
23881
+ opts
23882
+ } = file;
23883
+ let inputSourceMap;
23884
+ if (useInlineSourceMaps) {
23885
+ // https://github.com/istanbuljs/babel-plugin-istanbul/commit/a9e15643d249a2985e4387e4308022053b2cd0ad#diff-1fdf421c05c1140f6d71444ea2b27638R65
23886
+ inputSourceMap = opts.inputSourceMap || file.inputMap ? file.inputMap.sourcemap : null;
23887
+ } else {
23888
+ inputSourceMap = opts.inputSourceMap;
23889
+ }
23890
+ this.__dv__ = programVisitor(types, opts.filenameRelative || opts.filename, {
23891
+ coverageVariable: "__coverage__",
23892
+ inputSourceMap
23893
+ });
23894
+ this.__dv__.enter(path);
23895
+ },
23896
+ exit(path) {
23897
+ if (!this.__dv__) {
23898
+ return;
23899
+ }
23900
+ const object = this.__dv__.exit(path);
23901
+ // object got two properties: fileCoverage and sourceMappingURL
23902
+ this.file.metadata.coverage = object.fileCoverage;
23903
+ }
23904
+ }
23905
+ }
23906
+ };
23907
+ };
23908
+
23924
23909
  const relativeUrlToEmptyCoverage = async (relativeUrl, {
23925
23910
  signal,
23926
23911
  rootDirectoryUrl
@@ -24800,7 +24785,6 @@ const executeSteps = async (executionSteps, {
24800
24785
  process.exitCode !== 1;
24801
24786
  const startMs = Date.now();
24802
24787
  let rawOutput = "";
24803
- logger.info("");
24804
24788
  let executionLog = createLog({
24805
24789
  newLine: ""
24806
24790
  });
@@ -25109,7 +25093,10 @@ const executeTestPlan = async ({
25109
25093
  "file:///**/.*": false,
25110
25094
  "file:///**/.*/": false,
25111
25095
  "file:///**/node_modules/": false,
25112
- "./**/src/": true,
25096
+ "./**/src/**/*.js": true,
25097
+ "./**/src/**/*.ts": true,
25098
+ "./**/src/**/*.jsx": true,
25099
+ "./**/src/**/*.tsx": true,
25113
25100
  "./**/tests/": false,
25114
25101
  "./**/*.test.html": false,
25115
25102
  "./**/*.test.js": false,
@@ -25118,8 +25105,15 @@ const executeTestPlan = async ({
25118
25105
  coverageIncludeMissing = true,
25119
25106
  coverageAndExecutionAllowed = false,
25120
25107
  coverageMethodForNodeJs = process.env.NODE_V8_COVERAGE ? "NODE_V8_COVERAGE" : "Profiler",
25121
- coverageMethodForBrowsers = "playwright_api",
25122
- // "istanbul" also accepted
25108
+ // - When chromium only -> coverage generated by v8
25109
+ // - When chromium + node -> coverage generated by v8 are merged
25110
+ // - When firefox only -> coverage generated by babel+istanbul
25111
+ // - When chromium + firefox
25112
+ // -> by default only coverage from chromium is used
25113
+ // and a warning is logged according to coverageV8ConflictWarning
25114
+ // -> to collect coverage from both browsers, pass coverageMethodForBrowsers: "istanbul"
25115
+ coverageMethodForBrowsers,
25116
+ // undefined | "playwright" | "istanbul"
25123
25117
  coverageV8ConflictWarning = true,
25124
25118
  coverageTempDirectoryUrl,
25125
25119
  // skip empty means empty files won't appear in the coverage reports (json and html)
@@ -25134,6 +25128,7 @@ const executeTestPlan = async ({
25134
25128
  ...rest
25135
25129
  }) => {
25136
25130
  let someNeedsServer = false;
25131
+ let someHasCoverageV8 = false;
25137
25132
  let someNodeRuntime = false;
25138
25133
  const runtimes = {};
25139
25134
  // param validation
@@ -25160,6 +25155,9 @@ const executeTestPlan = async ({
25160
25155
  if (runtime) {
25161
25156
  runtimes[runtime.name] = runtime.version;
25162
25157
  if (runtime.type === "browser") {
25158
+ if (runtime.capabilities && runtime.capabilities.coverageV8) {
25159
+ someHasCoverageV8 = true;
25160
+ }
25163
25161
  someNeedsServer = true;
25164
25162
  }
25165
25163
  if (runtime.type === "node") {
@@ -25172,6 +25170,9 @@ const executeTestPlan = async ({
25172
25170
  await assertAndNormalizeWebServer(webServer);
25173
25171
  }
25174
25172
  if (coverageEnabled) {
25173
+ if (coverageMethodForBrowsers === undefined) {
25174
+ coverageMethodForBrowsers = someHasCoverageV8 ? "playwright" : "istanbul";
25175
+ }
25175
25176
  if (typeof coverageConfig !== "object") {
25176
25177
  throw new TypeError(`coverageConfig must be an object, got ${coverageConfig}`);
25177
25178
  }
@@ -25332,6 +25333,172 @@ const executeTestPlan = async ({
25332
25333
  };
25333
25334
  };
25334
25335
 
25336
+ const initJsSupervisorMiddleware = async (page, {
25337
+ webServer,
25338
+ fileUrl,
25339
+ fileServerUrl
25340
+ }) => {
25341
+ const inlineScriptContents = new Map();
25342
+ const interceptHtmlToExecute = async ({
25343
+ route
25344
+ }) => {
25345
+ const response = await route.fetch();
25346
+ const originalBody = await response.text();
25347
+ const injectionResult = await injectSupervisorIntoHTML({
25348
+ content: originalBody,
25349
+ url: fileUrl
25350
+ }, {
25351
+ supervisorScriptSrc: `/@fs/${supervisorFileUrl$1.slice("file:///".length)}`,
25352
+ supervisorOptions: {},
25353
+ inlineAsRemote: true,
25354
+ webServer,
25355
+ onInlineScript: ({
25356
+ src,
25357
+ textContent
25358
+ }) => {
25359
+ const inlineScriptWebUrl = new URL(src, `${webServer.origin}/`).href;
25360
+ inlineScriptContents.set(inlineScriptWebUrl, textContent);
25361
+ }
25362
+ });
25363
+ route.fulfill({
25364
+ response,
25365
+ body: injectionResult.content,
25366
+ headers: {
25367
+ ...response.headers(),
25368
+ "content-length": Buffer.byteLength(injectionResult.content)
25369
+ }
25370
+ });
25371
+ };
25372
+ const interceptInlineScript = ({
25373
+ url,
25374
+ route
25375
+ }) => {
25376
+ const inlineScriptContent = inlineScriptContents.get(url);
25377
+ route.fulfill({
25378
+ status: 200,
25379
+ body: inlineScriptContent,
25380
+ headers: {
25381
+ "content-type": "text/javascript",
25382
+ "content-length": Buffer.byteLength(inlineScriptContent)
25383
+ }
25384
+ });
25385
+ };
25386
+ const interceptFileSystemUrl = ({
25387
+ url,
25388
+ route
25389
+ }) => {
25390
+ const relativeUrl = url.slice(webServer.origin.length);
25391
+ const fsPath = relativeUrl.slice("/@fs/".length);
25392
+ const fsUrl = `file:///${fsPath}`;
25393
+ const fileContent = readFileSync$1(new URL(fsUrl), "utf8");
25394
+ route.fulfill({
25395
+ status: 200,
25396
+ body: fileContent,
25397
+ headers: {
25398
+ "content-type": "text/javascript",
25399
+ "content-length": Buffer.byteLength(fileContent)
25400
+ }
25401
+ });
25402
+ };
25403
+ await page.route("**", async route => {
25404
+ const request = route.request();
25405
+ const url = request.url();
25406
+ if (url === fileServerUrl && urlToExtension$1(url) === ".html") {
25407
+ interceptHtmlToExecute({
25408
+ url,
25409
+ request,
25410
+ route
25411
+ });
25412
+ return;
25413
+ }
25414
+ if (inlineScriptContents.has(url)) {
25415
+ interceptInlineScript({
25416
+ url,
25417
+ request,
25418
+ route
25419
+ });
25420
+ return;
25421
+ }
25422
+ const fsServerUrl = new URL("/@fs/", webServer.origin);
25423
+ if (url.startsWith(fsServerUrl)) {
25424
+ interceptFileSystemUrl({
25425
+ url,
25426
+ request,
25427
+ route
25428
+ });
25429
+ return;
25430
+ }
25431
+ route.fallback();
25432
+ });
25433
+ };
25434
+
25435
+ const initIstanbulMiddleware = async (page, {
25436
+ webServer,
25437
+ rootDirectoryUrl,
25438
+ coverageConfig
25439
+ }) => {
25440
+ const associations = URL_META.resolveAssociations({
25441
+ cover: coverageConfig
25442
+ }, rootDirectoryUrl);
25443
+ await page.route("**", async route => {
25444
+ const request = route.request();
25445
+ const url = request.url(); // transform into a local url
25446
+ const fileUrl = moveUrl({
25447
+ url,
25448
+ from: `${webServer.origin}/`,
25449
+ to: rootDirectoryUrl
25450
+ });
25451
+ const needsInstrumentation = URL_META.applyAssociations({
25452
+ url: fileUrl,
25453
+ associations
25454
+ }).cover;
25455
+ if (!needsInstrumentation) {
25456
+ route.fallback();
25457
+ return;
25458
+ }
25459
+ const response = await route.fetch();
25460
+ const originalBody = await response.text();
25461
+ try {
25462
+ const result = await applyBabelPlugins({
25463
+ babelPlugins: [babelPluginInstrument],
25464
+ urlInfo: {
25465
+ originalUrl: fileUrl,
25466
+ // jsenv server could send info to know it's a js module or js classic
25467
+ // but in the end it's not super important
25468
+ // - it's ok to parse js classic as js module considering it's only for istanbul instrumentation
25469
+ type: "js_module",
25470
+ content: originalBody
25471
+ }
25472
+ });
25473
+ let code = result.code;
25474
+ code = SOURCEMAP.writeComment({
25475
+ contentType: "text/javascript",
25476
+ content: code,
25477
+ specifier: generateSourcemapDataUrl(result.map)
25478
+ });
25479
+ route.fulfill({
25480
+ response,
25481
+ body: code,
25482
+ headers: {
25483
+ ...response.headers(),
25484
+ "content-length": Buffer.byteLength(code)
25485
+ }
25486
+ });
25487
+ } catch (e) {
25488
+ if (e.code === "PARSE_ERROR") {
25489
+ route.fulfill({
25490
+ response
25491
+ });
25492
+ } else {
25493
+ console.error(e);
25494
+ route.fulfill({
25495
+ response
25496
+ });
25497
+ }
25498
+ }
25499
+ });
25500
+ };
25501
+
25335
25502
  const createRuntimeFromPlaywright = ({
25336
25503
  browserName,
25337
25504
  browserVersion,
@@ -25343,7 +25510,10 @@ const createRuntimeFromPlaywright = ({
25343
25510
  const runtime = {
25344
25511
  type: "browser",
25345
25512
  name: browserName,
25346
- version: browserVersion
25513
+ version: browserVersion,
25514
+ capabilities: {
25515
+ coverageV8: coveragePlaywrightAPIAvailable
25516
+ }
25347
25517
  };
25348
25518
  let browserAndContextPromise;
25349
25519
  runtime.run = async ({
@@ -25434,16 +25604,17 @@ ${webServer.rootDirectoryUrl}`);
25434
25604
  }
25435
25605
  await disconnected;
25436
25606
  };
25437
- const coverageInHeaders = coverageEnabled && (!coveragePlaywrightAPIAvailable || coverageMethodForBrowsers !== "playwright_api");
25438
- const page = await browserContext.newPage({
25439
- extraHTTPHeaders: {
25440
- ...(coverageInHeaders ? {
25441
- "x-coverage-istanbul": JSON.stringify(coverageConfig)
25442
- } : {})
25443
- }
25444
- });
25607
+ const page = await browserContext.newPage();
25608
+ const istanbulInstrumentationEnabled = coverageEnabled && (!runtime.capabilities.coverageV8 || coverageMethodForBrowsers === "istanbul");
25609
+ if (istanbulInstrumentationEnabled) {
25610
+ await initIstanbulMiddleware(page, {
25611
+ webServer,
25612
+ rootDirectoryUrl,
25613
+ coverageConfig
25614
+ });
25615
+ }
25445
25616
  if (!webServer.isJsenvDevServer) {
25446
- await initJsExecutionMiddleware(page, {
25617
+ await initJsSupervisorMiddleware(page, {
25447
25618
  webServer,
25448
25619
  fileUrl,
25449
25620
  fileServerUrl
@@ -25466,7 +25637,7 @@ ${webServer.rootDirectoryUrl}`);
25466
25637
  };
25467
25638
  const callbacks = [];
25468
25639
  if (coverageEnabled) {
25469
- if (coveragePlaywrightAPIAvailable && coverageMethodForBrowsers === "playwright_api") {
25640
+ if (runtime.capabilities.coverageV8 && coverageMethodForBrowsers === "playwright") {
25470
25641
  await page.coverage.startJSCoverage({
25471
25642
  // reportAnonymousScripts: true,
25472
25643
  });
@@ -25848,106 +26019,6 @@ const extractTextFromConsoleMessage = consoleMessage => {
25848
26019
  // return text
25849
26020
  };
25850
26021
 
25851
- const initJsExecutionMiddleware = async (page, {
25852
- webServer,
25853
- fileUrl,
25854
- fileServerUrl
25855
- }) => {
25856
- const inlineScriptContents = new Map();
25857
- const interceptHtmlToExecute = async ({
25858
- route
25859
- }) => {
25860
- // Fetch original response.
25861
- const response = await route.fetch();
25862
- // Add a prefix to the title.
25863
- const originalBody = await response.text();
25864
- const injectionResult = await injectSupervisorIntoHTML({
25865
- content: originalBody,
25866
- url: fileUrl
25867
- }, {
25868
- supervisorScriptSrc: `/@fs/${supervisorFileUrl$1.slice("file:///".length)}`,
25869
- supervisorOptions: {},
25870
- inlineAsRemote: true,
25871
- webServer,
25872
- onInlineScript: ({
25873
- src,
25874
- textContent
25875
- }) => {
25876
- const inlineScriptWebUrl = new URL(src, `${webServer.origin}/`).href;
25877
- inlineScriptContents.set(inlineScriptWebUrl, textContent);
25878
- }
25879
- });
25880
- route.fulfill({
25881
- response,
25882
- body: injectionResult.content,
25883
- headers: {
25884
- ...response.headers(),
25885
- "content-length": Buffer.byteLength(injectionResult.content)
25886
- }
25887
- });
25888
- };
25889
- const interceptInlineScript = ({
25890
- url,
25891
- route
25892
- }) => {
25893
- const inlineScriptContent = inlineScriptContents.get(url);
25894
- route.fulfill({
25895
- status: 200,
25896
- body: inlineScriptContent,
25897
- headers: {
25898
- "content-type": "text/javascript",
25899
- "content-length": Buffer.byteLength(inlineScriptContent)
25900
- }
25901
- });
25902
- };
25903
- const interceptFileSystemUrl = ({
25904
- url,
25905
- route
25906
- }) => {
25907
- const relativeUrl = url.slice(webServer.origin.length);
25908
- const fsPath = relativeUrl.slice("/@fs/".length);
25909
- const fsUrl = `file:///${fsPath}`;
25910
- const fileContent = readFileSync$1(new URL(fsUrl), "utf8");
25911
- route.fulfill({
25912
- status: 200,
25913
- body: fileContent,
25914
- headers: {
25915
- "content-type": "text/javascript",
25916
- "content-length": Buffer.byteLength(fileContent)
25917
- }
25918
- });
25919
- };
25920
- await page.route("**", async route => {
25921
- const request = route.request();
25922
- const url = request.url();
25923
- if (url === fileServerUrl && urlToExtension$1(url) === ".html") {
25924
- interceptHtmlToExecute({
25925
- url,
25926
- request,
25927
- route
25928
- });
25929
- return;
25930
- }
25931
- if (inlineScriptContents.has(url)) {
25932
- interceptInlineScript({
25933
- url,
25934
- request,
25935
- route
25936
- });
25937
- return;
25938
- }
25939
- const fsServerUrl = new URL("/@fs/", webServer.origin);
25940
- if (url.startsWith(fsServerUrl)) {
25941
- interceptFileSystemUrl({
25942
- url,
25943
- request,
25944
- route
25945
- });
25946
- return;
25947
- }
25948
- route.fallback();
25949
- });
25950
- };
25951
26022
  const registerEvent = ({
25952
26023
  object,
25953
26024
  eventType,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "34.0.3",
3
+ "version": "34.1.0",
4
4
  "description": "Tool to develop, test and build js projects",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -1,5 +1,4 @@
1
- import { readFileSync, writeFileSync } from "node:fs"
2
-
1
+ import { writeFileSync } from "node:fs"
3
2
  import { createDetailedMessage } from "@jsenv/log"
4
3
  import {
5
4
  Abort,
@@ -7,15 +6,13 @@ import {
7
6
  raceProcessTeardownEvents,
8
7
  raceCallbacks,
9
8
  } from "@jsenv/abort"
10
- import { moveUrl, urlIsInsideOf, urlToExtension } from "@jsenv/urls"
9
+ import { moveUrl, urlIsInsideOf } from "@jsenv/urls"
11
10
  import { memoize } from "@jsenv/utils/src/memoize/memoize.js"
12
11
 
12
+ import { initJsSupervisorMiddleware } from "./middleware_js_supervisor.js"
13
+ import { initIstanbulMiddleware } from "./middleware_istanbul.js"
13
14
  import { filterV8Coverage } from "@jsenv/core/src/test/coverage/v8_coverage.js"
14
15
  import { composeTwoFileByFileIstanbulCoverages } from "@jsenv/core/src/test/coverage/istanbul_coverage_composition.js"
15
- import {
16
- injectSupervisorIntoHTML,
17
- supervisorFileUrl,
18
- } from "../../../plugins/supervisor/html_supervisor_injection.js"
19
16
 
20
17
  export const createRuntimeFromPlaywright = ({
21
18
  browserName,
@@ -29,6 +26,9 @@ export const createRuntimeFromPlaywright = ({
29
26
  type: "browser",
30
27
  name: browserName,
31
28
  version: browserVersion,
29
+ capabilities: {
30
+ coverageV8: coveragePlaywrightAPIAvailable,
31
+ },
32
32
  }
33
33
  let browserAndContextPromise
34
34
  runtime.run = async ({
@@ -116,21 +116,22 @@ ${webServer.rootDirectoryUrl}`)
116
116
  }
117
117
  await disconnected
118
118
  }
119
- const coverageInHeaders =
119
+
120
+ const page = await browserContext.newPage()
121
+
122
+ const istanbulInstrumentationEnabled =
120
123
  coverageEnabled &&
121
- (!coveragePlaywrightAPIAvailable ||
122
- coverageMethodForBrowsers !== "playwright_api")
123
- const page = await browserContext.newPage({
124
- extraHTTPHeaders: {
125
- ...(coverageInHeaders
126
- ? {
127
- "x-coverage-istanbul": JSON.stringify(coverageConfig),
128
- }
129
- : {}),
130
- },
131
- })
124
+ (!runtime.capabilities.coverageV8 ||
125
+ coverageMethodForBrowsers === "istanbul")
126
+ if (istanbulInstrumentationEnabled) {
127
+ await initIstanbulMiddleware(page, {
128
+ webServer,
129
+ rootDirectoryUrl,
130
+ coverageConfig,
131
+ })
132
+ }
132
133
  if (!webServer.isJsenvDevServer) {
133
- await initJsExecutionMiddleware(page, {
134
+ await initJsSupervisorMiddleware(page, {
134
135
  webServer,
135
136
  fileUrl,
136
137
  fileServerUrl,
@@ -155,8 +156,8 @@ ${webServer.rootDirectoryUrl}`)
155
156
  const callbacks = []
156
157
  if (coverageEnabled) {
157
158
  if (
158
- coveragePlaywrightAPIAvailable &&
159
- coverageMethodForBrowsers === "playwright_api"
159
+ runtime.capabilities.coverageV8 &&
160
+ coverageMethodForBrowsers === "playwright"
160
161
  ) {
161
162
  await page.coverage.startJSCoverage({
162
163
  // reportAnonymousScripts: true,
@@ -570,100 +571,6 @@ const extractTextFromConsoleMessage = (consoleMessage) => {
570
571
  // return text
571
572
  }
572
573
 
573
- const initJsExecutionMiddleware = async (
574
- page,
575
- { webServer, fileUrl, fileServerUrl },
576
- ) => {
577
- const inlineScriptContents = new Map()
578
-
579
- const interceptHtmlToExecute = async ({ route }) => {
580
- // Fetch original response.
581
- const response = await route.fetch()
582
- // Add a prefix to the title.
583
- const originalBody = await response.text()
584
- const injectionResult = await injectSupervisorIntoHTML(
585
- {
586
- content: originalBody,
587
- url: fileUrl,
588
- },
589
- {
590
- supervisorScriptSrc: `/@fs/${supervisorFileUrl.slice(
591
- "file:///".length,
592
- )}`,
593
- supervisorOptions: {},
594
- inlineAsRemote: true,
595
- webServer,
596
- onInlineScript: ({ src, textContent }) => {
597
- const inlineScriptWebUrl = new URL(src, `${webServer.origin}/`).href
598
- inlineScriptContents.set(inlineScriptWebUrl, textContent)
599
- },
600
- },
601
- )
602
- route.fulfill({
603
- response,
604
- body: injectionResult.content,
605
- headers: {
606
- ...response.headers(),
607
- "content-length": Buffer.byteLength(injectionResult.content),
608
- },
609
- })
610
- }
611
-
612
- const interceptInlineScript = ({ url, route }) => {
613
- const inlineScriptContent = inlineScriptContents.get(url)
614
- route.fulfill({
615
- status: 200,
616
- body: inlineScriptContent,
617
- headers: {
618
- "content-type": "text/javascript",
619
- "content-length": Buffer.byteLength(inlineScriptContent),
620
- },
621
- })
622
- }
623
-
624
- const interceptFileSystemUrl = ({ url, route }) => {
625
- const relativeUrl = url.slice(webServer.origin.length)
626
- const fsPath = relativeUrl.slice("/@fs/".length)
627
- const fsUrl = `file:///${fsPath}`
628
- const fileContent = readFileSync(new URL(fsUrl), "utf8")
629
- route.fulfill({
630
- status: 200,
631
- body: fileContent,
632
- headers: {
633
- "content-type": "text/javascript",
634
- "content-length": Buffer.byteLength(fileContent),
635
- },
636
- })
637
- }
638
-
639
- await page.route("**", async (route) => {
640
- const request = route.request()
641
- const url = request.url()
642
- if (url === fileServerUrl && urlToExtension(url) === ".html") {
643
- interceptHtmlToExecute({
644
- url,
645
- request,
646
- route,
647
- })
648
- return
649
- }
650
- if (inlineScriptContents.has(url)) {
651
- interceptInlineScript({
652
- url,
653
- request,
654
- route,
655
- })
656
- return
657
- }
658
- const fsServerUrl = new URL("/@fs/", webServer.origin)
659
- if (url.startsWith(fsServerUrl)) {
660
- interceptFileSystemUrl({ url, request, route })
661
- return
662
- }
663
- route.fallback()
664
- })
665
- }
666
-
667
574
  const registerEvent = ({ object, eventType, callback }) => {
668
575
  object.on(eventType, callback)
669
576
  return () => {
@@ -0,0 +1,69 @@
1
+ import { URL_META } from "@jsenv/url-meta"
2
+ import { moveUrl } from "@jsenv/urls"
3
+ import { applyBabelPlugins } from "@jsenv/ast"
4
+ import { SOURCEMAP, generateSourcemapDataUrl } from "@jsenv/sourcemap"
5
+
6
+ import { babelPluginInstrument } from "../../../test/coverage/babel_plugin_instrument.js"
7
+
8
+ export const initIstanbulMiddleware = async (
9
+ page,
10
+ { webServer, rootDirectoryUrl, coverageConfig },
11
+ ) => {
12
+ const associations = URL_META.resolveAssociations(
13
+ { cover: coverageConfig },
14
+ rootDirectoryUrl,
15
+ )
16
+ await page.route("**", async (route) => {
17
+ const request = route.request()
18
+ const url = request.url() // transform into a local url
19
+ const fileUrl = moveUrl({
20
+ url,
21
+ from: `${webServer.origin}/`,
22
+ to: rootDirectoryUrl,
23
+ })
24
+ const needsInstrumentation = URL_META.applyAssociations({
25
+ url: fileUrl,
26
+ associations,
27
+ }).cover
28
+ if (!needsInstrumentation) {
29
+ route.fallback()
30
+ return
31
+ }
32
+ const response = await route.fetch()
33
+ const originalBody = await response.text()
34
+ try {
35
+ const result = await applyBabelPlugins({
36
+ babelPlugins: [babelPluginInstrument],
37
+ urlInfo: {
38
+ originalUrl: fileUrl,
39
+ // jsenv server could send info to know it's a js module or js classic
40
+ // but in the end it's not super important
41
+ // - it's ok to parse js classic as js module considering it's only for istanbul instrumentation
42
+ type: "js_module",
43
+ content: originalBody,
44
+ },
45
+ })
46
+ let code = result.code
47
+ code = SOURCEMAP.writeComment({
48
+ contentType: "text/javascript",
49
+ content: code,
50
+ specifier: generateSourcemapDataUrl(result.map),
51
+ })
52
+ route.fulfill({
53
+ response,
54
+ body: code,
55
+ headers: {
56
+ ...response.headers(),
57
+ "content-length": Buffer.byteLength(code),
58
+ },
59
+ })
60
+ } catch (e) {
61
+ if (e.code === "PARSE_ERROR") {
62
+ route.fulfill({ response })
63
+ } else {
64
+ console.error(e)
65
+ route.fulfill({ response })
66
+ }
67
+ }
68
+ })
69
+ }
@@ -0,0 +1,100 @@
1
+ import { readFileSync } from "node:fs"
2
+
3
+ import { urlToExtension } from "@jsenv/urls"
4
+
5
+ import {
6
+ injectSupervisorIntoHTML,
7
+ supervisorFileUrl,
8
+ } from "../../../plugins/supervisor/html_supervisor_injection.js"
9
+
10
+ export const initJsSupervisorMiddleware = async (
11
+ page,
12
+ { webServer, fileUrl, fileServerUrl },
13
+ ) => {
14
+ const inlineScriptContents = new Map()
15
+
16
+ const interceptHtmlToExecute = async ({ route }) => {
17
+ const response = await route.fetch()
18
+ const originalBody = await response.text()
19
+ const injectionResult = await injectSupervisorIntoHTML(
20
+ {
21
+ content: originalBody,
22
+ url: fileUrl,
23
+ },
24
+ {
25
+ supervisorScriptSrc: `/@fs/${supervisorFileUrl.slice(
26
+ "file:///".length,
27
+ )}`,
28
+ supervisorOptions: {},
29
+ inlineAsRemote: true,
30
+ webServer,
31
+ onInlineScript: ({ src, textContent }) => {
32
+ const inlineScriptWebUrl = new URL(src, `${webServer.origin}/`).href
33
+ inlineScriptContents.set(inlineScriptWebUrl, textContent)
34
+ },
35
+ },
36
+ )
37
+ route.fulfill({
38
+ response,
39
+ body: injectionResult.content,
40
+ headers: {
41
+ ...response.headers(),
42
+ "content-length": Buffer.byteLength(injectionResult.content),
43
+ },
44
+ })
45
+ }
46
+
47
+ const interceptInlineScript = ({ url, route }) => {
48
+ const inlineScriptContent = inlineScriptContents.get(url)
49
+ route.fulfill({
50
+ status: 200,
51
+ body: inlineScriptContent,
52
+ headers: {
53
+ "content-type": "text/javascript",
54
+ "content-length": Buffer.byteLength(inlineScriptContent),
55
+ },
56
+ })
57
+ }
58
+
59
+ const interceptFileSystemUrl = ({ url, route }) => {
60
+ const relativeUrl = url.slice(webServer.origin.length)
61
+ const fsPath = relativeUrl.slice("/@fs/".length)
62
+ const fsUrl = `file:///${fsPath}`
63
+ const fileContent = readFileSync(new URL(fsUrl), "utf8")
64
+ route.fulfill({
65
+ status: 200,
66
+ body: fileContent,
67
+ headers: {
68
+ "content-type": "text/javascript",
69
+ "content-length": Buffer.byteLength(fileContent),
70
+ },
71
+ })
72
+ }
73
+
74
+ await page.route("**", async (route) => {
75
+ const request = route.request()
76
+ const url = request.url()
77
+ if (url === fileServerUrl && urlToExtension(url) === ".html") {
78
+ interceptHtmlToExecute({
79
+ url,
80
+ request,
81
+ route,
82
+ })
83
+ return
84
+ }
85
+ if (inlineScriptContents.has(url)) {
86
+ interceptInlineScript({
87
+ url,
88
+ request,
89
+ route,
90
+ })
91
+ return
92
+ }
93
+ const fsServerUrl = new URL("/@fs/", webServer.origin)
94
+ if (url.startsWith(fsServerUrl)) {
95
+ interceptFileSystemUrl({ url, request, route })
96
+ return
97
+ }
98
+ route.fallback()
99
+ })
100
+ }
@@ -1,7 +1,5 @@
1
1
  import { applyBabelPlugins } from "@jsenv/ast"
2
- import { URL_META } from "@jsenv/url-meta"
3
2
 
4
- import { babelPluginInstrument } from "@jsenv/core/src/test/coverage/babel_plugin_instrument.js"
5
3
  import { RUNTIME_COMPAT } from "@jsenv/core/src/kitchen/compat/runtime_compat.js"
6
4
  import { getBaseBabelPluginStructure } from "./helpers/babel_plugin_structure.js"
7
5
  import { babelPluginBabelHelpersAsJsenvImports } from "./helpers/babel_plugin_babel_helpers_as_jsenv_imports.js"
@@ -33,23 +31,6 @@ export const jsenvPluginBabel = ({
33
31
  isJsModule,
34
32
  getImportSpecifier,
35
33
  })
36
- if (context.dev) {
37
- const requestHeaders = context.request.headers
38
- if (requestHeaders["x-coverage-instanbul"]) {
39
- const coverageConfig = JSON.parse(
40
- requestHeaders["x-coverage-instanbul"],
41
- )
42
- const associations = URL_META.resolveAssociations(
43
- { cover: coverageConfig },
44
- context.rootDirectoryUrl,
45
- )
46
- if (
47
- URL_META.applyAssociations({ url: urlInfo.url, associations }).cover
48
- ) {
49
- babelPluginStructure["transform-instrument"] = [babelPluginInstrument]
50
- }
51
- }
52
- }
53
34
  if (getCustomBabelPlugins) {
54
35
  Object.assign(babelPluginStructure, getCustomBabelPlugins(context))
55
36
  }
@@ -146,8 +146,6 @@ export const executeSteps = async (
146
146
 
147
147
  const startMs = Date.now()
148
148
  let rawOutput = ""
149
-
150
- logger.info("")
151
149
  let executionLog = createLog({ newLine: "" })
152
150
  const counters = {
153
151
  total: executionSteps.length,
@@ -67,7 +67,10 @@ export const executeTestPlan = async ({
67
67
  "file:///**/.*": false,
68
68
  "file:///**/.*/": false,
69
69
  "file:///**/node_modules/": false,
70
- "./**/src/": true,
70
+ "./**/src/**/*.js": true,
71
+ "./**/src/**/*.ts": true,
72
+ "./**/src/**/*.jsx": true,
73
+ "./**/src/**/*.tsx": true,
71
74
  "./**/tests/": false,
72
75
  "./**/*.test.html": false,
73
76
  "./**/*.test.js": false,
@@ -78,7 +81,14 @@ export const executeTestPlan = async ({
78
81
  coverageMethodForNodeJs = process.env.NODE_V8_COVERAGE
79
82
  ? "NODE_V8_COVERAGE"
80
83
  : "Profiler",
81
- coverageMethodForBrowsers = "playwright_api", // "istanbul" also accepted
84
+ // - When chromium only -> coverage generated by v8
85
+ // - When chromium + node -> coverage generated by v8 are merged
86
+ // - When firefox only -> coverage generated by babel+istanbul
87
+ // - When chromium + firefox
88
+ // -> by default only coverage from chromium is used
89
+ // and a warning is logged according to coverageV8ConflictWarning
90
+ // -> to collect coverage from both browsers, pass coverageMethodForBrowsers: "istanbul"
91
+ coverageMethodForBrowsers, // undefined | "playwright" | "istanbul"
82
92
  coverageV8ConflictWarning = true,
83
93
  coverageTempDirectoryUrl,
84
94
  // skip empty means empty files won't appear in the coverage reports (json and html)
@@ -93,6 +103,7 @@ export const executeTestPlan = async ({
93
103
  ...rest
94
104
  }) => {
95
105
  let someNeedsServer = false
106
+ let someHasCoverageV8 = false
96
107
  let someNodeRuntime = false
97
108
  const runtimes = {}
98
109
  // param validation
@@ -123,6 +134,9 @@ export const executeTestPlan = async ({
123
134
  if (runtime) {
124
135
  runtimes[runtime.name] = runtime.version
125
136
  if (runtime.type === "browser") {
137
+ if (runtime.capabilities && runtime.capabilities.coverageV8) {
138
+ someHasCoverageV8 = true
139
+ }
126
140
  someNeedsServer = true
127
141
  }
128
142
  if (runtime.type === "node") {
@@ -137,6 +151,11 @@ export const executeTestPlan = async ({
137
151
  }
138
152
 
139
153
  if (coverageEnabled) {
154
+ if (coverageMethodForBrowsers === undefined) {
155
+ coverageMethodForBrowsers = someHasCoverageV8
156
+ ? "playwright"
157
+ : "istanbul"
158
+ }
140
159
  if (typeof coverageConfig !== "object") {
141
160
  throw new TypeError(
142
161
  `coverageConfig must be an object, got ${coverageConfig}`,