@jsenv/core 27.1.0 → 27.2.2

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 (36) hide show
  1. package/dist/controllable_child_process.mjs +139 -0
  2. package/dist/controllable_worker_thread.mjs +103 -0
  3. package/dist/js/execute_using_dynamic_import.js +169 -0
  4. package/dist/js/v8_coverage.js +539 -0
  5. package/dist/main.js +580 -806
  6. package/package.json +10 -9
  7. package/src/build/build.js +9 -12
  8. package/src/build/build_urls_generator.js +1 -1
  9. package/src/build/inject_global_version_mappings.js +3 -2
  10. package/src/build/inject_service_worker_urls.js +1 -2
  11. package/src/execute/run.js +50 -68
  12. package/src/execute/runtimes/browsers/chromium.js +1 -1
  13. package/src/execute/runtimes/browsers/firefox.js +1 -1
  14. package/src/execute/runtimes/browsers/from_playwright.js +13 -8
  15. package/src/execute/runtimes/browsers/webkit.js +1 -1
  16. package/src/execute/runtimes/node/{controllable_file.mjs → controllable_child_process.mjs} +18 -50
  17. package/src/execute/runtimes/node/controllable_worker_thread.mjs +103 -0
  18. package/src/execute/runtimes/node/execute_using_dynamic_import.js +49 -0
  19. package/src/execute/runtimes/node/exit_codes.js +9 -0
  20. package/src/execute/runtimes/node/{node_process.js → node_child_process.js} +56 -50
  21. package/src/execute/runtimes/node/node_worker_thread.js +268 -25
  22. package/src/execute/runtimes/node/profiler_v8_coverage.js +56 -0
  23. package/src/main.js +3 -1
  24. package/src/omega/kitchen.js +19 -6
  25. package/src/omega/server/file_service.js +2 -2
  26. package/src/omega/url_graph/url_graph_load.js +0 -1
  27. package/src/omega/url_graph.js +1 -0
  28. package/src/plugins/bundling/js_module/bundle_js_module.js +2 -5
  29. package/src/plugins/transpilation/as_js_classic/jsenv_plugin_as_js_classic.js +18 -15
  30. package/src/plugins/url_resolution/jsenv_plugin_url_resolution.js +2 -1
  31. package/src/test/coverage/report_to_coverage.js +16 -19
  32. package/src/test/coverage/v8_coverage.js +26 -0
  33. package/src/test/coverage/{v8_coverage_from_directory.js → v8_coverage_node_directory.js} +22 -26
  34. package/src/test/execute_plan.js +92 -91
  35. package/src/test/execute_test_plan.js +15 -13
  36. package/dist/js/controllable_file.mjs +0 -227
@@ -28,7 +28,7 @@ import { executePlan } from "./execute_plan.js"
28
28
  * @param {boolean} [testPlanParameters.failFast=false] Fails immediatly when a test execution fails
29
29
  * @param {number} [testPlanParameters.cooldownBetweenExecutions=0] Millisecond to wait between each execution
30
30
  * @param {boolean} [testPlanParameters.logMemoryHeapUsage=false] Add memory heap usage during logs
31
- * @param {boolean} [testPlanParameters.coverage=false] Controls if coverage is collected during files executions
31
+ * @param {boolean} [testPlanParameters.coverageEnabled=false] Controls if coverage is collected during files executions
32
32
  * @param {boolean} [testPlanParameters.coverageV8ConflictWarning=true] Warn when coverage from 2 executions cannot be merged
33
33
  * @return {Object} An object containing the result of all file executions
34
34
  */
@@ -60,23 +60,24 @@ export const executeTestPlan = async ({
60
60
  cooldownBetweenExecutions = 0,
61
61
  gcBetweenExecutions = logMemoryHeapUsage,
62
62
 
63
- coverage = process.argv.includes("--cover") ||
63
+ coverageEnabled = process.argv.includes("--cover") ||
64
64
  process.argv.includes("--coverage"),
65
- coverageTempDirectoryRelativeUrl = "./.coverage/tmp/",
66
65
  coverageConfig = {
67
66
  "./src/": true,
68
67
  },
69
68
  coverageIncludeMissing = true,
70
69
  coverageAndExecutionAllowed = false,
71
- coverageForceIstanbul = false,
70
+ coverageMethodForNodeJs = "NODE_V8_COVERAGE", // "Profiler" also accepted
71
+ coverageMethodForBrowsers = "playwright_api", // "istanbul" also accepted
72
72
  coverageV8ConflictWarning = true,
73
- coverageReportTextLog = true,
74
- coverageReportJsonFile = process.env.CI ? null : "./.coverage/coverage.json",
75
- coverageReportHtmlDirectory = process.env.CI ? "./.coverage/" : null,
73
+ coverageTempDirectoryRelativeUrl = "./.coverage/tmp/",
76
74
  // skip empty means empty files won't appear in the coverage reports (json and html)
77
75
  coverageReportSkipEmpty = false,
78
76
  // skip full means file with 100% coverage won't appear in coverage reports (json and html)
79
77
  coverageReportSkipFull = false,
78
+ coverageReportTextLog = true,
79
+ coverageReportJsonFile = process.env.CI ? null : "./.coverage/coverage.json",
80
+ coverageReportHtmlDirectory = process.env.CI ? "./.coverage/" : null,
80
81
 
81
82
  sourcemaps = "inline",
82
83
  plugins = [],
@@ -95,7 +96,7 @@ export const executeTestPlan = async ({
95
96
  if (typeof testPlan !== "object") {
96
97
  throw new Error(`testPlan must be an object, got ${testPlan}`)
97
98
  }
98
- if (coverage) {
99
+ if (coverageEnabled) {
99
100
  if (typeof coverageConfig !== "object") {
100
101
  throw new TypeError(
101
102
  `coverageConfig must be an object, got ${coverageConfig}`,
@@ -156,10 +157,11 @@ export const executeTestPlan = async ({
156
157
  cooldownBetweenExecutions,
157
158
  gcBetweenExecutions,
158
159
 
159
- coverage,
160
+ coverageEnabled,
160
161
  coverageConfig,
161
162
  coverageIncludeMissing,
162
- coverageForceIstanbul,
163
+ coverageMethodForBrowsers,
164
+ coverageMethodForNodeJs,
163
165
  coverageV8ConflictWarning,
164
166
  coverageTempDirectoryRelativeUrl,
165
167
 
@@ -189,7 +191,7 @@ export const executeTestPlan = async ({
189
191
  // keep this one first because it does ensureEmptyDirectory
190
192
  // and in case coverage json file gets written in the same directory
191
193
  // it must be done before
192
- if (coverage && coverageReportHtmlDirectory) {
194
+ if (coverageEnabled && coverageReportHtmlDirectory) {
193
195
  const coverageHtmlDirectoryUrl = resolveDirectoryUrl(
194
196
  coverageReportHtmlDirectory,
195
197
  rootDirectoryUrl,
@@ -216,7 +218,7 @@ export const executeTestPlan = async ({
216
218
  }),
217
219
  )
218
220
  }
219
- if (coverage && coverageReportJsonFile) {
221
+ if (coverageEnabled && coverageReportJsonFile) {
220
222
  const coverageJsonFileUrl = new URL(
221
223
  coverageReportJsonFile,
222
224
  rootDirectoryUrl,
@@ -229,7 +231,7 @@ export const executeTestPlan = async ({
229
231
  }),
230
232
  )
231
233
  }
232
- if (coverage && coverageReportTextLog) {
234
+ if (coverageEnabled && coverageReportTextLog) {
233
235
  promises.push(
234
236
  generateCoverageTextLog(result.planCoverage, {
235
237
  coverageReportSkipEmpty,
@@ -1,227 +0,0 @@
1
- import v8 from "node:v8";
2
- import { u as uneval } from "./uneval.js";
3
- import { performance, PerformanceObserver } from "node:perf_hooks";
4
-
5
- const startObservingPerformances = () => {
6
- const measureEntries = []; // https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html
7
-
8
- const perfObserver = new PerformanceObserver(( // https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html#perf_hooks_class_performanceobserverentrylist
9
- list) => {
10
- const perfMeasureEntries = list.getEntriesByType("measure");
11
- measureEntries.push(...perfMeasureEntries);
12
- });
13
- perfObserver.observe({
14
- entryTypes: ["measure"]
15
- });
16
- return async () => {
17
- // wait for node to call the performance observer
18
- await new Promise(resolve => {
19
- setTimeout(resolve);
20
- });
21
- performance.clearMarks();
22
- perfObserver.disconnect();
23
- return { ...readNodePerformance(),
24
- measures: measuresFromMeasureEntries(measureEntries)
25
- };
26
- };
27
- };
28
-
29
- const readNodePerformance = () => {
30
- const nodePerformance = {
31
- nodeTiming: asPlainObject(performance.nodeTiming),
32
- timeOrigin: performance.timeOrigin,
33
- eventLoopUtilization: performance.eventLoopUtilization()
34
- };
35
- return nodePerformance;
36
- }; // remove getters that cannot be stringified
37
-
38
-
39
- const asPlainObject = objectWithGetters => {
40
- const objectWithoutGetters = {};
41
- Object.keys(objectWithGetters).forEach(key => {
42
- objectWithoutGetters[key] = objectWithGetters[key];
43
- });
44
- return objectWithoutGetters;
45
- };
46
-
47
- const measuresFromMeasureEntries = measureEntries => {
48
- const measures = {}; // Sort to ensure measures order is predictable
49
- // It seems to be already predictable on Node 16+ but
50
- // it's not the case on Node 14.
51
-
52
- measureEntries.sort((a, b) => {
53
- return a.startTime - b.startTime;
54
- });
55
- measureEntries.forEach(( // https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html#perf_hooks_class_performanceentry
56
- perfMeasureEntry) => {
57
- measures[perfMeasureEntry.name] = perfMeasureEntry.duration;
58
- });
59
- return measures;
60
- };
61
-
62
- const ACTIONS_AVAILABLE = {
63
- "execute-using-dynamic-import": async ({
64
- fileUrl,
65
- collectPerformance
66
- }) => {
67
- const getNamespace = async () => {
68
- const namespace = await import(fileUrl);
69
- const namespaceResolved = {};
70
- await Promise.all([...Object.keys(namespace).map(async key => {
71
- const value = await namespace[key];
72
- namespaceResolved[key] = value;
73
- })]);
74
- return namespaceResolved;
75
- };
76
-
77
- if (collectPerformance) {
78
- const getPerformance = startObservingPerformances();
79
- const namespace = await getNamespace();
80
- const performance = await getPerformance();
81
- return {
82
- namespace,
83
- performance
84
- };
85
- }
86
-
87
- const namespace = await getNamespace();
88
- return {
89
- namespace
90
- };
91
- },
92
- "execute-using-require": async ({
93
- fileUrl
94
- }) => {
95
- const {
96
- createRequire
97
- } = await import("module");
98
- const {
99
- fileURLToPath
100
- } = await import("url");
101
- const filePath = fileURLToPath(fileUrl);
102
-
103
- const require = createRequire(fileUrl); // eslint-disable-next-line import/no-dynamic-require
104
-
105
-
106
- const namespace = require(filePath);
107
-
108
- const namespaceResolved = {};
109
- await Promise.all([...Object.keys(namespace).map(async key => {
110
- const value = await namespace[key];
111
- namespaceResolved[key] = value;
112
- })]);
113
- return namespaceResolved;
114
- }
115
- };
116
- const ACTION_REQUEST_EVENT_NAME = "action";
117
- const ACTION_RESPONSE_EVENT_NAME = "action-result";
118
- const ACTION_RESPONSE_STATUS_FAILED = "action-failed";
119
- const ACTION_RESPONSE_STATUS_COMPLETED = "action-completed";
120
-
121
- const sendActionFailed = error => {
122
- if (error.hasOwnProperty("toString")) {
123
- delete error.toString;
124
- }
125
-
126
- sendToParent(ACTION_RESPONSE_EVENT_NAME, // process.send algorithm does not send non enumerable values
127
- // so use @jsenv/uneval
128
- uneval({
129
- status: ACTION_RESPONSE_STATUS_FAILED,
130
- value: error
131
- }, {
132
- ignoreSymbols: true
133
- }));
134
- };
135
-
136
- const sendActionCompleted = value => {
137
- sendToParent(ACTION_RESPONSE_EVENT_NAME, // here we use JSON.stringify because we should not
138
- // have non enumerable value (unlike there is on Error objects)
139
- // otherwise uneval is quite slow to turn a giant object
140
- // into a string (and value can be giant when using coverage)
141
- JSON.stringify({
142
- status: ACTION_RESPONSE_STATUS_COMPLETED,
143
- value
144
- }));
145
- };
146
-
147
- const sendToParent = (type, data) => {
148
- // https://nodejs.org/api/process.html#process_process_connected
149
- // not connected anymore, cannot communicate with parent
150
- if (!process.connected) {
151
- return;
152
- } // this can keep process alive longer than expected
153
- // when source is a long string.
154
- // It means node process may stay alive longer than expected
155
- // the time to send the data to the parent.
156
-
157
-
158
- process.send({
159
- type,
160
- data
161
- });
162
- };
163
-
164
- const onceProcessMessage = (type, callback) => {
165
- const listener = event => {
166
- if (event.type === type) {
167
- // commenting line below keep this process alive
168
- removeListener(); // eslint-disable-next-line no-eval
169
-
170
- callback(eval(`(${event.data})`));
171
- }
172
- };
173
-
174
- const removeListener = () => {
175
- process.removeListener("message", listener);
176
- };
177
-
178
- process.on("message", listener);
179
- return removeListener;
180
- };
181
-
182
- const removeActionRequestListener = onceProcessMessage(ACTION_REQUEST_EVENT_NAME, async ({
183
- actionType,
184
- actionParams
185
- }) => {
186
- const action = ACTIONS_AVAILABLE[actionType];
187
-
188
- if (!action) {
189
- sendActionFailed(new Error(`unknown action ${actionType}`));
190
- return;
191
- }
192
-
193
- let value;
194
- let failed = false;
195
-
196
- try {
197
- value = await action(actionParams);
198
- } catch (e) {
199
- failed = true;
200
- value = e;
201
- }
202
-
203
- if (process.env.NODE_V8_COVERAGE) {
204
- v8.takeCoverage(); // if (actionParams.stopCoverageAfterExecution) {
205
- // v8.stopCoverage()
206
- // }
207
- } // setTimeout(() => {}, 100)
208
-
209
-
210
- if (failed) {
211
- sendActionFailed(value);
212
- } else {
213
- sendActionCompleted(value);
214
- } // removeActionRequestListener()
215
-
216
-
217
- if (actionParams.exitAfterAction) {
218
- removeActionRequestListener(); // for some reason this fixes v8 coverage directory sometimes empty on Ubuntu
219
- // process.exit()
220
- }
221
- }); // remove listener to process.on('message')
222
- // which is sufficient to let child process die
223
- // assuming nothing else keeps it alive
224
- // process.once("SIGTERM", removeActionRequestListener)
225
- // process.once("SIGINT", removeActionRequestListener)
226
-
227
- setTimeout(() => sendToParent("ready"));