@jsenv/core 28.1.1 → 28.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 (51) hide show
  1. package/dist/js/script_type_module_supervisor.js +8 -13
  2. package/dist/js/supervisor.js +702 -534
  3. package/dist/main.js +13275 -13164
  4. package/package.json +5 -5
  5. package/readme.md +3 -3
  6. package/src/build/build.js +960 -712
  7. package/src/build/inject_global_version_mappings.js +5 -20
  8. package/src/build/start_build_server.js +2 -2
  9. package/src/dev/start_dev_server.js +3 -2
  10. package/src/execute/run.js +1 -1
  11. package/src/execute/runtimes/browsers/from_playwright.js +1 -1
  12. package/src/omega/compat/runtime_compat.js +9 -6
  13. package/src/omega/errors.js +3 -0
  14. package/src/omega/fetched_content_compliance.js +2 -2
  15. package/src/omega/kitchen.js +189 -145
  16. package/src/omega/server/file_service.js +104 -71
  17. package/src/omega/url_graph/url_graph_loader.js +77 -0
  18. package/src/omega/url_graph/url_info_transformations.js +12 -15
  19. package/src/omega/url_graph.js +115 -101
  20. package/src/plugins/autoreload/jsenv_plugin_autoreload_client.js +1 -0
  21. package/src/plugins/autoreload/jsenv_plugin_autoreload_server.js +34 -36
  22. package/src/plugins/autoreload/jsenv_plugin_hmr.js +3 -2
  23. package/src/plugins/bundling/js_module/{bundle_js_module.js → bundle_js_modules.js} +51 -14
  24. package/src/plugins/bundling/jsenv_plugin_bundling.js +2 -2
  25. package/src/plugins/import_meta_hot/jsenv_plugin_import_meta_hot.js +11 -0
  26. package/src/plugins/inline/jsenv_plugin_html_inline_content.js +73 -62
  27. package/src/plugins/node_esm_resolution/jsenv_plugin_node_esm_resolution.js +77 -89
  28. package/src/plugins/plugin_controller.js +26 -22
  29. package/src/plugins/server_events/jsenv_plugin_server_events_client_injection.js +1 -0
  30. package/src/plugins/supervisor/client/script_type_module_supervisor.js +7 -9
  31. package/src/plugins/supervisor/client/supervisor.js +125 -96
  32. package/src/plugins/supervisor/jsenv_plugin_supervisor.js +2 -4
  33. package/src/plugins/toolbar/client/execution/toolbar_execution.js +1 -1
  34. package/src/plugins/transpilation/as_js_classic/async-to-promises.js +16 -0
  35. package/src/plugins/transpilation/as_js_classic/convert_js_module_to_js_classic.js +85 -0
  36. package/src/plugins/transpilation/as_js_classic/jsenv_plugin_as_js_classic.js +48 -190
  37. package/src/plugins/transpilation/as_js_classic/jsenv_plugin_as_js_classic_conversion.js +102 -0
  38. package/src/plugins/transpilation/as_js_classic/jsenv_plugin_as_js_classic_html.js +161 -240
  39. package/src/plugins/transpilation/as_js_classic/jsenv_plugin_as_js_classic_library.js +84 -0
  40. package/src/plugins/transpilation/as_js_classic/jsenv_plugin_as_js_classic_workers.js +19 -12
  41. package/src/plugins/transpilation/babel/jsenv_plugin_babel.js +1 -24
  42. package/src/plugins/transpilation/import_assertions/jsenv_plugin_import_assertions.js +82 -52
  43. package/src/plugins/transpilation/jsenv_plugin_transpilation.js +12 -13
  44. package/src/plugins/url_analysis/html/html_urls.js +91 -34
  45. package/src/plugins/url_analysis/js/js_urls.js +5 -4
  46. package/src/plugins/url_resolution/jsenv_plugin_url_resolution.js +1 -0
  47. package/src/test/execute_plan.js +3 -8
  48. package/src/test/execute_test_plan.js +1 -1
  49. package/src/build/inject_service_worker_urls.js +0 -78
  50. package/src/build/resync_resource_hints.js +0 -112
  51. package/src/omega/url_graph/url_graph_load.js +0 -74
@@ -1,9 +1,10 @@
1
+ import { readFileSync } from "node:fs"
1
2
  import {
2
3
  fetchFileSystem,
3
4
  serveDirectory,
4
5
  composeTwoResponses,
5
6
  } from "@jsenv/server"
6
- import { registerDirectoryLifecycle } from "@jsenv/filesystem"
7
+ import { registerDirectoryLifecycle, bufferToEtag } from "@jsenv/filesystem"
7
8
  import { urlIsInsideOf, moveUrl } from "@jsenv/urls"
8
9
  import { URL_META } from "@jsenv/url-meta"
9
10
 
@@ -42,38 +43,41 @@ export const createFileService = ({
42
43
 
43
44
  const clientFileChangeCallbackList = []
44
45
  const clientFilesPruneCallbackList = []
45
- const clientFileChangeCallback = ({ relativeUrl, event }) => {
46
- const url = new URL(relativeUrl, rootDirectoryUrl).href
47
- clientFileChangeCallbackList.forEach((callback) => {
48
- callback({ url, event })
49
- })
50
- }
51
46
  const clientFilePatterns = {
52
47
  ...clientFiles,
53
48
  ".jsenv/": false,
54
49
  }
55
50
 
56
- if (scenarios.dev) {
57
- const stopWatchingClientFiles = registerDirectoryLifecycle(
58
- rootDirectoryUrl,
59
- {
60
- watchPatterns: clientFilePatterns,
61
- cooldownBetweenFileEvents,
62
- keepProcessAlive: false,
63
- recursive: true,
64
- added: ({ relativeUrl }) => {
65
- clientFileChangeCallback({ event: "added", relativeUrl })
66
- },
67
- updated: ({ relativeUrl }) => {
68
- clientFileChangeCallback({ event: "modified", relativeUrl })
69
- },
70
- removed: ({ relativeUrl }) => {
71
- clientFileChangeCallback({ event: "removed", relativeUrl })
72
- },
73
- },
74
- )
75
- serverStopCallbacks.push(stopWatchingClientFiles)
51
+ const onFileChange = (url) => {
52
+ clientFileChangeCallbackList.forEach((callback) => {
53
+ callback(url)
54
+ })
76
55
  }
56
+ const stopWatchingClientFiles = registerDirectoryLifecycle(rootDirectoryUrl, {
57
+ watchPatterns: clientFilePatterns,
58
+ cooldownBetweenFileEvents,
59
+ keepProcessAlive: false,
60
+ recursive: true,
61
+ added: ({ relativeUrl }) => {
62
+ onFileChange({
63
+ url: new URL(relativeUrl, rootDirectoryUrl).href,
64
+ event: "added",
65
+ })
66
+ },
67
+ updated: ({ relativeUrl }) => {
68
+ onFileChange({
69
+ url: new URL(relativeUrl, rootDirectoryUrl).href,
70
+ event: "modified",
71
+ })
72
+ },
73
+ removed: ({ relativeUrl }) => {
74
+ onFileChange({
75
+ url: new URL(relativeUrl, rootDirectoryUrl).href,
76
+ event: "removed",
77
+ })
78
+ },
79
+ })
80
+ serverStopCallbacks.push(stopWatchingClientFiles)
77
81
 
78
82
  const contextCache = new Map()
79
83
  const getOrCreateContext = (request) => {
@@ -89,17 +93,12 @@ export const createFileService = ({
89
93
  { watch: clientFilePatterns },
90
94
  rootDirectoryUrl,
91
95
  )
92
- const urlGraph = createUrlGraph({
93
- clientFileChangeCallbackList,
94
- clientFilesPruneCallbackList,
95
- onCreateUrlInfo: (urlInfo) => {
96
- const { watch } = URL_META.applyAssociations({
97
- url: urlInfo.url,
98
- associations: watchAssociations,
99
- })
100
- urlInfo.isValid = () => watch
101
- },
102
- includeOriginalUrls: scenarios.dev,
96
+ const urlGraph = createUrlGraph()
97
+ clientFileChangeCallbackList.push(({ url }) => {
98
+ const urlInfo = urlGraph.getUrlInfo(url)
99
+ if (urlInfo) {
100
+ urlGraph.considerModified(urlInfo)
101
+ }
103
102
  })
104
103
  const kitchen = createKitchen({
105
104
  signal,
@@ -133,7 +132,51 @@ export const createFileService = ({
133
132
  sourcemapsSourcesProtocol,
134
133
  sourcemapsSourcesContent,
135
134
  writeGeneratedFiles,
135
+ outDirectoryUrl: scenarios.dev
136
+ ? `${rootDirectoryUrl}.jsenv/${runtimeName}@${runtimeVersion}/`
137
+ : `${rootDirectoryUrl}.jsenv/${
138
+ scenarios.test ? "test" : "build"
139
+ }/${runtimeName}@${runtimeVersion}/`,
136
140
  })
141
+ urlGraph.createUrlInfoCallbackRef.current = (urlInfo) => {
142
+ const { watch } = URL_META.applyAssociations({
143
+ url: urlInfo.url,
144
+ associations: watchAssociations,
145
+ })
146
+ urlInfo.isWatched = watch
147
+ // si une urlInfo dépends de pleins d'autres alors
148
+ // on voudrait check chacune de ces url infos (package.json dans mon cas)
149
+ urlInfo.isValid = () => {
150
+ if (!urlInfo.url.startsWith("file:")) {
151
+ return false
152
+ }
153
+ if (watch && urlInfo.contentEtag === undefined) {
154
+ // we trust the watching mecanism
155
+ // doing urlInfo.contentEtag = undefined
156
+ // when file is modified
157
+ return false
158
+ }
159
+ if (!watch) {
160
+ const fileContentAsBuffer = readFileSync(new URL(urlInfo.url))
161
+ const fileContentEtag = bufferToEtag(fileContentAsBuffer)
162
+ if (fileContentEtag !== urlInfo.originalContentEtag) {
163
+ return false
164
+ }
165
+ }
166
+ for (const implicitUrl of urlInfo.implicitUrls) {
167
+ const implicitUrlInfo = context.urlGraph.getUrlInfo(implicitUrl)
168
+ if (implicitUrlInfo && !implicitUrlInfo.isValid()) {
169
+ return false
170
+ }
171
+ }
172
+ return true
173
+ }
174
+ }
175
+ urlGraph.prunedUrlInfosCallbackRef.current = (urlInfos, firstUrlInfo) => {
176
+ clientFilesPruneCallbackList.forEach((callback) => {
177
+ callback(urlInfos, firstUrlInfo)
178
+ })
179
+ }
137
180
  serverStopCallbacks.push(() => {
138
181
  kitchen.pluginController.callHooks("destroy", kitchen.kitchenContext)
139
182
  })
@@ -192,8 +235,7 @@ export const createFileService = ({
192
235
  headers: request.headers,
193
236
  })
194
237
  }
195
- const { runtimeName, runtimeVersion, urlGraph, kitchen } =
196
- getOrCreateContext(request)
238
+ const { urlGraph, kitchen } = getOrCreateContext(request)
197
239
  const responseFromPlugin =
198
240
  await kitchen.pluginController.callAsyncHooksUntil(
199
241
  "serve",
@@ -218,25 +260,21 @@ export const createFileService = ({
218
260
  reference = entryPoint[0]
219
261
  }
220
262
  const urlInfo = urlGraph.reuseOrCreateUrlInfo(reference.url)
221
-
222
263
  const ifNoneMatch = request.headers["if-none-match"]
223
- if (
224
- ifNoneMatch &&
225
- urlInfo.contentEtag === ifNoneMatch &&
226
- // urlInfo.isValid
227
- // - is false by default because there must be some logic capable
228
- // to invalidate the url (otherwise server would return 304 forever)
229
- // - is set to a function returning true if the file is watched
230
- // in start_dev_server.js
231
- // - is set to a custom function by cjs_to_esm in compiled_file_cache.js
232
- urlInfo.isValid()
233
- ) {
234
- return {
235
- status: 304,
236
- headers: {
237
- "cache-control": `private,max-age=0,must-revalidate`,
238
- ...urlInfo.headers,
239
- },
264
+ const urlInfoTargetedByCache = urlGraph.getParentIfInline(urlInfo)
265
+
266
+ if (ifNoneMatch) {
267
+ if (
268
+ urlInfoTargetedByCache.contentEtag === ifNoneMatch &&
269
+ urlInfoTargetedByCache.isValid()
270
+ ) {
271
+ return {
272
+ status: 304,
273
+ headers: {
274
+ "cache-control": `private,max-age=0,must-revalidate`,
275
+ ...urlInfo.headers,
276
+ },
277
+ }
240
278
  }
241
279
  }
242
280
  try {
@@ -253,17 +291,11 @@ export const createFileService = ({
253
291
  urlInfo.originalContent = null
254
292
  urlInfo.type = null
255
293
  urlInfo.subtype = null
256
- urlInfo.dependsOnPackageJson = false
257
294
  urlInfo.timing = {}
258
295
  }
259
296
  await kitchen.cook(urlInfo, {
260
297
  request,
261
298
  reference,
262
- outDirectoryUrl: scenarios.dev
263
- ? `${rootDirectoryUrl}.jsenv/${runtimeName}@${runtimeVersion}/`
264
- : `${rootDirectoryUrl}.jsenv/${
265
- scenarios.test ? "test" : "build"
266
- }/${runtimeName}@${runtimeVersion}/`,
267
299
  })
268
300
  let { response } = urlInfo
269
301
  if (response) {
@@ -275,7 +307,7 @@ export const createFileService = ({
275
307
  headers: {
276
308
  "content-length": Buffer.byteLength(urlInfo.content),
277
309
  "cache-control": `private,max-age=0,must-revalidate`,
278
- "eTag": urlInfo.contentEtag,
310
+ "eTag": urlInfoTargetedByCache.contentEtag,
279
311
  ...urlInfo.headers,
280
312
  "content-type": urlInfo.contentType,
281
313
  },
@@ -293,13 +325,14 @@ export const createFileService = ({
293
325
  return response
294
326
  } catch (e) {
295
327
  urlInfo.error = e
296
- const code = e.code
328
+ const originalError = e ? e.cause || e : e
329
+ const code = originalError.code
297
330
  if (code === "PARSE_ERROR") {
298
331
  return {
299
332
  url: reference.url,
300
333
  status: 200, // let the browser re-throw the syntax error
301
- statusText: e.reason,
302
- statusMessage: e.message,
334
+ statusText: originalError.reason,
335
+ statusMessage: originalError.message,
303
336
  headers: {
304
337
  "content-type": urlInfo.contentType,
305
338
  "content-length": Buffer.byteLength(urlInfo.content),
@@ -321,15 +354,15 @@ export const createFileService = ({
321
354
  return {
322
355
  url: reference.url,
323
356
  status: 403,
324
- statusText: e.reason,
357
+ statusText: originalError.reason,
325
358
  }
326
359
  }
327
360
  if (code === "NOT_FOUND") {
328
361
  return {
329
362
  url: reference.url,
330
363
  status: 404,
331
- statusText: e.reason,
332
- statusMessage: e.message,
364
+ statusText: originalError.reason,
365
+ statusMessage: originalError.message,
333
366
  }
334
367
  }
335
368
  return {
@@ -0,0 +1,77 @@
1
+ export const createUrlGraphLoader = (context) => {
2
+ const promises = []
3
+ const promiseMap = new Map()
4
+ const load = (
5
+ urlInfo,
6
+ dishContext,
7
+ { ignoreRessourceHint = true, ignoreDynamicImport = false } = {},
8
+ ) => {
9
+ const promiseFromData = promiseMap.get(urlInfo)
10
+ if (promiseFromData) return promiseFromData
11
+ const promise = (async () => {
12
+ await context.cook(urlInfo, {
13
+ cookDuringCook: load,
14
+ ...dishContext,
15
+ })
16
+ loadReferencedUrlInfos(urlInfo, {
17
+ ignoreRessourceHint,
18
+ ignoreDynamicImport,
19
+ })
20
+ })()
21
+ promises.push(promise)
22
+ promiseMap.set(urlInfo, promise)
23
+ return promise
24
+ }
25
+
26
+ const loadReferencedUrlInfos = (
27
+ urlInfo,
28
+ { ignoreRessourceHint, ignoreDynamicImport },
29
+ ) => {
30
+ const { references } = urlInfo
31
+ references.forEach((reference) => {
32
+ // we don't cook resource hints
33
+ // because they might refer to resource that will be modified during build
34
+ // It also means something else have to reference that url in order to cook it
35
+ // so that the preload is deleted by "resync_resource_hints.js" otherwise
36
+ if (ignoreRessourceHint && reference.isResourceHint) {
37
+ return
38
+ }
39
+ if (ignoreDynamicImport && reference.subtype === "import_dynamic") {
40
+ return
41
+ }
42
+ // we use reference.generatedUrl to mimic what a browser would do:
43
+ // do a fetch to the specifier as found in the file
44
+ const referencedUrlInfo = context.urlGraph.reuseOrCreateUrlInfo(
45
+ reference.generatedUrl,
46
+ )
47
+ load(referencedUrlInfo, {
48
+ reference,
49
+ ignoreRessourceHint,
50
+ ignoreDynamicImport,
51
+ })
52
+ })
53
+ }
54
+
55
+ const getAllLoadDonePromise = async (operation) => {
56
+ const waitAll = async () => {
57
+ if (operation) {
58
+ operation.throwIfAborted()
59
+ }
60
+ if (promises.length === 0) {
61
+ return
62
+ }
63
+ const promisesToWait = promises.slice()
64
+ promises.length = 0
65
+ await Promise.all(promisesToWait)
66
+ await waitAll()
67
+ }
68
+ await waitAll()
69
+ promiseMap.clear()
70
+ }
71
+
72
+ return {
73
+ load,
74
+ loadReferencedUrlInfos,
75
+ getAllLoadDonePromise,
76
+ }
77
+ }
@@ -15,23 +15,14 @@ export const createUrlInfoTransformer = ({
15
15
  sourcemapsSourcesContent,
16
16
  sourcemapsRelativeSources,
17
17
  urlGraph,
18
- clientRuntimeCompat,
19
18
  injectSourcemapPlaceholder,
20
19
  foundSourcemap,
21
20
  }) => {
22
- const runtimeNames = Object.keys(clientRuntimeCompat)
23
- const chromeAsSingleRuntime =
24
- runtimeNames.length === 1 && runtimeNames[0] === "chrome"
25
21
  if (sourcemapsSourcesProtocol === undefined) {
26
22
  sourcemapsSourcesProtocol = "file:///"
27
23
  }
28
24
  if (sourcemapsSourcesContent === undefined) {
29
- if (chromeAsSingleRuntime && sourcemapsSourcesProtocol === "file:///") {
30
- // chrome is able to fetch source when referenced with "file:"
31
- sourcemapsSourcesContent = false
32
- } else {
33
- sourcemapsSourcesContent = true
34
- }
25
+ sourcemapsSourcesContent = true
35
26
  }
36
27
 
37
28
  const sourcemapsEnabled =
@@ -75,6 +66,9 @@ export const createUrlInfoTransformer = ({
75
66
  }
76
67
 
77
68
  const initTransformations = async (urlInfo, context) => {
69
+ urlInfo.originalContentEtag =
70
+ urlInfo.originalContentEtag ||
71
+ bufferToEtag(Buffer.from(urlInfo.originalContent))
78
72
  if (!sourcemapsEnabled) {
79
73
  return
80
74
  }
@@ -133,7 +127,7 @@ export const createUrlInfoTransformer = ({
133
127
  }
134
128
  }
135
129
 
136
- const applyIntermediateTransformations = async (urlInfo, transformations) => {
130
+ const applyIntermediateTransformations = (urlInfo, transformations) => {
137
131
  if (!transformations) {
138
132
  return
139
133
  }
@@ -150,7 +144,7 @@ export const createUrlInfoTransformer = ({
150
144
  }
151
145
  if (sourcemapsEnabled && sourcemap) {
152
146
  const sourcemapNormalized = normalizeSourcemap(urlInfo, sourcemap)
153
- const finalSourcemap = await composeTwoSourcemaps(
147
+ const finalSourcemap = composeTwoSourcemaps(
154
148
  urlInfo.sourcemap,
155
149
  sourcemapNormalized,
156
150
  )
@@ -172,9 +166,9 @@ export const createUrlInfoTransformer = ({
172
166
  }
173
167
  }
174
168
 
175
- const applyFinalTransformations = async (urlInfo, transformations) => {
169
+ const applyFinalTransformations = (urlInfo, transformations) => {
176
170
  if (transformations) {
177
- await applyIntermediateTransformations(urlInfo, transformations)
171
+ applyIntermediateTransformations(urlInfo, transformations)
178
172
  }
179
173
  if (
180
174
  sourcemapsEnabled &&
@@ -227,7 +221,10 @@ export const createUrlInfoTransformer = ({
227
221
  // in the end we don't use the sourcemap placeholder
228
222
  urlGraph.deleteUrlInfo(urlInfo.sourcemapReference.url)
229
223
  }
230
- urlInfo.contentEtag = bufferToEtag(Buffer.from(urlInfo.content))
224
+ urlInfo.contentEtag =
225
+ urlInfo.content === urlInfo.originalContent
226
+ ? urlInfo.originalContentEtag
227
+ : bufferToEtag(Buffer.from(urlInfo.content))
231
228
  }
232
229
 
233
230
  return {