@jsenv/core 27.0.3 → 27.2.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.
Files changed (37) 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 +683 -818
  6. package/package.json +9 -8
  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 +98 -91
  35. package/src/test/execute_test_plan.js +19 -13
  36. package/src/test/logs_file_execution.js +90 -13
  37. package/dist/js/controllable_file.mjs +0 -227
@@ -0,0 +1,139 @@
1
+ import { u as uneval } from "./js/uneval.js";
2
+ import { e as executeUsingDynamicImport } from "./js/execute_using_dynamic_import.js";
3
+ import "node:inspector";
4
+ import "node:perf_hooks";
5
+
6
+ const ACTIONS_AVAILABLE = {
7
+ "execute-using-dynamic-import": executeUsingDynamicImport,
8
+ "execute-using-require": async ({
9
+ fileUrl
10
+ }) => {
11
+ const {
12
+ createRequire
13
+ } = await import("node:module");
14
+ const {
15
+ fileURLToPath
16
+ } = await import("node:url");
17
+ const filePath = fileURLToPath(fileUrl);
18
+
19
+ const require = createRequire(fileUrl); // eslint-disable-next-line import/no-dynamic-require
20
+
21
+
22
+ const namespace = require(filePath);
23
+
24
+ const namespaceResolved = {};
25
+ await Promise.all(Object.keys(namespace).map(async key => {
26
+ const value = await namespace[key];
27
+ namespaceResolved[key] = value;
28
+ }));
29
+ return namespaceResolved;
30
+ }
31
+ };
32
+ const ACTION_REQUEST_EVENT_NAME = "action";
33
+ const ACTION_RESPONSE_EVENT_NAME = "action-result";
34
+ const ACTION_RESPONSE_STATUS_FAILED = "action-failed";
35
+ const ACTION_RESPONSE_STATUS_COMPLETED = "action-completed";
36
+
37
+ const sendActionFailed = error => {
38
+ if (error.hasOwnProperty("toString")) {
39
+ delete error.toString;
40
+ }
41
+
42
+ sendToParent(ACTION_RESPONSE_EVENT_NAME, // process.send algorithm does not send non enumerable values
43
+ // so use @jsenv/uneval
44
+ uneval({
45
+ status: ACTION_RESPONSE_STATUS_FAILED,
46
+ value: error
47
+ }, {
48
+ ignoreSymbols: true
49
+ }));
50
+ };
51
+
52
+ const sendActionCompleted = value => {
53
+ sendToParent(ACTION_RESPONSE_EVENT_NAME, // here we use JSON.stringify because we should not
54
+ // have non enumerable value (unlike there is on Error objects)
55
+ // otherwise uneval is quite slow to turn a giant object
56
+ // into a string (and value can be giant when using coverage)
57
+ JSON.stringify({
58
+ status: ACTION_RESPONSE_STATUS_COMPLETED,
59
+ value
60
+ }));
61
+ };
62
+
63
+ const sendToParent = (type, data) => {
64
+ // https://nodejs.org/api/process.html#process_process_connected
65
+ // not connected anymore, cannot communicate with parent
66
+ if (!process.connected) {
67
+ return;
68
+ } // this can keep process alive longer than expected
69
+ // when source is a long string.
70
+ // It means node process may stay alive longer than expected
71
+ // the time to send the data to the parent.
72
+
73
+
74
+ process.send({
75
+ jsenv: true,
76
+ type,
77
+ data
78
+ });
79
+ };
80
+
81
+ const onceParentMessage = (type, callback) => {
82
+ const listener = message => {
83
+ if (message && message.jsenv && message.type === type) {
84
+ removeListener(); // commenting this line keep this process alive
85
+
86
+ callback(message.data);
87
+ }
88
+ };
89
+
90
+ const removeListener = () => {
91
+ process.removeListener("message", listener);
92
+ };
93
+
94
+ process.on("message", listener);
95
+ return removeListener;
96
+ };
97
+
98
+ const removeActionRequestListener = onceParentMessage(ACTION_REQUEST_EVENT_NAME, async ({
99
+ actionType,
100
+ actionParams
101
+ }) => {
102
+ const action = ACTIONS_AVAILABLE[actionType];
103
+
104
+ if (!action) {
105
+ sendActionFailed(new Error(`unknown action ${actionType}`));
106
+ return;
107
+ }
108
+
109
+ let value;
110
+ let failed = false;
111
+
112
+ try {
113
+ value = await action(actionParams);
114
+ } catch (e) {
115
+ failed = true;
116
+ value = e;
117
+ } // setTimeout(() => {}, 100)
118
+
119
+
120
+ if (failed) {
121
+ sendActionFailed(value);
122
+ } else {
123
+ sendActionCompleted(value);
124
+ } // removeActionRequestListener()
125
+
126
+
127
+ if (actionParams.exitAfterAction) {
128
+ removeActionRequestListener(); // for some reason this fixes v8 coverage directory sometimes empty on Ubuntu
129
+ // process.exit()
130
+ }
131
+ }); // remove listener to process.on('message')
132
+ // which is sufficient to let child process die
133
+ // assuming nothing else keeps it alive
134
+ // process.once("SIGTERM", removeActionRequestListener)
135
+ // process.once("SIGINT", removeActionRequestListener)
136
+
137
+ setTimeout(() => {
138
+ sendToParent("ready");
139
+ });
@@ -0,0 +1,103 @@
1
+ import { parentPort } from "node:worker_threads";
2
+ import { u as uneval } from "./js/uneval.js";
3
+ import { e as executeUsingDynamicImport } from "./js/execute_using_dynamic_import.js";
4
+ import "node:inspector";
5
+ import "node:perf_hooks";
6
+
7
+ const ACTIONS_AVAILABLE = {
8
+ "execute-using-dynamic-import": executeUsingDynamicImport
9
+ };
10
+ const ACTION_REQUEST_EVENT_NAME = "action";
11
+ const ACTION_RESPONSE_EVENT_NAME = "action-result";
12
+ const ACTION_RESPONSE_STATUS_FAILED = "action-failed";
13
+ const ACTION_RESPONSE_STATUS_COMPLETED = "action-completed";
14
+
15
+ const sendActionFailed = error => {
16
+ if (error.hasOwnProperty("toString")) {
17
+ delete error.toString;
18
+ }
19
+
20
+ sendToParent(ACTION_RESPONSE_EVENT_NAME, // process.send algorithm does not send non enumerable values
21
+ // so use @jsenv/uneval
22
+ uneval({
23
+ status: ACTION_RESPONSE_STATUS_FAILED,
24
+ value: error
25
+ }, {
26
+ ignoreSymbols: true
27
+ }));
28
+ };
29
+
30
+ const sendActionCompleted = value => {
31
+ sendToParent(ACTION_RESPONSE_EVENT_NAME, // here we use JSON.stringify because we should not
32
+ // have non enumerable value (unlike there is on Error objects)
33
+ // otherwise uneval is quite slow to turn a giant object
34
+ // into a string (and value can be giant when using coverage)
35
+ JSON.stringify({
36
+ status: ACTION_RESPONSE_STATUS_COMPLETED,
37
+ value
38
+ }));
39
+ };
40
+
41
+ const sendToParent = (type, data) => {
42
+ // this can keep process alive longer than expected
43
+ // when source is a long string.
44
+ // It means node process may stay alive longer than expected
45
+ // the time to send the data to the parent.
46
+ parentPort.postMessage({
47
+ jsenv: true,
48
+ type,
49
+ data
50
+ });
51
+ };
52
+
53
+ const onceParentMessage = (type, callback) => {
54
+ const listener = message => {
55
+ if (message && message.jsenv && message.type === type) {
56
+ removeListener(); // commenting this line keep this worker alive
57
+
58
+ callback(message.data);
59
+ }
60
+ };
61
+
62
+ const removeListener = () => {
63
+ parentPort.removeListener("message", listener);
64
+ };
65
+
66
+ parentPort.on("message", listener);
67
+ return removeListener;
68
+ };
69
+
70
+ const removeActionRequestListener = onceParentMessage(ACTION_REQUEST_EVENT_NAME, async ({
71
+ actionType,
72
+ actionParams
73
+ }) => {
74
+ const action = ACTIONS_AVAILABLE[actionType];
75
+
76
+ if (!action) {
77
+ sendActionFailed(new Error(`unknown action ${actionType}`));
78
+ return;
79
+ }
80
+
81
+ let value;
82
+ let failed = false;
83
+
84
+ try {
85
+ value = await action(actionParams);
86
+ } catch (e) {
87
+ failed = true;
88
+ value = e;
89
+ }
90
+
91
+ if (failed) {
92
+ sendActionFailed(value);
93
+ } else {
94
+ sendActionCompleted(value);
95
+ }
96
+
97
+ if (actionParams.exitAfterAction) {
98
+ removeActionRequestListener();
99
+ }
100
+ });
101
+ setTimeout(() => {
102
+ sendToParent("ready");
103
+ });
@@ -0,0 +1,169 @@
1
+ import { Session } from "node:inspector";
2
+ import { performance, PerformanceObserver } from "node:perf_hooks";
3
+
4
+ /*
5
+ * Calling Profiler.startPreciseCoverage DO NOT propagate to
6
+ * subprocesses (new Worker or child_process.fork())
7
+ * So the best solution remains NODE_V8_COVERAGE
8
+ * This profiler strategy remains useful when:
9
+ * - As fallback when NODE_V8_COVERAGE is not configured
10
+ * - If explicitely enabled with coverageMethodForNodeJs: 'Profiler'
11
+ * - Used by jsenv during automated tests about coverage
12
+ * - Anyone prefering this approach over NODE_V8_COVERAGE and assuming
13
+ * it will not fork subprocess or don't care if coverage is missed for this code
14
+ * - https://v8.dev/blog/javascript-code-coverage#for-embedders
15
+ * - https://github.com/nodejs/node/issues/28283
16
+ * - https://vanilla.aslushnikov.com/?Profiler.startPreciseCoverage
17
+ */
18
+ const startJsCoverage = async ({
19
+ callCount = true,
20
+ detailed = true
21
+ } = {}) => {
22
+ const session = new Session();
23
+ session.connect();
24
+
25
+ const postSession = (action, options) => {
26
+ const promise = new Promise((resolve, reject) => {
27
+ session.post(action, options, (error, data) => {
28
+ if (error) {
29
+ reject(error);
30
+ } else {
31
+ resolve(data);
32
+ }
33
+ });
34
+ });
35
+ return promise;
36
+ };
37
+
38
+ await postSession("Profiler.enable");
39
+ await postSession("Profiler.startPreciseCoverage", {
40
+ callCount,
41
+ detailed
42
+ });
43
+
44
+ const takeJsCoverage = async () => {
45
+ const coverage = await postSession("Profiler.takePreciseCoverage");
46
+ return coverage;
47
+ };
48
+
49
+ const stopJsCoverage = async () => {
50
+ const coverage = await takeJsCoverage();
51
+ await postSession("Profiler.stopPreciseCoverage");
52
+ await postSession("Profiler.disable");
53
+ return coverage;
54
+ };
55
+
56
+ return {
57
+ takeJsCoverage,
58
+ stopJsCoverage
59
+ };
60
+ };
61
+
62
+ const startObservingPerformances = () => {
63
+ const measureEntries = []; // https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html
64
+
65
+ const perfObserver = new PerformanceObserver(( // https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html#perf_hooks_class_performanceobserverentrylist
66
+ list) => {
67
+ const perfMeasureEntries = list.getEntriesByType("measure");
68
+ measureEntries.push(...perfMeasureEntries);
69
+ });
70
+ perfObserver.observe({
71
+ entryTypes: ["measure"]
72
+ });
73
+ return async () => {
74
+ // wait for node to call the performance observer
75
+ await new Promise(resolve => {
76
+ setTimeout(resolve);
77
+ });
78
+ performance.clearMarks();
79
+ perfObserver.disconnect();
80
+ return { ...readNodePerformance(),
81
+ measures: measuresFromMeasureEntries(measureEntries)
82
+ };
83
+ };
84
+ };
85
+
86
+ const readNodePerformance = () => {
87
+ const nodePerformance = {
88
+ nodeTiming: asPlainObject(performance.nodeTiming),
89
+ timeOrigin: performance.timeOrigin,
90
+ eventLoopUtilization: performance.eventLoopUtilization()
91
+ };
92
+ return nodePerformance;
93
+ }; // remove getters that cannot be stringified
94
+
95
+
96
+ const asPlainObject = objectWithGetters => {
97
+ const objectWithoutGetters = {};
98
+ Object.keys(objectWithGetters).forEach(key => {
99
+ objectWithoutGetters[key] = objectWithGetters[key];
100
+ });
101
+ return objectWithoutGetters;
102
+ };
103
+
104
+ const measuresFromMeasureEntries = measureEntries => {
105
+ const measures = {}; // Sort to ensure measures order is predictable
106
+ // It seems to be already predictable on Node 16+ but
107
+ // it's not the case on Node 14.
108
+
109
+ measureEntries.sort((a, b) => {
110
+ return a.startTime - b.startTime;
111
+ });
112
+ measureEntries.forEach(( // https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html#perf_hooks_class_performanceentry
113
+ perfMeasureEntry) => {
114
+ measures[perfMeasureEntry.name] = perfMeasureEntry.duration;
115
+ });
116
+ return measures;
117
+ };
118
+
119
+ const executeUsingDynamicImport = async ({
120
+ rootDirectoryUrl,
121
+ fileUrl,
122
+ collectPerformance,
123
+ coverageEnabled,
124
+ coverageConfig,
125
+ coverageMethodForNodeJs
126
+ }) => {
127
+ let result = {};
128
+ const afterImportCallbacks = [];
129
+
130
+ if (coverageEnabled && coverageMethodForNodeJs === "Profiler") {
131
+ const {
132
+ filterV8Coverage
133
+ } = await import("./v8_coverage.js").then(function (n) { return n.v; });
134
+ const {
135
+ stopJsCoverage
136
+ } = await startJsCoverage();
137
+ afterImportCallbacks.push(async () => {
138
+ const coverage = await stopJsCoverage();
139
+ const coverageLight = await filterV8Coverage(coverage, {
140
+ rootDirectoryUrl,
141
+ coverageConfig
142
+ });
143
+ result.coverage = coverageLight;
144
+ });
145
+ }
146
+
147
+ if (collectPerformance) {
148
+ const getPerformance = startObservingPerformances();
149
+ afterImportCallbacks.push(async () => {
150
+ const performance = await getPerformance();
151
+ result.performance = performance;
152
+ });
153
+ }
154
+
155
+ const namespace = await import(fileUrl);
156
+ const namespaceResolved = {};
157
+ await Promise.all(Object.keys(namespace).map(async key => {
158
+ const value = await namespace[key];
159
+ namespaceResolved[key] = value;
160
+ }));
161
+ result.namespace = namespaceResolved;
162
+ await afterImportCallbacks.reduce(async (previous, afterImportCallback) => {
163
+ await previous;
164
+ await afterImportCallback();
165
+ }, Promise.resolve());
166
+ return result;
167
+ };
168
+
169
+ export { executeUsingDynamicImport as e };