@jsenv/core 27.0.0-alpha.82 → 27.0.0-alpha.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/js/event_source_client.js +205 -1
  2. package/dist/main.js +845 -60
  3. package/package.json +2 -2
  4. package/src/build/start_build_server.js +29 -26
  5. package/src/dev/start_dev_server.js +34 -30
  6. package/src/execute/runtimes/browsers/from_playwright.js +3 -2
  7. package/src/helpers/event_source/event_source.js +197 -0
  8. package/src/helpers/event_source/sse_service.js +53 -0
  9. package/src/helpers/worker_reload.js +56 -0
  10. package/src/plugins/autoreload/dev_sse/client/event_source_client.js +1 -1
  11. package/src/plugins/autoreload/dev_sse/jsenv_plugin_dev_sse_server.js +1 -1
  12. package/src/test/coverage/babel_plugin_instrument.js +82 -0
  13. package/src/test/coverage/coverage_reporter_html_directory.js +36 -0
  14. package/src/test/coverage/coverage_reporter_json_file.js +22 -0
  15. package/src/test/coverage/coverage_reporter_text_log.js +19 -0
  16. package/src/test/coverage/empty_coverage_factory.js +52 -0
  17. package/src/test/coverage/file_by_file_coverage.js +26 -0
  18. package/src/test/coverage/istanbul_coverage_composition.js +28 -0
  19. package/src/test/coverage/istanbul_coverage_map_from_coverage.js +16 -0
  20. package/src/test/coverage/list_files_not_covered.js +15 -0
  21. package/src/test/coverage/missing_coverage.js +41 -0
  22. package/src/test/coverage/report_to_coverage.js +196 -0
  23. package/src/test/coverage/v8_and_istanbul.js +37 -0
  24. package/src/test/coverage/v8_coverage_composition.js +24 -0
  25. package/src/test/coverage/v8_coverage_from_directory.js +87 -0
  26. package/src/test/coverage/v8_coverage_to_istanbul.js +99 -0
  27. package/src/test/execute_plan.js +2 -2
  28. package/src/test/execute_test_plan.js +3 -3
package/dist/main.js CHANGED
@@ -1,8 +1,10 @@
1
- import { parentPort } from "node:worker_threads";
2
- import { registerFileLifecycle, readFileSync as readFileSync$1, bufferToEtag, writeFileSync, ensureWindowsDriveLetter, collectFiles, assertAndNormalizeDirectoryUrl, registerDirectoryLifecycle, writeFile, ensureEmptyDirectory, writeDirectory } from "@jsenv/filesystem";
3
- import { createDetailedMessage, createLogger, createTaskLog, loggerToLevels, ANSI, msAsDuration, msAsEllapsedTime, byteAsMemoryUsage, UNICODE, createLog, startSpinner, distributePercentages, byteAsFileSize } from "@jsenv/log";
1
+ import { createSSERoom, timeStart, fetchFileSystem, composeTwoResponses, serveDirectory, startServer, pluginCORS, jsenvAccessControlAllowedHeaders, pluginServerTiming, pluginRequestWaitingCheck, composeServices, findFreePort } from "@jsenv/server";
2
+ import { registerFileLifecycle, readFileSync as readFileSync$1, bufferToEtag, writeFileSync, ensureWindowsDriveLetter, collectFiles, assertAndNormalizeDirectoryUrl, registerDirectoryLifecycle, writeFile, readFile, readDirectory, ensureEmptyDirectory, writeDirectory } from "@jsenv/filesystem";
3
+ import { createCallbackListNotifiedOnce, createCallbackList, Abort, raceProcessTeardownEvents, raceCallbacks } from "@jsenv/abort";
4
+ import { createDetailedMessage, createLogger, createTaskLog, loggerToLevels, byteAsFileSize, ANSI, msAsDuration, msAsEllapsedTime, byteAsMemoryUsage, UNICODE, createLog, startSpinner, distributePercentages } from "@jsenv/log";
4
5
  import { urlToRelativeUrl, generateInlineContentUrl, ensurePathnameTrailingSlash, urlIsInsideOf, urlToFilename, DATA_URL, injectQueryParams, injectQueryParamsIntoSpecifier, fileSystemPathToUrl, urlToFileSystemPath, isFileSystemPath, normalizeUrl, stringifyUrlSite, setUrlFilename, moveUrl, getCallerPosition, resolveUrl, resolveDirectoryUrl, asUrlWithoutSearch, asUrlUntilPathname, urlToBasename, urlToExtension } from "@jsenv/urls";
5
- import { initReloadableProcess } from "@jsenv/utils/process_reload/process_reload.js";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+ import { workerData, Worker } from "node:worker_threads";
6
8
  import { URL_META } from "@jsenv/url-meta";
7
9
  import { parseHtmlString, stringifyHtmlAst, visitHtmlAst, getHtmlNodeAttributeByName, htmlNodePosition, findNode, getHtmlNodeTextNode, removeHtmlNode, setHtmlNodeGeneratedText, removeHtmlNodeAttributeByName, parseScriptNode, injectScriptAsEarlyAsPossible, createHtmlNode, removeHtmlNodeText, assignHtmlNodeAttributes, parseLinkNode } from "@jsenv/utils/html_ast/html_ast.js";
8
10
  import { htmlAttributeSrcSet } from "@jsenv/utils/html_ast/html_attribute_src_set.js";
@@ -13,7 +15,6 @@ import { parseJsUrls } from "@jsenv/utils/js_ast/parse_js_urls.js";
13
15
  import { resolveImport, normalizeImportMap, composeTwoImportMaps } from "@jsenv/importmap";
14
16
  import { applyNodeEsmResolution, defaultLookupPackageScope, defaultReadPackageJson, readCustomConditionsFromProcessArgs, applyFileSystemMagicResolution, getExtensionsToTry } from "@jsenv/node-esm-resolution";
15
17
  import { statSync, realpathSync, readdirSync, readFileSync, existsSync } from "node:fs";
16
- import { pathToFileURL } from "node:url";
17
18
  import { CONTENT_TYPE } from "@jsenv/utils/content_type/content_type.js";
18
19
  import { JS_QUOTES } from "@jsenv/utils/string/js_quotes.js";
19
20
  import { applyBabelPlugins } from "@jsenv/utils/js_ast/apply_babel_plugins.js";
@@ -26,32 +27,75 @@ import { injectImport } from "@jsenv/utils/js_ast/babel_utils.js";
26
27
  import { sortByDependencies } from "@jsenv/utils/graph/sort_by_dependencies.js";
27
28
  import { applyRollupPlugins } from "@jsenv/utils/js_ast/apply_rollup_plugins.js";
28
29
  import { sourcemapConverter } from "@jsenv/utils/sourcemap/sourcemap_converter.js";
29
- import { createCallbackList, createCallbackListNotifiedOnce, Abort, raceCallbacks, raceProcessTeardownEvents } from "@jsenv/abort";
30
- import { createSSEService } from "@jsenv/utils/event_source/sse_service.js";
31
- import { timeStart, fetchFileSystem, composeTwoResponses, serveDirectory, startServer, pluginCORS, jsenvAccessControlAllowedHeaders, pluginServerTiming, pluginRequestWaitingCheck, composeServices, findFreePort } from "@jsenv/server";
32
30
  import { SOURCEMAP, generateSourcemapUrl, sourcemapToBase64Url } from "@jsenv/utils/sourcemap/sourcemap_utils.js";
33
31
  import { validateResponseIntegrity } from "@jsenv/integrity";
34
32
  import { convertFileSystemErrorToResponseProperties } from "@jsenv/server/src/internal/convertFileSystemErrorToResponseProperties.js";
35
33
  import { memoizeByFirstArgument } from "@jsenv/utils/memoize/memoize_by_first_argument.js";
36
- import { generateCoverageJsonFile } from "@jsenv/utils/coverage/coverage_reporter_json_file.js";
37
- import { generateCoverageHtmlDirectory } from "@jsenv/utils/coverage/coverage_reporter_html_directory.js";
38
- import { generateCoverageTextLog } from "@jsenv/utils/coverage/coverage_reporter_text_log.js";
39
34
  import { memoryUsage } from "node:process";
40
35
  import wrapAnsi from "wrap-ansi";
41
36
  import stripAnsi from "strip-ansi";
42
37
  import cuid from "cuid";
43
- import { babelPluginInstrument } from "@jsenv/utils/coverage/babel_plugin_instrument.js";
44
- import { reportToCoverage } from "@jsenv/utils/coverage/report_to_coverage.js";
45
38
  import v8 from "node:v8";
46
39
  import { runInNewContext, Script } from "node:vm";
47
40
  import { memoize } from "@jsenv/utils/memoize/memoize.js";
48
- import { filterV8Coverage } from "@jsenv/utils/coverage/v8_coverage_from_directory.js";
49
- import { composeTwoFileByFileIstanbulCoverages } from "@jsenv/utils/coverage/istanbul_coverage_composition.js";
50
41
  import { escapeRegexpSpecialChars } from "@jsenv/utils/string/escape_regexp_special_chars.js";
51
42
  import { fork } from "node:child_process";
52
43
  import { uneval } from "@jsenv/uneval";
53
44
  import { createVersionGenerator } from "@jsenv/utils/versioning/version_generator.js";
54
45
 
46
+ const createReloadableWorker = (workerFileUrl, options = {}) => {
47
+ const workerFilePath = fileURLToPath(workerFileUrl);
48
+ const isPrimary = !workerData || workerData.workerFilePath !== workerFilePath;
49
+ let worker;
50
+
51
+ const terminate = async () => {
52
+ if (worker) {
53
+ let _worker = worker;
54
+ worker = null;
55
+ const exitPromise = new Promise(resolve => {
56
+ _worker.once("exit", resolve);
57
+ });
58
+
59
+ _worker.terminate();
60
+
61
+ await exitPromise;
62
+ }
63
+ };
64
+
65
+ const load = async () => {
66
+ if (!isPrimary) {
67
+ throw new Error(`worker can be loaded from primary file only`);
68
+ }
69
+
70
+ worker = new Worker(workerFilePath, { ...options,
71
+ workerData: { ...options.workerData,
72
+ workerFilePath
73
+ }
74
+ });
75
+ worker.once("error", error => {
76
+ console.error(error);
77
+ });
78
+ worker.once("exit", () => {
79
+ worker = null;
80
+ });
81
+ await new Promise(resolve => {
82
+ worker.once("online", resolve);
83
+ });
84
+ };
85
+
86
+ const reload = async () => {
87
+ await terminate();
88
+ await load();
89
+ };
90
+
91
+ return {
92
+ isPrimary,
93
+ load,
94
+ reload,
95
+ terminate
96
+ };
97
+ };
98
+
55
99
  const parseAndTransformHtmlUrls = async (urlInfo, context) => {
56
100
  const url = urlInfo.originalUrl;
57
101
  const content = urlInfo.content;
@@ -5637,6 +5681,60 @@ const jsenvPluginDevSSEClient = () => {
5637
5681
  };
5638
5682
  };
5639
5683
 
5684
+ const createSSEService = ({
5685
+ serverEventCallbackList
5686
+ }) => {
5687
+ const destroyCallbackList = createCallbackListNotifiedOnce();
5688
+ const cache = [];
5689
+ const sseRoomLimit = 100;
5690
+
5691
+ const getOrCreateSSERoom = request => {
5692
+ const htmlFileRelativeUrl = request.ressource.slice(1);
5693
+ const cacheEntry = cache.find(cacheEntryCandidate => cacheEntryCandidate.htmlFileRelativeUrl === htmlFileRelativeUrl);
5694
+
5695
+ if (cacheEntry) {
5696
+ return cacheEntry.sseRoom;
5697
+ }
5698
+
5699
+ const sseRoom = createSSERoom({
5700
+ retryDuration: 2000,
5701
+ historyLength: 100,
5702
+ welcomeEventEnabled: true,
5703
+ effect: () => {
5704
+ return serverEventCallbackList.add(event => {
5705
+ sseRoom.sendEvent(event);
5706
+ });
5707
+ }
5708
+ });
5709
+ const removeSSECleanupCallback = destroyCallbackList.add(() => {
5710
+ removeSSECleanupCallback();
5711
+ sseRoom.close();
5712
+ });
5713
+ cache.push({
5714
+ htmlFileRelativeUrl,
5715
+ sseRoom,
5716
+ cleanup: () => {
5717
+ removeSSECleanupCallback();
5718
+ sseRoom.close();
5719
+ }
5720
+ });
5721
+
5722
+ if (cache.length >= sseRoomLimit) {
5723
+ const firstCacheEntry = cache.shift();
5724
+ firstCacheEntry.cleanup();
5725
+ }
5726
+
5727
+ return sseRoom;
5728
+ };
5729
+
5730
+ return {
5731
+ getOrCreateSSERoom,
5732
+ destroy: () => {
5733
+ destroyCallbackList.notify();
5734
+ }
5735
+ };
5736
+ };
5737
+
5640
5738
  const jsenvPluginDevSSEServer = ({
5641
5739
  rootDirectoryUrl,
5642
5740
  urlGraph,
@@ -8295,7 +8393,7 @@ const jsenvPluginExplorer = ({
8295
8393
 
8296
8394
  const startDevServer = async ({
8297
8395
  signal = new AbortController().signal,
8298
- handleSIGINT,
8396
+ handleSIGINT = true,
8299
8397
  logLevel = "info",
8300
8398
  omegaServerLogLevel = "warn",
8301
8399
  port = 3456,
@@ -8315,9 +8413,9 @@ const startDevServer = async ({
8315
8413
  devServerMainFile = getCallerPosition().url,
8316
8414
  // force disable server autoreload when this code is executed:
8317
8415
  // - inside a forked child process
8318
- // - inside a worker thread
8319
- // (because node cluster won't work)
8320
- devServerAutoreload = typeof process.send !== "function" && !parentPort && !process.env.VSCODE_INSPECTOR_OPTIONS,
8416
+ // - debugged by vscode
8417
+ // otherwise we get net:ERR_CONNECTION_REFUSED
8418
+ devServerAutoreload = typeof process.send !== "function" && !process.env.VSCODE_INSPECTOR_OPTIONS,
8321
8419
  clientFiles = {
8322
8420
  "./src/": true,
8323
8421
  "./test/": true
@@ -8357,32 +8455,36 @@ const startDevServer = async ({
8357
8455
  logLevel
8358
8456
  });
8359
8457
  rootDirectoryUrl = assertAndNormalizeDirectoryUrl(rootDirectoryUrl);
8360
- const reloadableProcess = await initReloadableProcess({
8361
- signal,
8362
- handleSIGINT,
8363
- ...(devServerAutoreload ? {
8364
- enabled: true,
8365
- logLevel: "warn",
8366
- fileToRestart: devServerMainFile
8367
- } : {
8368
- enabled: false
8369
- })
8370
- });
8458
+ const operation = Abort.startOperation();
8459
+ operation.addAbortSignal(signal);
8460
+
8461
+ if (handleSIGINT) {
8462
+ operation.addAbortSource(abort => {
8463
+ return raceProcessTeardownEvents({
8464
+ SIGINT: true
8465
+ }, abort);
8466
+ });
8467
+ }
8371
8468
 
8372
- if (reloadableProcess.isPrimary) {
8469
+ if (port === 0) {
8470
+ port = await findFreePort(port, {
8471
+ signal: operation.signal
8472
+ });
8473
+ }
8474
+
8475
+ const reloadableWorker = createReloadableWorker(devServerMainFile);
8476
+
8477
+ if (devServerAutoreload && reloadableWorker.isPrimary) {
8373
8478
  const devServerFileChangeCallback = ({
8374
8479
  relativeUrl,
8375
8480
  event
8376
8481
  }) => {
8377
8482
  const url = new URL(relativeUrl, rootDirectoryUrl).href;
8378
-
8379
- if (devServerAutoreload) {
8380
- logger.info(`file ${event} ${url} -> restarting server...`);
8381
- reloadableProcess.reload();
8382
- }
8483
+ logger.info(`file ${event} ${url} -> restarting server...`);
8484
+ reloadableWorker.reload();
8383
8485
  };
8384
8486
 
8385
- const unregisterDevServerFilesWatcher = registerDirectoryLifecycle(rootDirectoryUrl, {
8487
+ const stopWatchingDevServerFiles = registerDirectoryLifecycle(rootDirectoryUrl, {
8386
8488
  watchPatterns: {
8387
8489
  [devServerMainFile]: true,
8388
8490
  ...devServerFiles
@@ -8415,14 +8517,16 @@ const startDevServer = async ({
8415
8517
  });
8416
8518
  }
8417
8519
  });
8418
- signal.addEventListener("abort", () => {
8419
- unregisterDevServerFilesWatcher();
8520
+ operation.addAbortCallback(() => {
8521
+ stopWatchingDevServerFiles();
8522
+ reloadableWorker.terminate();
8420
8523
  });
8524
+ await reloadableWorker.load();
8421
8525
  return {
8422
8526
  origin: `${protocol}://127.0.0.1:${port}`,
8423
8527
  stop: () => {
8424
- unregisterDevServerFilesWatcher();
8425
- reloadableProcess.stop();
8528
+ stopWatchingDevServerFiles();
8529
+ reloadableWorker.terminate();
8426
8530
  }
8427
8531
  };
8428
8532
  }
@@ -8541,6 +8645,679 @@ const startDevServer = async ({
8541
8645
  };
8542
8646
  };
8543
8647
 
8648
+ const generateCoverageJsonFile = async ({
8649
+ coverage,
8650
+ coverageJsonFileUrl,
8651
+ coverageJsonFileLog,
8652
+ logger
8653
+ }) => {
8654
+ const coverageAsText = JSON.stringify(coverage, null, " ");
8655
+
8656
+ if (coverageJsonFileLog) {
8657
+ logger.info(`-> ${urlToFileSystemPath(coverageJsonFileUrl)} (${byteAsFileSize(Buffer.byteLength(coverageAsText))})`);
8658
+ }
8659
+
8660
+ await writeFile(coverageJsonFileUrl, coverageAsText);
8661
+ };
8662
+
8663
+ const istanbulCoverageMapFromCoverage = coverage => {
8664
+ const {
8665
+ createCoverageMap
8666
+ } = requireFromJsenv("istanbul-lib-coverage");
8667
+ const coverageAdjusted = {};
8668
+ Object.keys(coverage).forEach(key => {
8669
+ coverageAdjusted[key.slice(2)] = { ...coverage[key],
8670
+ path: key.slice(2)
8671
+ };
8672
+ });
8673
+ const coverageMap = createCoverageMap(coverageAdjusted);
8674
+ return coverageMap;
8675
+ };
8676
+
8677
+ const generateCoverageHtmlDirectory = async (coverage, {
8678
+ rootDirectoryUrl,
8679
+ coverageHtmlDirectoryRelativeUrl,
8680
+ coverageSkipEmpty,
8681
+ coverageSkipFull
8682
+ }) => {
8683
+ const libReport = requireFromJsenv("istanbul-lib-report");
8684
+ const reports = requireFromJsenv("istanbul-reports");
8685
+ const context = libReport.createContext({
8686
+ dir: urlToFileSystemPath(rootDirectoryUrl),
8687
+ coverageMap: istanbulCoverageMapFromCoverage(coverage),
8688
+ sourceFinder: path => {
8689
+ return readFileSync(urlToFileSystemPath(resolveUrl(path, rootDirectoryUrl)), "utf8");
8690
+ }
8691
+ });
8692
+ const report = reports.create("html", {
8693
+ skipEmpty: coverageSkipEmpty,
8694
+ skipFull: coverageSkipFull,
8695
+ subdir: coverageHtmlDirectoryRelativeUrl
8696
+ });
8697
+ report.execute(context);
8698
+ };
8699
+
8700
+ const generateCoverageTextLog = (coverage, {
8701
+ coverageSkipEmpty,
8702
+ coverageSkipFull
8703
+ }) => {
8704
+ const libReport = requireFromJsenv("istanbul-lib-report");
8705
+ const reports = requireFromJsenv("istanbul-reports");
8706
+ const context = libReport.createContext({
8707
+ coverageMap: istanbulCoverageMapFromCoverage(coverage)
8708
+ });
8709
+ const report = reports.create("text", {
8710
+ skipEmpty: coverageSkipEmpty,
8711
+ skipFull: coverageSkipFull
8712
+ });
8713
+ report.execute(context);
8714
+ };
8715
+
8716
+ const babelPluginInstrument = (api, {
8717
+ rootDirectoryUrl,
8718
+ useInlineSourceMaps = false,
8719
+ coverageConfig = {
8720
+ "./**/*": true
8721
+ }
8722
+ }) => {
8723
+ const {
8724
+ programVisitor
8725
+ } = requireFromJsenv("istanbul-lib-instrument");
8726
+ const {
8727
+ types
8728
+ } = api;
8729
+ const associations = URL_META.resolveAssociations({
8730
+ cover: coverageConfig
8731
+ }, rootDirectoryUrl);
8732
+
8733
+ const shouldInstrument = url => {
8734
+ return URL_META.applyAssociations({
8735
+ url,
8736
+ associations
8737
+ }).cover;
8738
+ };
8739
+
8740
+ return {
8741
+ name: "transform-instrument",
8742
+ visitor: {
8743
+ Program: {
8744
+ enter(path) {
8745
+ const {
8746
+ file
8747
+ } = this;
8748
+ const {
8749
+ opts
8750
+ } = file;
8751
+
8752
+ if (!opts.sourceFileName) {
8753
+ console.warn(`cannot instrument file when "sourceFileName" option is not set`);
8754
+ return;
8755
+ }
8756
+
8757
+ const fileUrl = fileSystemPathToUrl(opts.sourceFileName);
8758
+
8759
+ if (!shouldInstrument(fileUrl)) {
8760
+ return;
8761
+ }
8762
+
8763
+ this.__dv__ = null;
8764
+ let inputSourceMap;
8765
+
8766
+ if (useInlineSourceMaps) {
8767
+ // https://github.com/istanbuljs/babel-plugin-istanbul/commit/a9e15643d249a2985e4387e4308022053b2cd0ad#diff-1fdf421c05c1140f6d71444ea2b27638R65
8768
+ inputSourceMap = opts.inputSourceMap || file.inputMap ? file.inputMap.sourcemap : null;
8769
+ } else {
8770
+ inputSourceMap = opts.inputSourceMap;
8771
+ }
8772
+
8773
+ this.__dv__ = programVisitor(types, opts.filenameRelative || opts.filename, {
8774
+ coverageVariable: "__coverage__",
8775
+ inputSourceMap
8776
+ });
8777
+
8778
+ this.__dv__.enter(path);
8779
+ },
8780
+
8781
+ exit(path) {
8782
+ if (!this.__dv__) {
8783
+ return;
8784
+ }
8785
+
8786
+ const object = this.__dv__.exit(path); // object got two properties: fileCoverage and sourceMappingURL
8787
+
8788
+
8789
+ this.file.metadata.coverage = object.fileCoverage;
8790
+ }
8791
+
8792
+ }
8793
+ }
8794
+ };
8795
+ };
8796
+
8797
+ const visitNodeV8Directory = async ({
8798
+ logger,
8799
+ signal,
8800
+ NODE_V8_COVERAGE,
8801
+ onV8Coverage,
8802
+ maxMsWaitingForNodeToWriteCoverageFile = 2000
8803
+ }) => {
8804
+ const operation = Abort.startOperation();
8805
+ operation.addAbortSignal(signal);
8806
+
8807
+ const tryReadDirectory = async () => {
8808
+ const dirContent = await readDirectory(NODE_V8_COVERAGE);
8809
+
8810
+ if (dirContent.length > 0) {
8811
+ return dirContent;
8812
+ }
8813
+
8814
+ logger.warn(`v8 coverage directory is empty at ${NODE_V8_COVERAGE}`);
8815
+ return dirContent;
8816
+ };
8817
+
8818
+ try {
8819
+ operation.throwIfAborted();
8820
+ const dirContent = await tryReadDirectory();
8821
+ const coverageDirectoryUrl = assertAndNormalizeDirectoryUrl(NODE_V8_COVERAGE);
8822
+ await dirContent.reduce(async (previous, dirEntry) => {
8823
+ operation.throwIfAborted();
8824
+ await previous;
8825
+ const dirEntryUrl = resolveUrl(dirEntry, coverageDirectoryUrl);
8826
+
8827
+ const tryReadJsonFile = async (timeSpentTrying = 0) => {
8828
+ const fileContent = await readFile(dirEntryUrl, {
8829
+ as: "string"
8830
+ });
8831
+
8832
+ if (fileContent === "") {
8833
+ if (timeSpentTrying < 400) {
8834
+ await new Promise(resolve => setTimeout(resolve, 200));
8835
+ return tryReadJsonFile(timeSpentTrying + 200);
8836
+ }
8837
+
8838
+ console.warn(`Coverage JSON file is empty at ${dirEntryUrl}`);
8839
+ return null;
8840
+ }
8841
+
8842
+ try {
8843
+ const fileAsJson = JSON.parse(fileContent);
8844
+ return fileAsJson;
8845
+ } catch (e) {
8846
+ if (timeSpentTrying < maxMsWaitingForNodeToWriteCoverageFile) {
8847
+ await new Promise(resolve => setTimeout(resolve, 200));
8848
+ return tryReadJsonFile(timeSpentTrying + 200);
8849
+ }
8850
+
8851
+ console.warn(createDetailedMessage(`Error while reading coverage file`, {
8852
+ "error stack": e.stack,
8853
+ "file": dirEntryUrl
8854
+ }));
8855
+ return null;
8856
+ }
8857
+ };
8858
+
8859
+ const fileContent = await tryReadJsonFile();
8860
+
8861
+ if (fileContent) {
8862
+ onV8Coverage(fileContent);
8863
+ }
8864
+ }, Promise.resolve());
8865
+ } finally {
8866
+ await operation.end();
8867
+ }
8868
+ };
8869
+ const filterV8Coverage = (v8Coverage, {
8870
+ urlShouldBeCovered
8871
+ }) => {
8872
+ const v8CoverageFiltered = { ...v8Coverage,
8873
+ result: v8Coverage.result.filter(fileReport => urlShouldBeCovered(fileReport.url))
8874
+ };
8875
+ return v8CoverageFiltered;
8876
+ };
8877
+
8878
+ const composeTwoV8Coverages = (firstV8Coverage, secondV8Coverage) => {
8879
+ if (secondV8Coverage.result.length === 0) {
8880
+ return firstV8Coverage;
8881
+ } // eslint-disable-next-line import/no-unresolved
8882
+
8883
+
8884
+ const {
8885
+ mergeProcessCovs
8886
+ } = requireFromJsenv("@c88/v8-coverage"); // "mergeProcessCovs" do not preserves source-map-cache during the merge
8887
+ // so we store sourcemap cache now
8888
+
8889
+ const sourceMapCache = {};
8890
+
8891
+ const visit = coverageReport => {
8892
+ if (coverageReport["source-map-cache"]) {
8893
+ Object.assign(sourceMapCache, coverageReport["source-map-cache"]);
8894
+ }
8895
+ };
8896
+
8897
+ visit(firstV8Coverage);
8898
+ visit(secondV8Coverage);
8899
+ const v8Coverage = mergeProcessCovs([firstV8Coverage, secondV8Coverage]);
8900
+ v8Coverage["source-map-cache"] = sourceMapCache;
8901
+ return v8Coverage;
8902
+ };
8903
+
8904
+ const composeTwoFileByFileIstanbulCoverages = (firstFileByFileIstanbulCoverage, secondFileByFileIstanbulCoverage) => {
8905
+ const fileByFileIstanbulCoverage = {};
8906
+ Object.keys(firstFileByFileIstanbulCoverage).forEach(key => {
8907
+ fileByFileIstanbulCoverage[key] = firstFileByFileIstanbulCoverage[key];
8908
+ });
8909
+ Object.keys(secondFileByFileIstanbulCoverage).forEach(key => {
8910
+ const firstCoverage = firstFileByFileIstanbulCoverage[key];
8911
+ const secondCoverage = secondFileByFileIstanbulCoverage[key];
8912
+ fileByFileIstanbulCoverage[key] = firstCoverage ? merge(firstCoverage, secondCoverage) : secondCoverage;
8913
+ });
8914
+ return fileByFileIstanbulCoverage;
8915
+ };
8916
+
8917
+ const merge = (firstIstanbulCoverage, secondIstanbulCoverage) => {
8918
+ const {
8919
+ createFileCoverage
8920
+ } = requireFromJsenv("istanbul-lib-coverage");
8921
+ const istanbulFileCoverageObject = createFileCoverage(firstIstanbulCoverage);
8922
+ istanbulFileCoverageObject.merge(secondIstanbulCoverage);
8923
+ const istanbulCoverage = istanbulFileCoverageObject.toJSON();
8924
+ return istanbulCoverage;
8925
+ };
8926
+
8927
+ const v8CoverageToIstanbul = async (v8Coverage, {
8928
+ signal
8929
+ }) => {
8930
+ const operation = Abort.startOperation();
8931
+ operation.addAbortSignal(signal);
8932
+
8933
+ try {
8934
+ const v8ToIstanbul = requireFromJsenv("v8-to-istanbul");
8935
+ const sourcemapCache = v8Coverage["source-map-cache"];
8936
+ let istanbulCoverageComposed = null;
8937
+ await v8Coverage.result.reduce(async (previous, fileV8Coverage) => {
8938
+ operation.throwIfAborted();
8939
+ await previous;
8940
+ const {
8941
+ source
8942
+ } = fileV8Coverage;
8943
+ let sources; // when v8 coverage comes from playwright (chromium) v8Coverage.source is set
8944
+
8945
+ if (typeof source === "string") {
8946
+ sources = {
8947
+ source
8948
+ };
8949
+ } // when v8 coverage comes from Node.js, the source can be read from sourcemapCache
8950
+ else if (sourcemapCache) {
8951
+ sources = sourcesFromSourceMapCache(fileV8Coverage.url, sourcemapCache);
8952
+ }
8953
+
8954
+ const path = urlToFileSystemPath(fileV8Coverage.url);
8955
+ const converter = v8ToIstanbul(path, // wrapperLength is undefined we don't need it
8956
+ // https://github.com/istanbuljs/v8-to-istanbul/blob/2b54bc97c5edf8a37b39a171ec29134ba9bfd532/lib/v8-to-istanbul.js#L27
8957
+ undefined, sources);
8958
+ await converter.load();
8959
+ converter.applyCoverage(fileV8Coverage.functions);
8960
+ const istanbulCoverage = converter.toIstanbul();
8961
+ istanbulCoverageComposed = istanbulCoverageComposed ? composeTwoFileByFileIstanbulCoverages(istanbulCoverageComposed, istanbulCoverage) : istanbulCoverage;
8962
+ }, Promise.resolve());
8963
+
8964
+ if (!istanbulCoverageComposed) {
8965
+ return {};
8966
+ }
8967
+
8968
+ istanbulCoverageComposed = markAsConvertedFromV8(istanbulCoverageComposed);
8969
+ return istanbulCoverageComposed;
8970
+ } finally {
8971
+ await operation.end();
8972
+ }
8973
+ };
8974
+
8975
+ const markAsConvertedFromV8 = fileByFileCoverage => {
8976
+ const fileByFileMarked = {};
8977
+ Object.keys(fileByFileCoverage).forEach(key => {
8978
+ const fileCoverage = fileByFileCoverage[key];
8979
+ fileByFileMarked[key] = { ...fileCoverage,
8980
+ fromV8: true
8981
+ };
8982
+ });
8983
+ return fileByFileMarked;
8984
+ };
8985
+
8986
+ const sourcesFromSourceMapCache = (url, sourceMapCache) => {
8987
+ const sourceMapAndLineLengths = sourceMapCache[url];
8988
+
8989
+ if (!sourceMapAndLineLengths) {
8990
+ return {};
8991
+ }
8992
+
8993
+ const {
8994
+ data,
8995
+ lineLengths
8996
+ } = sourceMapAndLineLengths; // See: https://github.com/nodejs/node/pull/34305
8997
+
8998
+ if (!data) {
8999
+ return undefined;
9000
+ }
9001
+
9002
+ const sources = {
9003
+ sourcemap: data,
9004
+ ...(lineLengths ? {
9005
+ source: sourcesFromLineLengths(lineLengths)
9006
+ } : {})
9007
+ };
9008
+ return sources;
9009
+ };
9010
+
9011
+ const sourcesFromLineLengths = lineLengths => {
9012
+ let source = "";
9013
+ lineLengths.forEach(length => {
9014
+ source += `${"".padEnd(length, ".")}\n`;
9015
+ });
9016
+ return source;
9017
+ };
9018
+
9019
+ const composeV8AndIstanbul = (v8FileByFileCoverage, istanbulFileByFileCoverage, {
9020
+ coverageV8ConflictWarning
9021
+ }) => {
9022
+ const fileByFileCoverage = {};
9023
+ const v8Files = Object.keys(v8FileByFileCoverage);
9024
+ const istanbulFiles = Object.keys(istanbulFileByFileCoverage);
9025
+ v8Files.forEach(key => {
9026
+ fileByFileCoverage[key] = v8FileByFileCoverage[key];
9027
+ });
9028
+ istanbulFiles.forEach(key => {
9029
+ const v8Coverage = v8FileByFileCoverage[key];
9030
+
9031
+ if (v8Coverage) {
9032
+ if (coverageV8ConflictWarning) {
9033
+ console.warn(createDetailedMessage(`Coverage conflict on "${key}", found two coverage that cannot be merged together: v8 and istanbul. The istanbul coverage will be ignored.`, {
9034
+ details: `This happens when a file is executed on a runtime using v8 coverage (node or chromium) and on runtime using istanbul coverage (firefox or webkit)`,
9035
+ suggestion: "You can disable this warning with coverageV8ConflictWarning: false"
9036
+ }));
9037
+ }
9038
+
9039
+ fileByFileCoverage[key] = v8Coverage;
9040
+ } else {
9041
+ fileByFileCoverage[key] = istanbulFileByFileCoverage[key];
9042
+ }
9043
+ });
9044
+ return fileByFileCoverage;
9045
+ };
9046
+
9047
+ const normalizeFileByFileCoveragePaths = (fileByFileCoverage, rootDirectoryUrl) => {
9048
+ const fileByFileNormalized = {};
9049
+ Object.keys(fileByFileCoverage).forEach(key => {
9050
+ const fileCoverage = fileByFileCoverage[key];
9051
+ const {
9052
+ path
9053
+ } = fileCoverage;
9054
+ const url = isFileSystemPath(path) ? fileSystemPathToUrl(path) : resolveUrl(path, rootDirectoryUrl);
9055
+ const relativeUrl = urlToRelativeUrl(url, rootDirectoryUrl);
9056
+ fileByFileNormalized[`./${relativeUrl}`] = { ...fileCoverage,
9057
+ path: `./${relativeUrl}`
9058
+ };
9059
+ });
9060
+ return fileByFileNormalized;
9061
+ };
9062
+
9063
+ const listRelativeFileUrlToCover = async ({
9064
+ signal,
9065
+ rootDirectoryUrl,
9066
+ coverageConfig
9067
+ }) => {
9068
+ const matchingFileResultArray = await collectFiles({
9069
+ signal,
9070
+ directoryUrl: rootDirectoryUrl,
9071
+ associations: {
9072
+ cover: coverageConfig
9073
+ },
9074
+ predicate: ({
9075
+ cover
9076
+ }) => cover
9077
+ });
9078
+ return matchingFileResultArray.map(({
9079
+ relativeUrl
9080
+ }) => relativeUrl);
9081
+ };
9082
+
9083
+ const relativeUrlToEmptyCoverage = async (relativeUrl, {
9084
+ signal,
9085
+ rootDirectoryUrl
9086
+ }) => {
9087
+ const operation = Abort.startOperation();
9088
+ operation.addAbortSignal(signal);
9089
+
9090
+ try {
9091
+ const fileUrl = resolveUrl(relativeUrl, rootDirectoryUrl);
9092
+ const content = await readFile(fileUrl, {
9093
+ as: "string"
9094
+ });
9095
+ operation.throwIfAborted();
9096
+ const {
9097
+ metadata
9098
+ } = await applyBabelPlugins({
9099
+ babelPlugins: [[babelPluginInstrument, {
9100
+ rootDirectoryUrl
9101
+ }]],
9102
+ urlInfo: {
9103
+ originalUrl: fileUrl,
9104
+ content
9105
+ }
9106
+ });
9107
+ const {
9108
+ coverage
9109
+ } = metadata;
9110
+
9111
+ if (!coverage) {
9112
+ throw new Error(`missing coverage for file`);
9113
+ } // https://github.com/gotwarlost/istanbul/blob/bc84c315271a5dd4d39bcefc5925cfb61a3d174a/lib/command/common/run-with-cover.js#L229
9114
+
9115
+
9116
+ Object.keys(coverage.s).forEach(function (key) {
9117
+ coverage.s[key] = 0;
9118
+ });
9119
+ return coverage;
9120
+ } catch (e) {
9121
+ if (e && e.code === "PARSE_ERROR") {
9122
+ // return an empty coverage for that file when
9123
+ // it contains a syntax error
9124
+ return createEmptyCoverage(relativeUrl);
9125
+ }
9126
+
9127
+ throw e;
9128
+ } finally {
9129
+ await operation.end();
9130
+ }
9131
+ };
9132
+
9133
+ const createEmptyCoverage = relativeUrl => {
9134
+ const {
9135
+ createFileCoverage
9136
+ } = requireFromJsenv("istanbul-lib-coverage");
9137
+ return createFileCoverage(relativeUrl).toJSON();
9138
+ };
9139
+
9140
+ const getMissingFileByFileCoverage = async ({
9141
+ signal,
9142
+ rootDirectoryUrl,
9143
+ coverageConfig,
9144
+ fileByFileCoverage
9145
+ }) => {
9146
+ const relativeUrlsToCover = await listRelativeFileUrlToCover({
9147
+ signal,
9148
+ rootDirectoryUrl,
9149
+ coverageConfig
9150
+ });
9151
+ const relativeUrlsMissing = relativeUrlsToCover.filter(relativeUrlToCover => Object.keys(fileByFileCoverage).every(key => {
9152
+ return key !== `./${relativeUrlToCover}`;
9153
+ }));
9154
+ const operation = Abort.startOperation();
9155
+ operation.addAbortSignal(signal);
9156
+ const missingFileByFileCoverage = {};
9157
+ await relativeUrlsMissing.reduce(async (previous, relativeUrlMissing) => {
9158
+ operation.throwIfAborted();
9159
+ await previous;
9160
+ await operation.withSignal(async signal => {
9161
+ const emptyCoverage = await relativeUrlToEmptyCoverage(relativeUrlMissing, {
9162
+ signal,
9163
+ rootDirectoryUrl
9164
+ });
9165
+ missingFileByFileCoverage[`./${relativeUrlMissing}`] = emptyCoverage;
9166
+ });
9167
+ }, Promise.resolve());
9168
+ return missingFileByFileCoverage;
9169
+ };
9170
+
9171
+ const reportToCoverage = async (report, {
9172
+ signal,
9173
+ logger,
9174
+ rootDirectoryUrl,
9175
+ coverageConfig,
9176
+ coverageIncludeMissing,
9177
+ urlShouldBeCovered,
9178
+ coverageForceIstanbul,
9179
+ coverageV8ConflictWarning
9180
+ }) => {
9181
+ // collect v8 and istanbul coverage from executions
9182
+ let {
9183
+ v8Coverage,
9184
+ fileByFileIstanbulCoverage
9185
+ } = await getCoverageFromReport({
9186
+ signal,
9187
+ report,
9188
+ onMissing: ({
9189
+ file,
9190
+ executionResult,
9191
+ executionName
9192
+ }) => {
9193
+ // several reasons not to have coverage here:
9194
+ // 1. the file we executed did not import an instrumented file.
9195
+ // - a test file without import
9196
+ // - a test file importing only file excluded from coverage
9197
+ // - a coverDescription badly configured so that we don't realize
9198
+ // a file should be covered
9199
+ // 2. the file we wanted to executed timedout
9200
+ // - infinite loop
9201
+ // - too extensive operation
9202
+ // - a badly configured or too low allocatedMs for that execution.
9203
+ // 3. the file we wanted to execute contains syntax-error
9204
+ // in any scenario we are fine because
9205
+ // coverDescription will generate empty coverage for files
9206
+ // that were suppose to be coverage but were not.
9207
+ if (executionResult.status === "completed" && executionResult.runtimeName !== "node" && !process.env.NODE_V8_COVERAGE) {
9208
+ logger.warn(`No execution.coverageFileUrl from execution named "${executionName}" of ${file}`);
9209
+ }
9210
+ }
9211
+ });
9212
+
9213
+ if (!coverageForceIstanbul && process.env.NODE_V8_COVERAGE) {
9214
+ await visitNodeV8Directory({
9215
+ logger,
9216
+ signal,
9217
+ NODE_V8_COVERAGE: process.env.NODE_V8_COVERAGE,
9218
+ onV8Coverage: nodeV8Coverage => {
9219
+ const nodeV8CoverageLight = filterV8Coverage(nodeV8Coverage, {
9220
+ urlShouldBeCovered
9221
+ });
9222
+ v8Coverage = v8Coverage ? composeTwoV8Coverages(v8Coverage, nodeV8CoverageLight) : nodeV8CoverageLight;
9223
+ }
9224
+ });
9225
+ } // try to merge v8 with istanbul, if any
9226
+
9227
+
9228
+ let fileByFileCoverage;
9229
+
9230
+ if (v8Coverage) {
9231
+ let v8FileByFileCoverage = await v8CoverageToIstanbul(v8Coverage, {
9232
+ signal
9233
+ });
9234
+ v8FileByFileCoverage = normalizeFileByFileCoveragePaths(v8FileByFileCoverage, rootDirectoryUrl);
9235
+
9236
+ if (fileByFileIstanbulCoverage) {
9237
+ fileByFileIstanbulCoverage = normalizeFileByFileCoveragePaths(fileByFileIstanbulCoverage, rootDirectoryUrl);
9238
+ fileByFileCoverage = composeV8AndIstanbul(v8FileByFileCoverage, fileByFileIstanbulCoverage, {
9239
+ coverageV8ConflictWarning
9240
+ });
9241
+ } else {
9242
+ fileByFileCoverage = v8FileByFileCoverage;
9243
+ }
9244
+ } // get istanbul only
9245
+ else if (fileByFileIstanbulCoverage) {
9246
+ fileByFileCoverage = normalizeFileByFileCoveragePaths(fileByFileIstanbulCoverage, rootDirectoryUrl);
9247
+ } // no coverage found in execution (or zero file where executed)
9248
+ else {
9249
+ fileByFileCoverage = {};
9250
+ } // now add coverage for file not covered
9251
+
9252
+
9253
+ if (coverageIncludeMissing) {
9254
+ const missingFileByFileCoverage = await getMissingFileByFileCoverage({
9255
+ signal,
9256
+ rootDirectoryUrl,
9257
+ coverageConfig,
9258
+ fileByFileCoverage
9259
+ });
9260
+ Object.assign(fileByFileCoverage, normalizeFileByFileCoveragePaths(missingFileByFileCoverage, rootDirectoryUrl));
9261
+ }
9262
+
9263
+ return fileByFileCoverage;
9264
+ };
9265
+
9266
+ const getCoverageFromReport = async ({
9267
+ signal,
9268
+ report,
9269
+ onMissing
9270
+ }) => {
9271
+ const operation = Abort.startOperation();
9272
+ operation.addAbortSignal(signal);
9273
+
9274
+ try {
9275
+ let v8Coverage;
9276
+ let fileByFileIstanbulCoverage; // collect v8 and istanbul coverage from executions
9277
+
9278
+ await Object.keys(report).reduce(async (previous, file) => {
9279
+ operation.throwIfAborted();
9280
+ await previous;
9281
+ const executionResultForFile = report[file];
9282
+ await Object.keys(executionResultForFile).reduce(async (previous, executionName) => {
9283
+ operation.throwIfAborted();
9284
+ await previous;
9285
+ const executionResultForFileOnRuntime = executionResultForFile[executionName];
9286
+ const {
9287
+ coverageFileUrl
9288
+ } = executionResultForFileOnRuntime;
9289
+
9290
+ if (!coverageFileUrl) {
9291
+ onMissing({
9292
+ executionName,
9293
+ file,
9294
+ executionResult: executionResultForFileOnRuntime
9295
+ });
9296
+ return;
9297
+ }
9298
+
9299
+ const executionCoverage = await readFile(coverageFileUrl, {
9300
+ as: "json"
9301
+ });
9302
+
9303
+ if (isV8Coverage(executionCoverage)) {
9304
+ v8Coverage = v8Coverage ? composeTwoV8Coverages(v8Coverage, executionCoverage) : executionCoverage;
9305
+ } else {
9306
+ fileByFileIstanbulCoverage = fileByFileIstanbulCoverage ? composeTwoFileByFileIstanbulCoverages(fileByFileIstanbulCoverage, executionCoverage) : executionCoverage;
9307
+ }
9308
+ }, Promise.resolve());
9309
+ }, Promise.resolve());
9310
+ return {
9311
+ v8Coverage,
9312
+ fileByFileIstanbulCoverage
9313
+ };
9314
+ } finally {
9315
+ await operation.end();
9316
+ }
9317
+ };
9318
+
9319
+ const isV8Coverage = coverage => Boolean(coverage.result);
9320
+
8544
9321
  const run = async ({
8545
9322
  signal = new AbortController().signal,
8546
9323
  logger,
@@ -13036,9 +13813,9 @@ const startBuildServer = async ({
13036
13813
  buildServerMainFile = getCallerPosition().url,
13037
13814
  // force disable server autoreload when this code is executed:
13038
13815
  // - inside a forked child process
13039
- // - inside a worker thread
13040
- // (because node cluster won't work)
13041
- buildServerAutoreload = typeof process.send !== "function" && !parentPort && !process.env.VSCODE_INSPECTOR_OPTIONS,
13816
+ // - debugged by vscode
13817
+ // otherwise we get net:ERR_CONNECTION_REFUSED
13818
+ buildServerAutoreload = typeof process.send !== "function" && !process.env.VSCODE_INSPECTOR_OPTIONS,
13042
13819
  cooldownBetweenFileEvents
13043
13820
  }) => {
13044
13821
  const logger = createLogger({
@@ -13046,19 +13823,26 @@ const startBuildServer = async ({
13046
13823
  });
13047
13824
  rootDirectoryUrl = assertAndNormalizeDirectoryUrl(rootDirectoryUrl);
13048
13825
  buildDirectoryUrl = assertAndNormalizeDirectoryUrl(buildDirectoryUrl);
13049
- const reloadableProcess = await initReloadableProcess({
13050
- signal,
13051
- handleSIGINT,
13052
- ...(buildServerAutoreload ? {
13053
- enabled: true,
13054
- logLevel: "info",
13055
- fileToRestart: buildServerMainFile
13056
- } : {
13057
- enabled: false
13058
- })
13059
- });
13826
+ const operation = Abort.startOperation();
13827
+ operation.addAbortSignal(signal);
13828
+
13829
+ if (handleSIGINT) {
13830
+ operation.addAbortSource(abort => {
13831
+ return raceProcessTeardownEvents({
13832
+ SIGINT: true
13833
+ }, abort);
13834
+ });
13835
+ }
13836
+
13837
+ if (port === 0) {
13838
+ port = await findFreePort(port, {
13839
+ signal: operation.signal
13840
+ });
13841
+ }
13842
+
13843
+ const reloadableWorker = createReloadableWorker(buildServerMainFile);
13060
13844
 
13061
- if (reloadableProcess.isPrimary) {
13845
+ if (buildServerAutoreload && reloadableWorker.isPrimary) {
13062
13846
  const buildServerFileChangeCallback = ({
13063
13847
  relativeUrl,
13064
13848
  event
@@ -13067,7 +13851,7 @@ const startBuildServer = async ({
13067
13851
 
13068
13852
  if (buildServerAutoreload) {
13069
13853
  logger.info(`file ${event} ${url} -> restarting server...`);
13070
- reloadableProcess.reload();
13854
+ reloadableWorker.reload();
13071
13855
  }
13072
13856
  };
13073
13857
 
@@ -13104,19 +13888,20 @@ const startBuildServer = async ({
13104
13888
  });
13105
13889
  }
13106
13890
  });
13107
- signal.addEventListener("abort", () => {
13891
+ operation.addAbortCallback(() => {
13108
13892
  stopWatchingBuildServerFiles();
13893
+ reloadableWorker.terminate();
13109
13894
  });
13895
+ await reloadableWorker.load();
13110
13896
  return {
13111
13897
  origin: `${protocol}://127.0.0.1:${port}`,
13112
13898
  stop: () => {
13113
13899
  stopWatchingBuildServerFiles();
13114
- reloadableProcess.stop();
13900
+ reloadableWorker.terminate();
13115
13901
  }
13116
13902
  };
13117
13903
  }
13118
13904
 
13119
- signal = reloadableProcess.signal;
13120
13905
  const startBuildServerTask = createTaskLog("start build server", {
13121
13906
  disabled: !loggerToLevels(logger).info
13122
13907
  });