@jsenv/core 34.0.2 → 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/README.md CHANGED
@@ -7,7 +7,7 @@ It has naturally evolved to cover the core needs of a JavaScript project: develo
7
7
  - :sparkles: Same tooling for dev, tests and build.
8
8
  - :exploding_head: Can execute tests on Chrome, Firefox, Safari and Node.js.
9
9
 
10
- [Documentation](https://github.com/jsenv/jsenv-core/wiki/A\)-Getting-started)
10
+ [Documentation](<https://github.com/jsenv/jsenv-core/wiki/A)-Getting-started>)
11
11
 
12
12
  # Installation
13
13
 
package/dist/jsenv.js CHANGED
@@ -7910,7 +7910,6 @@ const createUrlInfoTransformer = ({
7910
7910
  await context.cook(sourcemapUrlInfo, {
7911
7911
  reference: sourcemapReference
7912
7912
  });
7913
- sourcemapUrlInfo.isInline = sourcemaps === "inline";
7914
7913
  const sourcemapRaw = JSON.parse(sourcemapUrlInfo.content);
7915
7914
  const sourcemap = normalizeSourcemap(urlInfo, sourcemapRaw);
7916
7915
  urlInfo.sourcemap = sourcemap;
@@ -9003,6 +9002,7 @@ ${ANSI.color(normalizedReturnValue, ANSI.YELLOW)}
9003
9002
  url: urlInfo.url
9004
9003
  },
9005
9004
  type: "sourcemap_comment",
9005
+ expectedType: "sourcemap",
9006
9006
  subtype: urlInfo.contentType === "text/javascript" ? "js" : "css",
9007
9007
  parentUrl: urlInfo.url,
9008
9008
  specifier
@@ -9026,11 +9026,15 @@ ${ANSI.color(normalizedReturnValue, ANSI.YELLOW)}
9026
9026
  const [sourcemapReference, sourcemapUrlInfo] = resolveReference(createReference({
9027
9027
  trace: traceFromUrlSite(sourcemapUrlSite),
9028
9028
  type,
9029
+ expectedType: "sourcemap",
9029
9030
  parentUrl: urlInfo.url,
9030
9031
  specifier,
9031
9032
  specifierLine,
9032
9033
  specifierColumn
9033
9034
  }));
9035
+ if (sourcemapReference.isInline) {
9036
+ sourcemapUrlInfo.isInline = true;
9037
+ }
9034
9038
  sourcemapUrlInfo.type = "sourcemap";
9035
9039
  return [sourcemapReference, sourcemapUrlInfo];
9036
9040
  }
@@ -9541,6 +9545,12 @@ const adjustUrlSite = (urlInfo, {
9541
9545
  }, urlInfo);
9542
9546
  };
9543
9547
  const inferUrlInfoType = urlInfo => {
9548
+ const {
9549
+ type
9550
+ } = urlInfo;
9551
+ if (type === "sourcemap") {
9552
+ return "sourcemap";
9553
+ }
9544
9554
  const {
9545
9555
  contentType
9546
9556
  } = urlInfo;
@@ -18937,54 +18947,6 @@ const splitFileExtension$1 = filename => {
18937
18947
  return [filename.slice(0, dotLastIndex), filename.slice(dotLastIndex)];
18938
18948
  };
18939
18949
 
18940
- // https://github.com/istanbuljs/babel-plugin-istanbul/blob/321740f7b25d803f881466ea819d870f7ed6a254/src/index.js
18941
-
18942
- const babelPluginInstrument = (api, {
18943
- useInlineSourceMaps = false
18944
- }) => {
18945
- const {
18946
- programVisitor
18947
- } = requireFromJsenv("istanbul-lib-instrument");
18948
- const {
18949
- types
18950
- } = api;
18951
- return {
18952
- name: "transform-instrument",
18953
- visitor: {
18954
- Program: {
18955
- enter(path) {
18956
- const {
18957
- file
18958
- } = this;
18959
- const {
18960
- opts
18961
- } = file;
18962
- let inputSourceMap;
18963
- if (useInlineSourceMaps) {
18964
- // https://github.com/istanbuljs/babel-plugin-istanbul/commit/a9e15643d249a2985e4387e4308022053b2cd0ad#diff-1fdf421c05c1140f6d71444ea2b27638R65
18965
- inputSourceMap = opts.inputSourceMap || file.inputMap ? file.inputMap.sourcemap : null;
18966
- } else {
18967
- inputSourceMap = opts.inputSourceMap;
18968
- }
18969
- this.__dv__ = programVisitor(types, opts.filenameRelative || opts.filename, {
18970
- coverageVariable: "__coverage__",
18971
- inputSourceMap
18972
- });
18973
- this.__dv__.enter(path);
18974
- },
18975
- exit(path) {
18976
- if (!this.__dv__) {
18977
- return;
18978
- }
18979
- const object = this.__dv__.exit(path);
18980
- // object got two properties: fileCoverage and sourceMappingURL
18981
- this.file.metadata.coverage = object.fileCoverage;
18982
- }
18983
- }
18984
- }
18985
- };
18986
- };
18987
-
18988
18950
  /*
18989
18951
  * Generated helpers
18990
18952
  * - https://github.com/babel/babel/commits/main/packages/babel-helpers/src/helpers.ts
@@ -19794,21 +19756,6 @@ const jsenvPluginBabel = ({
19794
19756
  isJsModule,
19795
19757
  getImportSpecifier
19796
19758
  });
19797
- if (context.dev) {
19798
- const requestHeaders = context.request.headers;
19799
- if (requestHeaders["x-coverage-instanbul"]) {
19800
- const coverageConfig = JSON.parse(requestHeaders["x-coverage-instanbul"]);
19801
- const associations = URL_META.resolveAssociations({
19802
- cover: coverageConfig
19803
- }, context.rootDirectoryUrl);
19804
- if (URL_META.applyAssociations({
19805
- url: urlInfo.url,
19806
- associations
19807
- }).cover) {
19808
- babelPluginStructure["transform-instrument"] = [babelPluginInstrument];
19809
- }
19810
- }
19811
- }
19812
19759
  if (getCustomBabelPlugins) {
19813
19760
  Object.assign(babelPluginStructure, getCustomBabelPlugins(context));
19814
19761
  }
@@ -23911,6 +23858,54 @@ const listRelativeFileUrlToCover = async ({
23911
23858
  }) => relativeUrl);
23912
23859
  };
23913
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
+
23914
23909
  const relativeUrlToEmptyCoverage = async (relativeUrl, {
23915
23910
  signal,
23916
23911
  rootDirectoryUrl
@@ -24790,7 +24785,6 @@ const executeSteps = async (executionSteps, {
24790
24785
  process.exitCode !== 1;
24791
24786
  const startMs = Date.now();
24792
24787
  let rawOutput = "";
24793
- logger.info("");
24794
24788
  let executionLog = createLog({
24795
24789
  newLine: ""
24796
24790
  });
@@ -25099,7 +25093,10 @@ const executeTestPlan = async ({
25099
25093
  "file:///**/.*": false,
25100
25094
  "file:///**/.*/": false,
25101
25095
  "file:///**/node_modules/": false,
25102
- "./**/src/": true,
25096
+ "./**/src/**/*.js": true,
25097
+ "./**/src/**/*.ts": true,
25098
+ "./**/src/**/*.jsx": true,
25099
+ "./**/src/**/*.tsx": true,
25103
25100
  "./**/tests/": false,
25104
25101
  "./**/*.test.html": false,
25105
25102
  "./**/*.test.js": false,
@@ -25108,8 +25105,15 @@ const executeTestPlan = async ({
25108
25105
  coverageIncludeMissing = true,
25109
25106
  coverageAndExecutionAllowed = false,
25110
25107
  coverageMethodForNodeJs = process.env.NODE_V8_COVERAGE ? "NODE_V8_COVERAGE" : "Profiler",
25111
- coverageMethodForBrowsers = "playwright_api",
25112
- // "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"
25113
25117
  coverageV8ConflictWarning = true,
25114
25118
  coverageTempDirectoryUrl,
25115
25119
  // skip empty means empty files won't appear in the coverage reports (json and html)
@@ -25124,6 +25128,7 @@ const executeTestPlan = async ({
25124
25128
  ...rest
25125
25129
  }) => {
25126
25130
  let someNeedsServer = false;
25131
+ let someHasCoverageV8 = false;
25127
25132
  let someNodeRuntime = false;
25128
25133
  const runtimes = {};
25129
25134
  // param validation
@@ -25150,6 +25155,9 @@ const executeTestPlan = async ({
25150
25155
  if (runtime) {
25151
25156
  runtimes[runtime.name] = runtime.version;
25152
25157
  if (runtime.type === "browser") {
25158
+ if (runtime.capabilities && runtime.capabilities.coverageV8) {
25159
+ someHasCoverageV8 = true;
25160
+ }
25153
25161
  someNeedsServer = true;
25154
25162
  }
25155
25163
  if (runtime.type === "node") {
@@ -25162,6 +25170,9 @@ const executeTestPlan = async ({
25162
25170
  await assertAndNormalizeWebServer(webServer);
25163
25171
  }
25164
25172
  if (coverageEnabled) {
25173
+ if (coverageMethodForBrowsers === undefined) {
25174
+ coverageMethodForBrowsers = someHasCoverageV8 ? "playwright" : "istanbul";
25175
+ }
25165
25176
  if (typeof coverageConfig !== "object") {
25166
25177
  throw new TypeError(`coverageConfig must be an object, got ${coverageConfig}`);
25167
25178
  }
@@ -25322,6 +25333,172 @@ const executeTestPlan = async ({
25322
25333
  };
25323
25334
  };
25324
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
+
25325
25502
  const createRuntimeFromPlaywright = ({
25326
25503
  browserName,
25327
25504
  browserVersion,
@@ -25333,7 +25510,10 @@ const createRuntimeFromPlaywright = ({
25333
25510
  const runtime = {
25334
25511
  type: "browser",
25335
25512
  name: browserName,
25336
- version: browserVersion
25513
+ version: browserVersion,
25514
+ capabilities: {
25515
+ coverageV8: coveragePlaywrightAPIAvailable
25516
+ }
25337
25517
  };
25338
25518
  let browserAndContextPromise;
25339
25519
  runtime.run = async ({
@@ -25424,16 +25604,17 @@ ${webServer.rootDirectoryUrl}`);
25424
25604
  }
25425
25605
  await disconnected;
25426
25606
  };
25427
- const coverageInHeaders = coverageEnabled && (!coveragePlaywrightAPIAvailable || coverageMethodForBrowsers !== "playwright_api");
25428
- const page = await browserContext.newPage({
25429
- extraHTTPHeaders: {
25430
- ...(coverageInHeaders ? {
25431
- "x-coverage-istanbul": JSON.stringify(coverageConfig)
25432
- } : {})
25433
- }
25434
- });
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
+ }
25435
25616
  if (!webServer.isJsenvDevServer) {
25436
- await initJsExecutionMiddleware(page, {
25617
+ await initJsSupervisorMiddleware(page, {
25437
25618
  webServer,
25438
25619
  fileUrl,
25439
25620
  fileServerUrl
@@ -25456,7 +25637,7 @@ ${webServer.rootDirectoryUrl}`);
25456
25637
  };
25457
25638
  const callbacks = [];
25458
25639
  if (coverageEnabled) {
25459
- if (coveragePlaywrightAPIAvailable && coverageMethodForBrowsers === "playwright_api") {
25640
+ if (runtime.capabilities.coverageV8 && coverageMethodForBrowsers === "playwright") {
25460
25641
  await page.coverage.startJSCoverage({
25461
25642
  // reportAnonymousScripts: true,
25462
25643
  });
@@ -25838,106 +26019,6 @@ const extractTextFromConsoleMessage = consoleMessage => {
25838
26019
  // return text
25839
26020
  };
25840
26021
 
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
- };
25941
26022
  const registerEvent = ({
25942
26023
  object,
25943
26024
  eventType,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "34.0.2",
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
+ }
@@ -291,6 +291,7 @@ ${ANSI.color(normalizedReturnValue, ANSI.YELLOW)}
291
291
  url: urlInfo.url,
292
292
  },
293
293
  type: "sourcemap_comment",
294
+ expectedType: "sourcemap",
294
295
  subtype: urlInfo.contentType === "text/javascript" ? "js" : "css",
295
296
  parentUrl: urlInfo.url,
296
297
  specifier,
@@ -316,12 +317,16 @@ ${ANSI.color(normalizedReturnValue, ANSI.YELLOW)}
316
317
  createReference({
317
318
  trace: traceFromUrlSite(sourcemapUrlSite),
318
319
  type,
320
+ expectedType: "sourcemap",
319
321
  parentUrl: urlInfo.url,
320
322
  specifier,
321
323
  specifierLine,
322
324
  specifierColumn,
323
325
  }),
324
326
  )
327
+ if (sourcemapReference.isInline) {
328
+ sourcemapUrlInfo.isInline = true
329
+ }
325
330
  sourcemapUrlInfo.type = "sourcemap"
326
331
  return [sourcemapReference, sourcemapUrlInfo]
327
332
  },
@@ -913,6 +918,10 @@ const adjustUrlSite = (urlInfo, { urlGraph, url, line, column }) => {
913
918
  }
914
919
 
915
920
  const inferUrlInfoType = (urlInfo) => {
921
+ const { type } = urlInfo
922
+ if (type === "sourcemap") {
923
+ return "sourcemap"
924
+ }
916
925
  const { contentType } = urlInfo
917
926
  if (contentType === "text/html") {
918
927
  return "html"
@@ -123,7 +123,6 @@ export const createUrlInfoTransformer = ({
123
123
  })
124
124
  try {
125
125
  await context.cook(sourcemapUrlInfo, { reference: sourcemapReference })
126
- sourcemapUrlInfo.isInline = sourcemaps === "inline"
127
126
  const sourcemapRaw = JSON.parse(sourcemapUrlInfo.content)
128
127
  const sourcemap = normalizeSourcemap(urlInfo, sourcemapRaw)
129
128
  urlInfo.sourcemap = sourcemap
@@ -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}`,