@jsenv/core 27.0.2 → 27.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) 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 +703 -827
  6. package/package.json +13 -12
  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/from_playwright.js +13 -8
  13. package/src/execute/runtimes/node/{controllable_file.mjs → controllable_child_process.mjs} +18 -50
  14. package/src/execute/runtimes/node/controllable_worker_thread.mjs +103 -0
  15. package/src/execute/runtimes/node/execute_using_dynamic_import.js +49 -0
  16. package/src/execute/runtimes/node/exit_codes.js +9 -0
  17. package/src/execute/runtimes/node/{node_process.js → node_child_process.js} +56 -50
  18. package/src/execute/runtimes/node/node_worker_thread.js +268 -25
  19. package/src/execute/runtimes/node/profiler_v8_coverage.js +56 -0
  20. package/src/main.js +3 -1
  21. package/src/omega/kitchen.js +19 -6
  22. package/src/omega/server/file_service.js +2 -2
  23. package/src/omega/url_graph/url_graph_load.js +0 -1
  24. package/src/omega/url_graph/url_info_transformations.js +27 -14
  25. package/src/omega/url_graph.js +2 -0
  26. package/src/plugins/bundling/js_module/bundle_js_module.js +2 -5
  27. package/src/plugins/transpilation/as_js_classic/jsenv_plugin_as_js_classic.js +18 -15
  28. package/src/plugins/url_resolution/jsenv_plugin_url_resolution.js +2 -1
  29. package/src/test/coverage/report_to_coverage.js +16 -19
  30. package/src/test/coverage/v8_coverage.js +26 -0
  31. package/src/test/coverage/{v8_coverage_from_directory.js → v8_coverage_node_directory.js} +22 -26
  32. package/src/test/execute_plan.js +98 -91
  33. package/src/test/execute_test_plan.js +19 -13
  34. package/src/test/logs_file_execution.js +90 -13
  35. package/dist/js/controllable_file.mjs +0 -227
@@ -86,6 +86,7 @@ export const createKitchen = ({
86
86
  baseUrl,
87
87
  isOriginalPosition,
88
88
  shouldHandle,
89
+ isEntryPoint = false,
89
90
  isInline = false,
90
91
  injected = false,
91
92
  isRessourceHint = false,
@@ -122,6 +123,7 @@ export const createKitchen = ({
122
123
  baseUrl,
123
124
  isOriginalPosition,
124
125
  shouldHandle,
126
+ isEntryPoint,
125
127
  isInline,
126
128
  injected,
127
129
  isRessourceHint,
@@ -138,6 +140,7 @@ export const createKitchen = ({
138
140
  reference.next = newReference
139
141
  newReference.prev = reference
140
142
  newReference.original = reference.original || reference
143
+ // newReference.isEntryPoint = reference.isEntryPoint
141
144
  }
142
145
  const resolveReference = (reference) => {
143
146
  try {
@@ -166,12 +169,16 @@ export const createKitchen = ({
166
169
  },
167
170
  )
168
171
 
169
- const urlInfo = urlGraph.reuseOrCreateUrlInfo(reference.url)
170
- applyReferenceEffectsOnUrlInfo(reference, urlInfo, kitchenContext)
171
-
172
172
  const referenceUrlObject = new URL(reference.url)
173
173
  reference.searchParams = referenceUrlObject.searchParams
174
174
  reference.generatedUrl = reference.url
175
+ if (reference.searchParams.has("entry_point")) {
176
+ reference.isEntryPoint = true
177
+ }
178
+
179
+ const urlInfo = urlGraph.reuseOrCreateUrlInfo(reference.url)
180
+ applyReferenceEffectsOnUrlInfo(reference, urlInfo, kitchenContext)
181
+
175
182
  // This hook must touch reference.generatedUrl, NOT reference.url
176
183
  // And this is because this hook inject query params used to:
177
184
  // - bypass browser cache (?v)
@@ -286,6 +293,7 @@ export const createKitchen = ({
286
293
  status = 200,
287
294
  headers = {},
288
295
  body,
296
+ isEntryPoint,
289
297
  } = fetchUrlContentReturnValue
290
298
  if (status !== 200) {
291
299
  throw new Error(`unexpected status, ${status}`)
@@ -319,6 +327,9 @@ export const createKitchen = ({
319
327
  if (data) {
320
328
  Object.assign(urlInfo.data, data)
321
329
  }
330
+ if (typeof isEntryPoint === "boolean") {
331
+ urlInfo.isEntryPoint = isEntryPoint
332
+ }
322
333
  if (filename) {
323
334
  urlInfo.filename = filename
324
335
  }
@@ -620,6 +631,7 @@ export const createKitchen = ({
620
631
 
621
632
  const prepareEntryPoint = (params) => {
622
633
  const entryReference = createReference(params)
634
+ entryReference.isEntryPoint = true
623
635
  const entryUrlInfo = resolveReference(entryReference)
624
636
  return [entryReference, entryUrlInfo]
625
637
  }
@@ -718,6 +730,10 @@ const applyReferenceEffectsOnUrlInfo = (reference, urlInfo, context) => {
718
730
  }
719
731
  urlInfo.originalUrl = urlInfo.originalUrl || reference.url
720
732
 
733
+ if (reference.isEntryPoint || isWebWorkerEntryPointReference(reference)) {
734
+ urlInfo.isEntryPoint = true
735
+ }
736
+
721
737
  Object.assign(urlInfo.data, reference.data)
722
738
  Object.assign(urlInfo.timing, reference.timing)
723
739
  if (reference.injected) {
@@ -746,9 +762,6 @@ const applyReferenceEffectsOnUrlInfo = (reference, urlInfo, context) => {
746
762
  : reference.content
747
763
  urlInfo.content = reference.content
748
764
  }
749
- if (isWebWorkerEntryPointReference(reference)) {
750
- urlInfo.data.isWebWorkerEntryPoint = true
751
- }
752
765
  }
753
766
 
754
767
  const adjustUrlSite = (urlInfo, { urlGraph, url, line, column }) => {
@@ -50,10 +50,10 @@ export const createFileService = ({
50
50
  reference = urlGraph.inferReference(request.ressource, parentUrl)
51
51
  }
52
52
  if (!reference) {
53
- const entryPoint = kitchen.prepareEntryPoint({
53
+ const entryPoint = kitchen.injectReference({
54
54
  trace: parentUrl || rootDirectoryUrl,
55
55
  parentUrl: parentUrl || rootDirectoryUrl,
56
- type: "entry_point",
56
+ type: "http_request",
57
57
  specifier: request.ressource,
58
58
  })
59
59
  reference = entryPoint[0]
@@ -56,7 +56,6 @@ export const loadUrlGraph = async ({
56
56
  type,
57
57
  specifier,
58
58
  })
59
- entryUrlInfo.data.isEntryPoint = true
60
59
  cook(entryUrlInfo, { reference: entryReference })
61
60
  return [entryReference, entryUrlInfo]
62
61
  },
@@ -117,7 +117,8 @@ export const createUrlInfoTransformer = ({
117
117
  if (!transformations) {
118
118
  return
119
119
  }
120
- const { type, contentType, content, sourcemap } = transformations
120
+ const { type, contentType, content, sourcemap, sourcemapIsWrong } =
121
+ transformations
121
122
  if (type) {
122
123
  urlInfo.type = type
123
124
  }
@@ -138,6 +139,16 @@ export const createUrlInfoTransformer = ({
138
139
  finalSourcemap,
139
140
  )
140
141
  urlInfo.sourcemap = finalSourcemapNormalized
142
+ // A plugin is allowed to modify url content
143
+ // without returning a sourcemap
144
+ // This is the case for preact and react plugins.
145
+ // They are currently generating wrong source mappings
146
+ // when used.
147
+ // Generating the correct sourcemap in this situation
148
+ // is a nightmare no-one could solve in years so
149
+ // jsenv won't emit a warning and use the following strategy:
150
+ // "no sourcemap is better than wrong sourcemap"
151
+ urlInfo.sourcemapIsWrong = sourcemapIsWrong
141
152
  }
142
153
  }
143
154
 
@@ -162,19 +173,21 @@ export const createUrlInfoTransformer = ({
162
173
  })
163
174
  }
164
175
  sourcemapUrlInfo.content = JSON.stringify(sourcemap, null, " ")
165
- if (sourcemaps === "inline") {
166
- sourcemapReference.generatedSpecifier =
167
- generateSourcemapDataUrl(sourcemap)
168
- }
169
- if (sourcemaps === "file" || sourcemaps === "inline") {
170
- urlInfo.content = SOURCEMAP.writeComment({
171
- contentType: urlInfo.contentType,
172
- content: urlInfo.content,
173
- specifier:
174
- sourcemaps === "file" && sourcemapsRelativeSources
175
- ? urlToRelativeUrl(sourcemapReference.url, urlInfo.url)
176
- : sourcemapReference.generatedSpecifier,
177
- })
176
+ if (!urlInfo.sourcemapIsWrong) {
177
+ if (sourcemaps === "inline") {
178
+ sourcemapReference.generatedSpecifier =
179
+ generateSourcemapDataUrl(sourcemap)
180
+ }
181
+ if (sourcemaps === "file" || sourcemaps === "inline") {
182
+ urlInfo.content = SOURCEMAP.writeComment({
183
+ contentType: urlInfo.contentType,
184
+ content: urlInfo.content,
185
+ specifier:
186
+ sourcemaps === "file" && sourcemapsRelativeSources
187
+ ? urlToRelativeUrl(sourcemapReference.url, urlInfo.url)
188
+ : sourcemapReference.generatedSpecifier,
189
+ })
190
+ }
178
191
  }
179
192
  } else if (urlInfo.sourcemapReference) {
180
193
  // in the end we don't use the sourcemap placeholder
@@ -207,6 +207,7 @@ const createUrlInfo = (url) => {
207
207
  originalUrl: undefined,
208
208
  generatedUrl: null,
209
209
  filename: "",
210
+ isEntryPoint: false,
210
211
  isInline: false,
211
212
  inlineUrlSite: null,
212
213
  shouldHandle: undefined,
@@ -215,6 +216,7 @@ const createUrlInfo = (url) => {
215
216
 
216
217
  sourcemap: null,
217
218
  sourcemapReference: null,
219
+ sourcemapIsWrong: false,
218
220
  timing: {},
219
221
  headers: {},
220
222
  }
@@ -94,7 +94,7 @@ const rollupPluginJsenv = ({
94
94
  let previousNonEntryPointModuleId
95
95
  jsModuleUrlInfos.forEach((jsModuleUrlInfo) => {
96
96
  const id = jsModuleUrlInfo.url
97
- if (jsModuleUrlInfo.data.isEntryPoint) {
97
+ if (jsModuleUrlInfo.isEntryPoint) {
98
98
  emitChunk({
99
99
  id,
100
100
  })
@@ -355,10 +355,7 @@ const willBeInsideJsDirectory = ({
355
355
  // generated by rollup
356
356
  return true
357
357
  }
358
- if (
359
- !jsModuleUrlInfo.data.isEntryPoint &&
360
- !jsModuleUrlInfo.data.isWebWorkerEntryPoint
361
- ) {
358
+ if (!jsModuleUrlInfo.isEntryPoint) {
362
359
  // not an entry point, jsenv will put it inside js/ directory
363
360
  return true
364
361
  }
@@ -101,24 +101,24 @@ const jsenvPluginAsJsClassicConversion = ({
101
101
  if (!originalUrlInfo) {
102
102
  return null
103
103
  }
104
- const isJsEntryPoint =
105
- // in general html files are entry points
106
- // but during build js can be sepcified as an entry point
107
- // (meaning there is no html file where we can inject systemjs)
108
- // in that case we need to inject systemjs in the js file
109
- originalUrlInfo.data.isEntryPoint ||
110
- // In thoose case we need to inject systemjs the worker js file
111
- originalUrlInfo.data.isWebWorkerEntryPoint
112
- // if it's an entry point without dependency (it does not use import)
113
- // then we can use UMD, otherwise we have to use systemjs
114
- // because it is imported by systemjs
115
104
  const jsClassicFormat =
116
- isJsEntryPoint && !originalUrlInfo.data.usesImport ? "umd" : "system"
105
+ // in general html file are entry points, but js can be entry point when:
106
+ // - passed in entryPoints to build
107
+ // - is used by web worker
108
+ // - the reference contains ?entry_point
109
+ // When js is entry point there can be no HTML to inject systemjs
110
+ // and systemjs must be injected into the js file
111
+ originalUrlInfo.isEntryPoint &&
112
+ // if it's an entry point without dependency (it does not use import)
113
+ // then we can use UMD, otherwise we have to use systemjs
114
+ // because it is imported by systemjs
115
+ !originalUrlInfo.data.usesImport
116
+ ? "umd"
117
+ : "system"
117
118
  const { content, sourcemap } = await convertJsModuleToJsClassic({
118
119
  systemJsInjection,
119
120
  systemJsClientFileUrl,
120
121
  urlInfo: originalUrlInfo,
121
- isJsEntryPoint,
122
122
  jsClassicFormat,
123
123
  })
124
124
  urlInfo.data.jsClassicFormat = jsClassicFormat
@@ -160,7 +160,6 @@ const convertJsModuleToJsClassic = async ({
160
160
  systemJsInjection,
161
161
  systemJsClientFileUrl,
162
162
  urlInfo,
163
- isJsEntryPoint,
164
163
  jsClassicFormat,
165
164
  }) => {
166
165
  const { code, map } = await applyBabelPlugins({
@@ -193,7 +192,11 @@ const convertJsModuleToJsClassic = async ({
193
192
  })
194
193
  let sourcemap = urlInfo.sourcemap
195
194
  sourcemap = await composeTwoSourcemaps(sourcemap, map)
196
- if (systemJsInjection && jsClassicFormat === "system" && isJsEntryPoint) {
195
+ if (
196
+ systemJsInjection &&
197
+ jsClassicFormat === "system" &&
198
+ urlInfo.isEntryPoint
199
+ ) {
197
200
  const magicSource = createMagicSource(code)
198
201
  const systemjsCode = readFileSync(systemJsClientFileUrl, { as: "string" })
199
202
  magicSource.prepend(`${systemjsCode}\n\n`)
@@ -9,7 +9,8 @@ export const jsenvPluginUrlResolution = () => {
9
9
  name: "jsenv:url_resolution",
10
10
  appliesDuring: "*",
11
11
  resolveUrl: {
12
- "entry_point": urlResolver,
12
+ "http_request": urlResolver, // during dev
13
+ "entry_point": urlResolver, // during build
13
14
  "link_href": urlResolver,
14
15
  "script_src": urlResolver,
15
16
  "a_href": urlResolver,
@@ -1,10 +1,8 @@
1
- import { readFile } from "@jsenv/filesystem"
1
+ import { readFileSync } from "node:fs"
2
2
  import { Abort } from "@jsenv/abort"
3
3
 
4
- import {
5
- visitNodeV8Directory,
6
- filterV8Coverage,
7
- } from "./v8_coverage_from_directory.js"
4
+ import { filterV8Coverage } from "./v8_coverage.js"
5
+ import { readNodeV8CoverageDirectory } from "./v8_coverage_node_directory.js"
8
6
  import { composeTwoV8Coverages } from "./v8_coverage_composition.js"
9
7
  import { composeTwoFileByFileIstanbulCoverages } from "./istanbul_coverage_composition.js"
10
8
  import { v8CoverageToIstanbul } from "./v8_coverage_to_istanbul.js"
@@ -20,8 +18,7 @@ export const reportToCoverage = async (
20
18
  rootDirectoryUrl,
21
19
  coverageConfig,
22
20
  coverageIncludeMissing,
23
- urlShouldBeCovered,
24
- coverageForceIstanbul,
21
+ coverageMethodForNodeJs,
25
22
  coverageV8ConflictWarning,
26
23
  },
27
24
  ) => {
@@ -49,24 +46,24 @@ export const reportToCoverage = async (
49
46
  // that were suppose to be coverage but were not.
50
47
  if (
51
48
  executionResult.status === "completed" &&
52
- executionResult.runtimeName !== "node" &&
53
- !process.env.NODE_V8_COVERAGE
49
+ executionResult.type === "node" &&
50
+ coverageMethodForNodeJs !== "NODE_V8_COVERAGE"
54
51
  ) {
55
52
  logger.warn(
56
- `No execution.coverageFileUrl from execution named "${executionName}" of ${file}`,
53
+ `No "coverageFileUrl" from execution named "${executionName}" of ${file}`,
57
54
  )
58
55
  }
59
56
  },
60
57
  })
61
58
 
62
- if (!coverageForceIstanbul && process.env.NODE_V8_COVERAGE) {
63
- await visitNodeV8Directory({
59
+ if (coverageMethodForNodeJs === "NODE_V8_COVERAGE") {
60
+ await readNodeV8CoverageDirectory({
64
61
  logger,
65
62
  signal,
66
- NODE_V8_COVERAGE: process.env.NODE_V8_COVERAGE,
67
- onV8Coverage: (nodeV8Coverage) => {
68
- const nodeV8CoverageLight = filterV8Coverage(nodeV8Coverage, {
69
- urlShouldBeCovered,
63
+ onV8Coverage: async (nodeV8Coverage) => {
64
+ const nodeV8CoverageLight = await filterV8Coverage(nodeV8Coverage, {
65
+ rootDirectoryUrl,
66
+ coverageConfig,
70
67
  })
71
68
  v8Coverage = v8Coverage
72
69
  ? composeTwoV8Coverages(v8Coverage, nodeV8CoverageLight)
@@ -164,9 +161,9 @@ const getCoverageFromReport = async ({ signal, report, onMissing }) => {
164
161
  return
165
162
  }
166
163
 
167
- const executionCoverage = await readFile(coverageFileUrl, {
168
- as: "json",
169
- })
164
+ const executionCoverage = JSON.parse(
165
+ String(readFileSync(new URL(coverageFileUrl))),
166
+ )
170
167
  if (isV8Coverage(executionCoverage)) {
171
168
  v8Coverage = v8Coverage
172
169
  ? composeTwoV8Coverages(v8Coverage, executionCoverage)
@@ -0,0 +1,26 @@
1
+ import { URL_META } from "@jsenv/url-meta"
2
+
3
+ export const filterV8Coverage = async (
4
+ v8Coverage,
5
+ { rootDirectoryUrl, coverageConfig },
6
+ ) => {
7
+ const associations = URL_META.resolveAssociations(
8
+ { cover: coverageConfig },
9
+ rootDirectoryUrl,
10
+ )
11
+ const urlShouldBeCovered = (url) => {
12
+ const { cover } = URL_META.applyAssociations({
13
+ url: new URL(url, rootDirectoryUrl).href,
14
+ associations,
15
+ })
16
+ return cover
17
+ }
18
+
19
+ const v8CoverageFiltered = {
20
+ ...v8Coverage,
21
+ result: v8Coverage.result.filter((fileReport) =>
22
+ urlShouldBeCovered(fileReport.url),
23
+ ),
24
+ }
25
+ return v8CoverageFiltered
26
+ }
@@ -1,27 +1,30 @@
1
- import {
2
- assertAndNormalizeDirectoryUrl,
3
- readDirectory,
4
- readFile,
5
- } from "@jsenv/filesystem"
6
- import { resolveUrl } from "@jsenv/urls"
1
+ import { readFileSync, readdirSync } from "node:fs"
2
+ import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem"
7
3
  import { createDetailedMessage } from "@jsenv/log"
8
4
  import { Abort } from "@jsenv/abort"
9
5
 
10
- export const visitNodeV8Directory = async ({
6
+ export const readNodeV8CoverageDirectory = async ({
11
7
  logger,
12
8
  signal,
13
- NODE_V8_COVERAGE,
14
9
  onV8Coverage,
15
10
  maxMsWaitingForNodeToWriteCoverageFile = 2000,
16
11
  }) => {
12
+ const NODE_V8_COVERAGE = process.env.NODE_V8_COVERAGE
17
13
  const operation = Abort.startOperation()
18
14
  operation.addAbortSignal(signal)
19
15
 
16
+ let timeSpentTrying = 0
20
17
  const tryReadDirectory = async () => {
21
- const dirContent = await readDirectory(NODE_V8_COVERAGE)
18
+ const dirContent = readdirSync(NODE_V8_COVERAGE)
22
19
  if (dirContent.length > 0) {
23
20
  return dirContent
24
21
  }
22
+ if (timeSpentTrying < maxMsWaitingForNodeToWriteCoverageFile) {
23
+ await new Promise((resolve) => setTimeout(resolve, 200))
24
+ timeSpentTrying += 200
25
+ logger.debug("retry to read coverage directory")
26
+ return tryReadDirectory()
27
+ }
25
28
  logger.warn(`v8 coverage directory is empty at ${NODE_V8_COVERAGE}`)
26
29
  return dirContent
27
30
  }
@@ -32,17 +35,19 @@ export const visitNodeV8Directory = async ({
32
35
 
33
36
  const coverageDirectoryUrl =
34
37
  assertAndNormalizeDirectoryUrl(NODE_V8_COVERAGE)
38
+
35
39
  await dirContent.reduce(async (previous, dirEntry) => {
36
40
  operation.throwIfAborted()
37
41
  await previous
38
42
 
39
- const dirEntryUrl = resolveUrl(dirEntry, coverageDirectoryUrl)
40
- const tryReadJsonFile = async (timeSpentTrying = 0) => {
41
- const fileContent = await readFile(dirEntryUrl, { as: "string" })
43
+ const dirEntryUrl = new URL(dirEntry, coverageDirectoryUrl)
44
+ const tryReadJsonFile = async () => {
45
+ const fileContent = String(readFileSync(dirEntryUrl))
42
46
  if (fileContent === "") {
43
- if (timeSpentTrying < 400) {
47
+ if (timeSpentTrying < maxMsWaitingForNodeToWriteCoverageFile) {
44
48
  await new Promise((resolve) => setTimeout(resolve, 200))
45
- return tryReadJsonFile(timeSpentTrying + 200)
49
+ timeSpentTrying += 200
50
+ return tryReadJsonFile()
46
51
  }
47
52
  console.warn(`Coverage JSON file is empty at ${dirEntryUrl}`)
48
53
  return null
@@ -54,7 +59,8 @@ export const visitNodeV8Directory = async ({
54
59
  } catch (e) {
55
60
  if (timeSpentTrying < maxMsWaitingForNodeToWriteCoverageFile) {
56
61
  await new Promise((resolve) => setTimeout(resolve, 200))
57
- return tryReadJsonFile(timeSpentTrying + 200)
62
+ timeSpentTrying += 200
63
+ return tryReadJsonFile()
58
64
  }
59
65
  console.warn(
60
66
  createDetailedMessage(`Error while reading coverage file`, {
@@ -68,20 +74,10 @@ export const visitNodeV8Directory = async ({
68
74
 
69
75
  const fileContent = await tryReadJsonFile()
70
76
  if (fileContent) {
71
- onV8Coverage(fileContent)
77
+ await onV8Coverage(fileContent)
72
78
  }
73
79
  }, Promise.resolve())
74
80
  } finally {
75
81
  await operation.end()
76
82
  }
77
83
  }
78
-
79
- export const filterV8Coverage = (v8Coverage, { urlShouldBeCovered }) => {
80
- const v8CoverageFiltered = {
81
- ...v8Coverage,
82
- result: v8Coverage.result.filter((fileReport) =>
83
- urlShouldBeCovered(fileReport.url),
84
- ),
85
- }
86
- return v8CoverageFiltered
87
- }