@jsenv/core 34.0.3 → 34.1.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.
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
  }
@@ -22696,6 +22633,35 @@ const canUseVersionedUrl = urlInfo => {
22696
22633
  return urlInfo.type !== "webmanifest";
22697
22634
  };
22698
22635
 
22636
+ const WEB_URL_CONVERTER = {
22637
+ asWebUrl: (fileUrl, webServer) => {
22638
+ if (urlIsInsideOf(fileUrl, webServer.rootDirectoryUrl)) {
22639
+ return moveUrl({
22640
+ url: fileUrl,
22641
+ from: webServer.rootDirectoryUrl,
22642
+ to: `${webServer.origin}/`
22643
+ });
22644
+ }
22645
+ const fsRootUrl = ensureWindowsDriveLetter("file:///", fileUrl);
22646
+ return `${webServer.origin}/@fs/${fileUrl.slice(fsRootUrl.length)}`;
22647
+ },
22648
+ asFileUrl: (webUrl, webServer) => {
22649
+ const {
22650
+ pathname,
22651
+ search
22652
+ } = new URL(webUrl);
22653
+ if (pathname.startsWith("/@fs/")) {
22654
+ const fsRootRelativeUrl = pathname.slice("/@fs/".length);
22655
+ return `file:///${fsRootRelativeUrl}${search}`;
22656
+ }
22657
+ return moveUrl({
22658
+ url: webUrl,
22659
+ from: `${webServer.origin}/`,
22660
+ to: webServer.rootDirectoryUrl
22661
+ });
22662
+ }
22663
+ };
22664
+
22699
22665
  /*
22700
22666
  * This plugin is very special because it is here
22701
22667
  * to provide "serverEvents" used by other plugins
@@ -23145,18 +23111,9 @@ const inferParentFromRequest = (request, sourceDirectoryUrl) => {
23145
23111
  const refererUrlObject = new URL(referer);
23146
23112
  refererUrlObject.searchParams.delete("hmr");
23147
23113
  refererUrlObject.searchParams.delete("v");
23148
- const {
23149
- pathname,
23150
- search
23151
- } = refererUrlObject;
23152
- if (pathname.startsWith("/@fs/")) {
23153
- const fsRootRelativeUrl = pathname.slice("/@fs/".length);
23154
- return `file:///${fsRootRelativeUrl}${search}`;
23155
- }
23156
- return moveUrl({
23157
- url: referer,
23158
- from: `${request.origin}/`,
23159
- to: sourceDirectoryUrl
23114
+ return WEB_URL_CONVERTER.asFileUrl(referer, {
23115
+ origin: request.origin,
23116
+ rootDirectoryUrl: sourceDirectoryUrl
23160
23117
  });
23161
23118
  };
23162
23119
 
@@ -23921,6 +23878,54 @@ const listRelativeFileUrlToCover = async ({
23921
23878
  }) => relativeUrl);
23922
23879
  };
23923
23880
 
23881
+ // https://github.com/istanbuljs/babel-plugin-istanbul/blob/321740f7b25d803f881466ea819d870f7ed6a254/src/index.js
23882
+
23883
+ const babelPluginInstrument = (api, {
23884
+ useInlineSourceMaps = false
23885
+ }) => {
23886
+ const {
23887
+ programVisitor
23888
+ } = requireFromJsenv("istanbul-lib-instrument");
23889
+ const {
23890
+ types
23891
+ } = api;
23892
+ return {
23893
+ name: "transform-instrument",
23894
+ visitor: {
23895
+ Program: {
23896
+ enter(path) {
23897
+ const {
23898
+ file
23899
+ } = this;
23900
+ const {
23901
+ opts
23902
+ } = file;
23903
+ let inputSourceMap;
23904
+ if (useInlineSourceMaps) {
23905
+ // https://github.com/istanbuljs/babel-plugin-istanbul/commit/a9e15643d249a2985e4387e4308022053b2cd0ad#diff-1fdf421c05c1140f6d71444ea2b27638R65
23906
+ inputSourceMap = opts.inputSourceMap || file.inputMap ? file.inputMap.sourcemap : null;
23907
+ } else {
23908
+ inputSourceMap = opts.inputSourceMap;
23909
+ }
23910
+ this.__dv__ = programVisitor(types, opts.filenameRelative || opts.filename, {
23911
+ coverageVariable: "__coverage__",
23912
+ inputSourceMap
23913
+ });
23914
+ this.__dv__.enter(path);
23915
+ },
23916
+ exit(path) {
23917
+ if (!this.__dv__) {
23918
+ return;
23919
+ }
23920
+ const object = this.__dv__.exit(path);
23921
+ // object got two properties: fileCoverage and sourceMappingURL
23922
+ this.file.metadata.coverage = object.fileCoverage;
23923
+ }
23924
+ }
23925
+ }
23926
+ };
23927
+ };
23928
+
23924
23929
  const relativeUrlToEmptyCoverage = async (relativeUrl, {
23925
23930
  signal,
23926
23931
  rootDirectoryUrl
@@ -24800,7 +24805,6 @@ const executeSteps = async (executionSteps, {
24800
24805
  process.exitCode !== 1;
24801
24806
  const startMs = Date.now();
24802
24807
  let rawOutput = "";
24803
- logger.info("");
24804
24808
  let executionLog = createLog({
24805
24809
  newLine: ""
24806
24810
  });
@@ -25106,10 +25110,13 @@ const executeTestPlan = async ({
25106
25110
  gcBetweenExecutions = logMemoryHeapUsage,
25107
25111
  coverageEnabled = process.argv.includes("--coverage"),
25108
25112
  coverageConfig = {
25109
- "file:///**/.*": false,
25110
- "file:///**/.*/": false,
25111
25113
  "file:///**/node_modules/": false,
25112
- "./**/src/": true,
25114
+ "./**/.*": false,
25115
+ "./**/.*/": false,
25116
+ "./**/src/**/*.js": true,
25117
+ "./**/src/**/*.ts": true,
25118
+ "./**/src/**/*.jsx": true,
25119
+ "./**/src/**/*.tsx": true,
25113
25120
  "./**/tests/": false,
25114
25121
  "./**/*.test.html": false,
25115
25122
  "./**/*.test.js": false,
@@ -25118,8 +25125,15 @@ const executeTestPlan = async ({
25118
25125
  coverageIncludeMissing = true,
25119
25126
  coverageAndExecutionAllowed = false,
25120
25127
  coverageMethodForNodeJs = process.env.NODE_V8_COVERAGE ? "NODE_V8_COVERAGE" : "Profiler",
25121
- coverageMethodForBrowsers = "playwright_api",
25122
- // "istanbul" also accepted
25128
+ // - When chromium only -> coverage generated by v8
25129
+ // - When chromium + node -> coverage generated by v8 are merged
25130
+ // - When firefox only -> coverage generated by babel+istanbul
25131
+ // - When chromium + firefox
25132
+ // -> by default only coverage from chromium is used
25133
+ // and a warning is logged according to coverageV8ConflictWarning
25134
+ // -> to collect coverage from both browsers, pass coverageMethodForBrowsers: "istanbul"
25135
+ coverageMethodForBrowsers,
25136
+ // undefined | "playwright" | "istanbul"
25123
25137
  coverageV8ConflictWarning = true,
25124
25138
  coverageTempDirectoryUrl,
25125
25139
  // skip empty means empty files won't appear in the coverage reports (json and html)
@@ -25134,6 +25148,7 @@ const executeTestPlan = async ({
25134
25148
  ...rest
25135
25149
  }) => {
25136
25150
  let someNeedsServer = false;
25151
+ let someHasCoverageV8 = false;
25137
25152
  let someNodeRuntime = false;
25138
25153
  const runtimes = {};
25139
25154
  // param validation
@@ -25160,6 +25175,9 @@ const executeTestPlan = async ({
25160
25175
  if (runtime) {
25161
25176
  runtimes[runtime.name] = runtime.version;
25162
25177
  if (runtime.type === "browser") {
25178
+ if (runtime.capabilities && runtime.capabilities.coverageV8) {
25179
+ someHasCoverageV8 = true;
25180
+ }
25163
25181
  someNeedsServer = true;
25164
25182
  }
25165
25183
  if (runtime.type === "node") {
@@ -25172,6 +25190,9 @@ const executeTestPlan = async ({
25172
25190
  await assertAndNormalizeWebServer(webServer);
25173
25191
  }
25174
25192
  if (coverageEnabled) {
25193
+ if (coverageMethodForBrowsers === undefined) {
25194
+ coverageMethodForBrowsers = someHasCoverageV8 ? "playwright" : "istanbul";
25195
+ }
25175
25196
  if (typeof coverageConfig !== "object") {
25176
25197
  throw new TypeError(`coverageConfig must be an object, got ${coverageConfig}`);
25177
25198
  }
@@ -25332,6 +25353,168 @@ const executeTestPlan = async ({
25332
25353
  };
25333
25354
  };
25334
25355
 
25356
+ const initJsSupervisorMiddleware = async (page, {
25357
+ webServer,
25358
+ fileUrl,
25359
+ fileServerUrl
25360
+ }) => {
25361
+ const inlineScriptContents = new Map();
25362
+ const interceptHtmlToExecute = async ({
25363
+ route
25364
+ }) => {
25365
+ const response = await route.fetch();
25366
+ const originalBody = await response.text();
25367
+ const injectionResult = await injectSupervisorIntoHTML({
25368
+ content: originalBody,
25369
+ url: fileUrl
25370
+ }, {
25371
+ supervisorScriptSrc: `/@fs/${supervisorFileUrl$1.slice("file:///".length)}`,
25372
+ supervisorOptions: {},
25373
+ inlineAsRemote: true,
25374
+ webServer,
25375
+ onInlineScript: ({
25376
+ src,
25377
+ textContent
25378
+ }) => {
25379
+ const inlineScriptWebUrl = new URL(src, `${webServer.origin}/`).href;
25380
+ inlineScriptContents.set(inlineScriptWebUrl, textContent);
25381
+ }
25382
+ });
25383
+ route.fulfill({
25384
+ response,
25385
+ body: injectionResult.content,
25386
+ headers: {
25387
+ ...response.headers(),
25388
+ "content-length": Buffer.byteLength(injectionResult.content)
25389
+ }
25390
+ });
25391
+ };
25392
+ const interceptInlineScript = ({
25393
+ url,
25394
+ route
25395
+ }) => {
25396
+ const inlineScriptContent = inlineScriptContents.get(url);
25397
+ route.fulfill({
25398
+ status: 200,
25399
+ body: inlineScriptContent,
25400
+ headers: {
25401
+ "content-type": "text/javascript",
25402
+ "content-length": Buffer.byteLength(inlineScriptContent)
25403
+ }
25404
+ });
25405
+ };
25406
+ const interceptFileSystemUrl = ({
25407
+ url,
25408
+ route
25409
+ }) => {
25410
+ const relativeUrl = url.slice(webServer.origin.length);
25411
+ const fsPath = relativeUrl.slice("/@fs/".length);
25412
+ const fsUrl = `file:///${fsPath}`;
25413
+ const fileContent = readFileSync$1(new URL(fsUrl), "utf8");
25414
+ route.fulfill({
25415
+ status: 200,
25416
+ body: fileContent,
25417
+ headers: {
25418
+ "content-type": "text/javascript",
25419
+ "content-length": Buffer.byteLength(fileContent)
25420
+ }
25421
+ });
25422
+ };
25423
+ await page.route("**", async route => {
25424
+ const request = route.request();
25425
+ const url = request.url();
25426
+ if (url === fileServerUrl && urlToExtension$1(url) === ".html") {
25427
+ interceptHtmlToExecute({
25428
+ url,
25429
+ request,
25430
+ route
25431
+ });
25432
+ return;
25433
+ }
25434
+ if (inlineScriptContents.has(url)) {
25435
+ interceptInlineScript({
25436
+ url,
25437
+ request,
25438
+ route
25439
+ });
25440
+ return;
25441
+ }
25442
+ const fsServerUrl = new URL("/@fs/", webServer.origin);
25443
+ if (url.startsWith(fsServerUrl)) {
25444
+ interceptFileSystemUrl({
25445
+ url,
25446
+ request,
25447
+ route
25448
+ });
25449
+ return;
25450
+ }
25451
+ route.fallback();
25452
+ });
25453
+ };
25454
+
25455
+ const initIstanbulMiddleware = async (page, {
25456
+ webServer,
25457
+ rootDirectoryUrl,
25458
+ coverageConfig
25459
+ }) => {
25460
+ const associations = URL_META.resolveAssociations({
25461
+ cover: coverageConfig
25462
+ }, rootDirectoryUrl);
25463
+ await page.route("**", async route => {
25464
+ const request = route.request();
25465
+ const url = request.url(); // transform into a local url
25466
+ const fileUrl = WEB_URL_CONVERTER.asFileUrl(url, webServer);
25467
+ const needsInstrumentation = URL_META.applyAssociations({
25468
+ url: fileUrl,
25469
+ associations
25470
+ }).cover;
25471
+ if (!needsInstrumentation) {
25472
+ route.fallback();
25473
+ return;
25474
+ }
25475
+ const response = await route.fetch();
25476
+ const originalBody = await response.text();
25477
+ try {
25478
+ const result = await applyBabelPlugins({
25479
+ babelPlugins: [babelPluginInstrument],
25480
+ urlInfo: {
25481
+ originalUrl: fileUrl,
25482
+ // jsenv server could send info to know it's a js module or js classic
25483
+ // but in the end it's not super important
25484
+ // - it's ok to parse js classic as js module considering it's only for istanbul instrumentation
25485
+ type: "js_module",
25486
+ content: originalBody
25487
+ }
25488
+ });
25489
+ let code = result.code;
25490
+ code = SOURCEMAP.writeComment({
25491
+ contentType: "text/javascript",
25492
+ content: code,
25493
+ specifier: generateSourcemapDataUrl(result.map)
25494
+ });
25495
+ route.fulfill({
25496
+ response,
25497
+ body: code,
25498
+ headers: {
25499
+ ...response.headers(),
25500
+ "content-length": Buffer.byteLength(code)
25501
+ }
25502
+ });
25503
+ } catch (e) {
25504
+ if (e.code === "PARSE_ERROR") {
25505
+ route.fulfill({
25506
+ response
25507
+ });
25508
+ } else {
25509
+ console.error(e);
25510
+ route.fulfill({
25511
+ response
25512
+ });
25513
+ }
25514
+ }
25515
+ });
25516
+ };
25517
+
25335
25518
  const createRuntimeFromPlaywright = ({
25336
25519
  browserName,
25337
25520
  browserVersion,
@@ -25343,7 +25526,10 @@ const createRuntimeFromPlaywright = ({
25343
25526
  const runtime = {
25344
25527
  type: "browser",
25345
25528
  name: browserName,
25346
- version: browserVersion
25529
+ version: browserVersion,
25530
+ capabilities: {
25531
+ coverageV8: coveragePlaywrightAPIAvailable
25532
+ }
25347
25533
  };
25348
25534
  let browserAndContextPromise;
25349
25535
  runtime.run = async ({
@@ -25374,11 +25560,7 @@ ${fileUrl}
25374
25560
  --- web server root directory url ---
25375
25561
  ${webServer.rootDirectoryUrl}`);
25376
25562
  }
25377
- const fileServerUrl = moveUrl({
25378
- url: fileUrl,
25379
- from: webServer.rootDirectoryUrl,
25380
- to: `${webServer.origin}/`
25381
- });
25563
+ const fileServerUrl = WEB_URL_CONVERTER.asWebUrl(fileUrl, webServer);
25382
25564
  const cleanupCallbackList = createCallbackListNotifiedOnce();
25383
25565
  const cleanup = memoize(async reason => {
25384
25566
  await cleanupCallbackList.notify({
@@ -25434,16 +25616,17 @@ ${webServer.rootDirectoryUrl}`);
25434
25616
  }
25435
25617
  await disconnected;
25436
25618
  };
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
- });
25619
+ const page = await browserContext.newPage();
25620
+ const istanbulInstrumentationEnabled = coverageEnabled && (!runtime.capabilities.coverageV8 || coverageMethodForBrowsers === "istanbul");
25621
+ if (istanbulInstrumentationEnabled) {
25622
+ await initIstanbulMiddleware(page, {
25623
+ webServer,
25624
+ rootDirectoryUrl,
25625
+ coverageConfig
25626
+ });
25627
+ }
25445
25628
  if (!webServer.isJsenvDevServer) {
25446
- await initJsExecutionMiddleware(page, {
25629
+ await initJsSupervisorMiddleware(page, {
25447
25630
  webServer,
25448
25631
  fileUrl,
25449
25632
  fileServerUrl
@@ -25466,7 +25649,7 @@ ${webServer.rootDirectoryUrl}`);
25466
25649
  };
25467
25650
  const callbacks = [];
25468
25651
  if (coverageEnabled) {
25469
- if (coveragePlaywrightAPIAvailable && coverageMethodForBrowsers === "playwright_api") {
25652
+ if (runtime.capabilities.coverageV8 && coverageMethodForBrowsers === "playwright") {
25470
25653
  await page.coverage.startJSCoverage({
25471
25654
  // reportAnonymousScripts: true,
25472
25655
  });
@@ -25475,11 +25658,7 @@ ${webServer.rootDirectoryUrl}`);
25475
25658
  // we convert urls starting with http:// to file:// because we later
25476
25659
  // convert the url to filesystem path in istanbulCoverageFromV8Coverage function
25477
25660
  const v8CoveragesWithFsUrls = v8CoveragesWithWebUrls.map(v8CoveragesWithWebUrl => {
25478
- const fsUrl = moveUrl({
25479
- url: v8CoveragesWithWebUrl.url,
25480
- from: `${webServer.origin}/`,
25481
- to: webServer.rootDirectoryUrl
25482
- });
25661
+ const fsUrl = WEB_URL_CONVERTER.asFileUrl(v8CoveragesWithWebUrl.url, webServer);
25483
25662
  return {
25484
25663
  ...v8CoveragesWithWebUrl,
25485
25664
  url: fsUrl
@@ -25848,106 +26027,6 @@ const extractTextFromConsoleMessage = consoleMessage => {
25848
26027
  // return text
25849
26028
  };
25850
26029
 
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
26030
  const registerEvent = ({
25952
26031
  object,
25953
26032
  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.1",
4
4
  "description": "Tool to develop, test and build js projects",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -1,9 +1,10 @@
1
1
  import { readFileSync } from "node:fs"
2
2
  import { serveDirectory, composeTwoResponses } from "@jsenv/server"
3
3
  import { bufferToEtag } from "@jsenv/filesystem"
4
- import { moveUrl, asUrlWithoutSearch } from "@jsenv/urls"
4
+ import { asUrlWithoutSearch } from "@jsenv/urls"
5
5
  import { URL_META } from "@jsenv/url-meta"
6
6
 
7
+ import { WEB_URL_CONVERTER } from "../web_url_converter.js"
7
8
  import { watchSourceFiles } from "../watch_source_files.js"
8
9
  import { explorerHtmlFileUrl } from "@jsenv/core/src/plugins/explorer/jsenv_plugin_explorer.js"
9
10
  import { createUrlGraph } from "@jsenv/core/src/kitchen/url_graph.js"
@@ -437,14 +438,8 @@ const inferParentFromRequest = (request, sourceDirectoryUrl) => {
437
438
  const refererUrlObject = new URL(referer)
438
439
  refererUrlObject.searchParams.delete("hmr")
439
440
  refererUrlObject.searchParams.delete("v")
440
- const { pathname, search } = refererUrlObject
441
- if (pathname.startsWith("/@fs/")) {
442
- const fsRootRelativeUrl = pathname.slice("/@fs/".length)
443
- return `file:///${fsRootRelativeUrl}${search}`
444
- }
445
- return moveUrl({
446
- url: referer,
447
- from: `${request.origin}/`,
448
- to: sourceDirectoryUrl,
441
+ return WEB_URL_CONVERTER.asFileUrl(referer, {
442
+ origin: request.origin,
443
+ rootDirectoryUrl: sourceDirectoryUrl,
449
444
  })
450
445
  }
@@ -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,14 @@ import {
7
6
  raceProcessTeardownEvents,
8
7
  raceCallbacks,
9
8
  } from "@jsenv/abort"
10
- import { moveUrl, urlIsInsideOf, urlToExtension } from "@jsenv/urls"
9
+ import { urlIsInsideOf } from "@jsenv/urls"
11
10
  import { memoize } from "@jsenv/utils/src/memoize/memoize.js"
12
11
 
12
+ import { WEB_URL_CONVERTER } from "../../../web_url_converter.js"
13
+ import { initJsSupervisorMiddleware } from "./middleware_js_supervisor.js"
14
+ import { initIstanbulMiddleware } from "./middleware_istanbul.js"
13
15
  import { filterV8Coverage } from "@jsenv/core/src/test/coverage/v8_coverage.js"
14
16
  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
17
 
20
18
  export const createRuntimeFromPlaywright = ({
21
19
  browserName,
@@ -29,6 +27,9 @@ export const createRuntimeFromPlaywright = ({
29
27
  type: "browser",
30
28
  name: browserName,
31
29
  version: browserVersion,
30
+ capabilities: {
31
+ coverageV8: coveragePlaywrightAPIAvailable,
32
+ },
32
33
  }
33
34
  let browserAndContextPromise
34
35
  runtime.run = async ({
@@ -62,12 +63,7 @@ ${fileUrl}
62
63
  --- web server root directory url ---
63
64
  ${webServer.rootDirectoryUrl}`)
64
65
  }
65
- const fileServerUrl = moveUrl({
66
- url: fileUrl,
67
- from: webServer.rootDirectoryUrl,
68
- to: `${webServer.origin}/`,
69
- })
70
-
66
+ const fileServerUrl = WEB_URL_CONVERTER.asWebUrl(fileUrl, webServer)
71
67
  const cleanupCallbackList = createCallbackListNotifiedOnce()
72
68
  const cleanup = memoize(async (reason) => {
73
69
  await cleanupCallbackList.notify({ reason })
@@ -116,21 +112,22 @@ ${webServer.rootDirectoryUrl}`)
116
112
  }
117
113
  await disconnected
118
114
  }
119
- const coverageInHeaders =
115
+
116
+ const page = await browserContext.newPage()
117
+
118
+ const istanbulInstrumentationEnabled =
120
119
  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
- })
120
+ (!runtime.capabilities.coverageV8 ||
121
+ coverageMethodForBrowsers === "istanbul")
122
+ if (istanbulInstrumentationEnabled) {
123
+ await initIstanbulMiddleware(page, {
124
+ webServer,
125
+ rootDirectoryUrl,
126
+ coverageConfig,
127
+ })
128
+ }
132
129
  if (!webServer.isJsenvDevServer) {
133
- await initJsExecutionMiddleware(page, {
130
+ await initJsSupervisorMiddleware(page, {
134
131
  webServer,
135
132
  fileUrl,
136
133
  fileServerUrl,
@@ -155,8 +152,8 @@ ${webServer.rootDirectoryUrl}`)
155
152
  const callbacks = []
156
153
  if (coverageEnabled) {
157
154
  if (
158
- coveragePlaywrightAPIAvailable &&
159
- coverageMethodForBrowsers === "playwright_api"
155
+ runtime.capabilities.coverageV8 &&
156
+ coverageMethodForBrowsers === "playwright"
160
157
  ) {
161
158
  await page.coverage.startJSCoverage({
162
159
  // reportAnonymousScripts: true,
@@ -167,11 +164,10 @@ ${webServer.rootDirectoryUrl}`)
167
164
  // convert the url to filesystem path in istanbulCoverageFromV8Coverage function
168
165
  const v8CoveragesWithFsUrls = v8CoveragesWithWebUrls.map(
169
166
  (v8CoveragesWithWebUrl) => {
170
- const fsUrl = moveUrl({
171
- url: v8CoveragesWithWebUrl.url,
172
- from: `${webServer.origin}/`,
173
- to: webServer.rootDirectoryUrl,
174
- })
167
+ const fsUrl = WEB_URL_CONVERTER.asFileUrl(
168
+ v8CoveragesWithWebUrl.url,
169
+ webServer,
170
+ )
175
171
  return {
176
172
  ...v8CoveragesWithWebUrl,
177
173
  url: fsUrl,
@@ -570,100 +566,6 @@ const extractTextFromConsoleMessage = (consoleMessage) => {
570
566
  // return text
571
567
  }
572
568
 
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
569
  const registerEvent = ({ object, eventType, callback }) => {
668
570
  object.on(eventType, callback)
669
571
  return () => {
@@ -0,0 +1,65 @@
1
+ import { URL_META } from "@jsenv/url-meta"
2
+ import { applyBabelPlugins } from "@jsenv/ast"
3
+ import { SOURCEMAP, generateSourcemapDataUrl } from "@jsenv/sourcemap"
4
+
5
+ import { WEB_URL_CONVERTER } from "../../../web_url_converter.js"
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 = WEB_URL_CONVERTER.asFileUrl(url, webServer)
20
+ const needsInstrumentation = URL_META.applyAssociations({
21
+ url: fileUrl,
22
+ associations,
23
+ }).cover
24
+ if (!needsInstrumentation) {
25
+ route.fallback()
26
+ return
27
+ }
28
+ const response = await route.fetch()
29
+ const originalBody = await response.text()
30
+ try {
31
+ const result = await applyBabelPlugins({
32
+ babelPlugins: [babelPluginInstrument],
33
+ urlInfo: {
34
+ originalUrl: fileUrl,
35
+ // jsenv server could send info to know it's a js module or js classic
36
+ // but in the end it's not super important
37
+ // - it's ok to parse js classic as js module considering it's only for istanbul instrumentation
38
+ type: "js_module",
39
+ content: originalBody,
40
+ },
41
+ })
42
+ let code = result.code
43
+ code = SOURCEMAP.writeComment({
44
+ contentType: "text/javascript",
45
+ content: code,
46
+ specifier: generateSourcemapDataUrl(result.map),
47
+ })
48
+ route.fulfill({
49
+ response,
50
+ body: code,
51
+ headers: {
52
+ ...response.headers(),
53
+ "content-length": Buffer.byteLength(code),
54
+ },
55
+ })
56
+ } catch (e) {
57
+ if (e.code === "PARSE_ERROR") {
58
+ route.fulfill({ response })
59
+ } else {
60
+ console.error(e)
61
+ route.fulfill({ response })
62
+ }
63
+ }
64
+ })
65
+ }
@@ -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,
@@ -64,10 +64,13 @@ export const executeTestPlan = async ({
64
64
 
65
65
  coverageEnabled = process.argv.includes("--coverage"),
66
66
  coverageConfig = {
67
- "file:///**/.*": false,
68
- "file:///**/.*/": false,
69
67
  "file:///**/node_modules/": false,
70
- "./**/src/": true,
68
+ "./**/.*": false,
69
+ "./**/.*/": false,
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}`,
@@ -0,0 +1,28 @@
1
+ import { ensureWindowsDriveLetter } from "@jsenv/filesystem"
2
+ import { urlIsInsideOf, moveUrl } from "@jsenv/urls"
3
+
4
+ export const WEB_URL_CONVERTER = {
5
+ asWebUrl: (fileUrl, webServer) => {
6
+ if (urlIsInsideOf(fileUrl, webServer.rootDirectoryUrl)) {
7
+ return moveUrl({
8
+ url: fileUrl,
9
+ from: webServer.rootDirectoryUrl,
10
+ to: `${webServer.origin}/`,
11
+ })
12
+ }
13
+ const fsRootUrl = ensureWindowsDriveLetter("file:///", fileUrl)
14
+ return `${webServer.origin}/@fs/${fileUrl.slice(fsRootUrl.length)}`
15
+ },
16
+ asFileUrl: (webUrl, webServer) => {
17
+ const { pathname, search } = new URL(webUrl)
18
+ if (pathname.startsWith("/@fs/")) {
19
+ const fsRootRelativeUrl = pathname.slice("/@fs/".length)
20
+ return `file:///${fsRootRelativeUrl}${search}`
21
+ }
22
+ return moveUrl({
23
+ url: webUrl,
24
+ from: `${webServer.origin}/`,
25
+ to: webServer.rootDirectoryUrl,
26
+ })
27
+ },
28
+ }