@jsenv/core 27.0.0-alpha.22 → 27.0.0-alpha.25

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "27.0.0-alpha.22",
3
+ "version": "27.0.0-alpha.25",
4
4
  "description": "Tool to develop, test and build js projects",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -11,8 +11,7 @@
11
11
  "node": ">=16.13.0"
12
12
  },
13
13
  "publishConfig": {
14
- "access": "public",
15
- "registry": "https://registry.npmjs.org"
14
+ "access": "public"
16
15
  },
17
16
  "type": "module",
18
17
  "imports": {},
@@ -111,4 +110,4 @@
111
110
  "redux": "4.1.2",
112
111
  "rollup": "2.70.1"
113
112
  }
114
- }
113
+ }
@@ -349,10 +349,30 @@ ${Object.keys(rawGraph.urlInfos).join("\n")}`,
349
349
  // - injecting "?as_js_classic" for the first time
350
350
  // - injecting "?as_js_classic" because the parentUrl has it
351
351
  if (reference.original) {
352
+ const referenceOriginalUrl = reference.original.url
353
+ let originalBuildUrl
354
+ if (urlIsInsideOf(referenceOriginalUrl, buildDirectoryUrl)) {
355
+ originalBuildUrl = referenceOriginalUrl
356
+ } else {
357
+ originalBuildUrl = Object.keys(rawUrls).find(
358
+ (key) => rawUrls[key] === referenceOriginalUrl,
359
+ )
360
+ }
361
+ let rawUrl
362
+ if (urlIsInsideOf(reference.url, buildDirectoryUrl)) {
363
+ // rawUrl = rawUrls[reference.url] || reference.url
364
+ const originalBuildUrl =
365
+ buildUrlRedirections[referenceOriginalUrl]
366
+ rawUrl = originalBuildUrl
367
+ ? rawUrls[originalBuildUrl]
368
+ : reference.url
369
+ } else {
370
+ rawUrl = reference.url
371
+ }
352
372
  // the url info do not exists yet (it will be created after this "normalize" hook)
353
373
  // And the content will be generated when url is cooked by url graph loader.
354
374
  // Here we just want to reserve an url for that file
355
- const buildUrl = buildUrlsGenerator.generate(reference.url, {
375
+ const buildUrl = buildUrlsGenerator.generate(rawUrl, {
356
376
  urlInfo: {
357
377
  data: {
358
378
  ...reference.data,
@@ -364,15 +384,8 @@ ${Object.keys(rawGraph.urlInfos).join("\n")}`,
364
384
  filename: reference.filename,
365
385
  },
366
386
  })
367
- const originalUrl = reference.original.url
368
- const originalBuildUrl = urlIsInsideOf(
369
- reference.url,
370
- buildDirectoryUrl,
371
- )
372
- ? originalUrl
373
- : Object.keys(rawUrls).find((key) => rawUrls[key] === originalUrl)
374
387
  buildUrlRedirections[originalBuildUrl] = buildUrl
375
- rawUrls[buildUrl] = reference.url
388
+ rawUrls[buildUrl] = rawUrl
376
389
  return buildUrl
377
390
  }
378
391
  if (reference.isInline) {
@@ -811,7 +824,6 @@ ${Object.keys(finalGraph.urlInfos).join("\n")}`,
811
824
  finalGraph,
812
825
  rawUrls,
813
826
  buildUrls,
814
- buildUrlRedirections,
815
827
  })
816
828
  const cleanupActions = []
817
829
  GRAPH.forEach(finalGraph, (urlInfo) => {
@@ -1,21 +1,24 @@
1
+ /*
2
+ * Update <link rel="preload"> and friends after build (once we know everything)
3
+ *
4
+ * - Used to remove ressource hint targeting an url that is no longer used:
5
+ * - Happens because of import assertions transpilation (file is inlined into JS)
6
+ */
7
+
1
8
  import {
2
9
  parseHtmlString,
3
10
  visitHtmlAst,
4
11
  stringifyHtmlAst,
5
12
  getHtmlNodeAttributeByName,
6
- assignHtmlNodeAttributes,
7
13
  removeHtmlNode,
8
14
  } from "@jsenv/utils/html_ast/html_ast.js"
9
15
 
10
16
  import { GRAPH } from "./graph_utils.js"
11
17
 
12
- // update ressource hint that where targeting a file that has changed during build
13
- // (happens for import assertions and file modified by "?as_js_classic")
14
18
  export const resyncRessourceHints = async ({
15
19
  finalGraphKitchen,
16
20
  finalGraph,
17
21
  buildUrls,
18
- buildUrlRedirections,
19
22
  }) => {
20
23
  const ressourceHintActions = []
21
24
  GRAPH.forEach(finalGraph, (urlInfo) => {
@@ -40,24 +43,6 @@ export const resyncRessourceHints = async ({
40
43
  removeHtmlNode(linkNode)
41
44
  return
42
45
  }
43
- const buildUrlRedirected = buildUrlRedirections[buildUrl]
44
- if (buildUrlRedirected) {
45
- const urlInfoRedirected = finalGraph.getUrlInfo(buildUrlRedirected)
46
- hrefAttribute.value = urlInfoRedirected.data.buildUrlSpecifier
47
-
48
- if (
49
- urlInfo.type === "js_module" &&
50
- urlInfoRedirected.type === "js_classic"
51
- ) {
52
- const relAttribute = getHtmlNodeAttributeByName(linkNode, "rel")
53
- if (relAttribute && relAttribute.value === "modulepreload") {
54
- assignHtmlNodeAttributes(linkNode, {
55
- rel: "preload",
56
- as: "script",
57
- })
58
- }
59
- }
60
- }
61
46
  }
62
47
  visitHtmlAst(htmlAst, (node) => {
63
48
  if (node.nodeName !== "link") {
@@ -32,6 +32,7 @@ export const startDevServer = async ({
32
32
  injectedGlobals,
33
33
  nodeEsmResolution,
34
34
  fileSystemMagicResolution,
35
+ transpilation,
35
36
  autoreload = true,
36
37
  explorerGroups = {
37
38
  source: {
@@ -95,6 +96,7 @@ export const startDevServer = async ({
95
96
  injectedGlobals,
96
97
  nodeEsmResolution,
97
98
  fileSystemMagicResolution,
99
+ transpilation,
98
100
  autoreload,
99
101
  }),
100
102
  jsenvPluginExplorer({
@@ -23,6 +23,7 @@ export const execute = async ({
23
23
  collectConsole,
24
24
  collectCoverage,
25
25
  coverageTempDirectoryUrl,
26
+ collectPerformance = false,
26
27
  runtime,
27
28
  runtimeParams,
28
29
 
@@ -33,6 +34,7 @@ export const execute = async ({
33
34
  fileSystemMagicResolution,
34
35
  injectedGlobals,
35
36
  transpilation,
37
+ htmlSupervisor = true,
36
38
 
37
39
  port,
38
40
  protocol,
@@ -75,10 +77,14 @@ export const execute = async ({
75
77
  plugins: [
76
78
  ...plugins,
77
79
  ...getCorePlugins({
80
+ rootDirectoryUrl,
81
+ urlGraph,
78
82
  scenario,
83
+
84
+ htmlSupervisor,
85
+ injectedGlobals,
79
86
  nodeEsmResolution,
80
87
  fileSystemMagicResolution,
81
- injectedGlobals,
82
88
  transpilation,
83
89
  }),
84
90
  ],
@@ -120,6 +126,7 @@ export const execute = async ({
120
126
  collectConsole,
121
127
  collectCoverage,
122
128
  coverageTempDirectoryUrl,
129
+ collectPerformance,
123
130
  runtime,
124
131
  runtimeParams,
125
132
  })
@@ -11,8 +11,7 @@ export const run = async ({
11
11
  collectConsole = false,
12
12
  collectCoverage = false,
13
13
  coverageTempDirectoryUrl,
14
- // measurePerformance,
15
- // collectPerformance = false,
14
+ collectPerformance = false,
16
15
 
17
16
  runtime,
18
17
  runtimeParams,
@@ -117,6 +116,7 @@ export const run = async ({
117
116
  signal: runOperation.signal,
118
117
  logger,
119
118
  ...runtimeParams,
119
+ collectPerformance,
120
120
  keepRunning,
121
121
  stopSignal,
122
122
  onConsole: (log) => onConsoleRef.current(log),
@@ -35,7 +35,7 @@ export const createRuntimeFromPlaywright = ({
35
35
  server,
36
36
 
37
37
  // measurePerformance,
38
- // collectPerformance,
38
+ collectPerformance,
39
39
  collectCoverage = false,
40
40
  coverageForceIstanbul,
41
41
  urlShouldBeCovered,
@@ -167,6 +167,39 @@ export const createRuntimeFromPlaywright = ({
167
167
  return result
168
168
  })
169
169
  }
170
+
171
+ if (collectPerformance) {
172
+ resultTransformer = composeTransformer(
173
+ resultTransformer,
174
+ async (result) => {
175
+ const performance = await page.evaluate(
176
+ /* eslint-disable no-undef */
177
+ /* istanbul ignore next */
178
+ () => {
179
+ const { performance } = window
180
+ if (!performance) {
181
+ return null
182
+ }
183
+ const measures = {}
184
+ const measurePerfEntries = performance.getEntriesByType("measure")
185
+ measurePerfEntries.forEach((measurePerfEntry) => {
186
+ measures[measurePerfEntry.name] = measurePerfEntry.duration
187
+ })
188
+ return {
189
+ timeOrigin: performance.timeOrigin,
190
+ timing: performance.timing.toJSON(),
191
+ navigation: performance.navigation.toJSON(),
192
+ measures,
193
+ }
194
+ /* eslint-enable no-undef */
195
+ },
196
+ )
197
+ result.performance = performance
198
+ return result
199
+ },
200
+ )
201
+ }
202
+
170
203
  const fileClientUrl = new URL(fileRelativeUrl, `${server.origin}/`).href
171
204
 
172
205
  // https://github.com/GoogleChrome/puppeteer/blob/v1.4.0/docs/api.md#event-console
@@ -1,17 +1,33 @@
1
1
  import v8 from "node:v8"
2
2
  import { uneval } from "@jsenv/uneval"
3
+ import { startObservingPerformances } from "./node_execution_performance.js"
3
4
 
4
5
  const ACTIONS_AVAILABLE = {
5
- "execute-using-dynamic-import": async ({ fileUrl }) => {
6
- const namespace = await import(fileUrl)
7
- const namespaceResolved = {}
8
- await Promise.all([
9
- ...Object.keys(namespace).map(async (key) => {
10
- const value = await namespace[key]
11
- namespaceResolved[key] = value
12
- }),
13
- ])
14
- return namespaceResolved
6
+ "execute-using-dynamic-import": async ({ fileUrl, collectPerformance }) => {
7
+ const getNamespace = async () => {
8
+ const namespace = await import(fileUrl)
9
+ const namespaceResolved = {}
10
+ await Promise.all([
11
+ ...Object.keys(namespace).map(async (key) => {
12
+ const value = await namespace[key]
13
+ namespaceResolved[key] = value
14
+ }),
15
+ ])
16
+ return namespaceResolved
17
+ }
18
+ if (collectPerformance) {
19
+ const getPerformance = startObservingPerformances()
20
+ const namespace = await getNamespace()
21
+ const performance = await getPerformance()
22
+ return {
23
+ namespace,
24
+ performance,
25
+ }
26
+ }
27
+ const namespace = await getNamespace()
28
+ return {
29
+ namespace,
30
+ }
15
31
  },
16
32
  "execute-using-require": async ({ fileUrl }) => {
17
33
  const { createRequire } = await import("module")
@@ -0,0 +1,67 @@
1
+ import { PerformanceObserver, performance } from "node:perf_hooks"
2
+
3
+ export const startObservingPerformances = () => {
4
+ const measureEntries = []
5
+ // https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html
6
+ const perfObserver = new PerformanceObserver(
7
+ (
8
+ // https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html#perf_hooks_class_performanceobserverentrylist
9
+ list,
10
+ ) => {
11
+ const perfMeasureEntries = list.getEntriesByType("measure")
12
+ measureEntries.push(...perfMeasureEntries)
13
+ },
14
+ )
15
+ perfObserver.observe({
16
+ entryTypes: ["measure"],
17
+ })
18
+ return async () => {
19
+ // wait for node to call the performance observer
20
+ await new Promise((resolve) => {
21
+ setTimeout(resolve)
22
+ })
23
+ performance.clearMarks()
24
+ perfObserver.disconnect()
25
+ return {
26
+ ...readNodePerformance(),
27
+ measures: measuresFromMeasureEntries(measureEntries),
28
+ }
29
+ }
30
+ }
31
+
32
+ const readNodePerformance = () => {
33
+ const nodePerformance = {
34
+ nodeTiming: asPlainObject(performance.nodeTiming),
35
+ timeOrigin: performance.timeOrigin,
36
+ eventLoopUtilization: performance.eventLoopUtilization(),
37
+ }
38
+ return nodePerformance
39
+ }
40
+
41
+ // remove getters that cannot be stringified
42
+ const asPlainObject = (objectWithGetters) => {
43
+ const objectWithoutGetters = {}
44
+ Object.keys(objectWithGetters).forEach((key) => {
45
+ objectWithoutGetters[key] = objectWithGetters[key]
46
+ })
47
+ return objectWithoutGetters
48
+ }
49
+
50
+ const measuresFromMeasureEntries = (measureEntries) => {
51
+ const measures = {}
52
+ // Sort to ensure measures order is predictable
53
+ // It seems to be already predictable on Node 16+ but
54
+ // it's not the case on Node 14.
55
+ measureEntries.sort((a, b) => {
56
+ return a.startTime - b.startTime
57
+ })
58
+ measureEntries.forEach(
59
+ (
60
+ // https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html#perf_hooks_class_performanceentry
61
+ perfMeasureEntry,
62
+ ) => {
63
+ measures[perfMeasureEntry.name] = perfMeasureEntry.duration
64
+ },
65
+ )
66
+ return measures
67
+ }
@@ -35,10 +35,9 @@ nodeProcess.run = async ({
35
35
  stopSignal,
36
36
  onConsole,
37
37
 
38
- measurePerformance,
39
- collectPerformance,
40
38
  collectCoverage = false,
41
39
  coverageForceIstanbul,
40
+ collectPerformance,
42
41
 
43
42
  debugPort,
44
43
  debugMode,
@@ -191,9 +190,7 @@ nodeProcess.run = async ({
191
190
  actionType: "execute-using-dynamic-import",
192
191
  actionParams: {
193
192
  fileUrl: new URL(fileRelativeUrl, rootDirectoryUrl).href,
194
- measurePerformance,
195
193
  collectPerformance,
196
- collectCoverage,
197
194
  },
198
195
  })
199
196
  const winner = await winnerPromise
@@ -246,7 +243,7 @@ nodeProcess.run = async ({
246
243
  }
247
244
  return {
248
245
  status: "completed",
249
- namespace: value,
246
+ ...value,
250
247
  }
251
248
  }
252
249
 
@@ -451,7 +451,7 @@ export const createKitchen = ({
451
451
  currentUrlInfo !== newUrlInfo &&
452
452
  currentUrlInfo.dependents.size === 0
453
453
  ) {
454
- delete context.urlGraph.urlInfos[currentReference.url]
454
+ context.urlGraph.deleteUrlInfo(currentReference.url)
455
455
  }
456
456
  return [nextReference, newUrlInfo]
457
457
  },
@@ -4,7 +4,16 @@ import { urlToRelativeUrl } from "@jsenv/filesystem"
4
4
  export const createUrlGraph = () => {
5
5
  const urlInfos = {}
6
6
  const getUrlInfo = (url) => urlInfos[url]
7
- const deleteUrlInfo = (url) => delete urlInfos[url]
7
+ const deleteUrlInfo = (url) => {
8
+ const urlInfo = urlInfos[url]
9
+ if (urlInfo) {
10
+ delete urlInfos[url]
11
+ if (urlInfo.sourcemapReference) {
12
+ deleteUrlInfo(urlInfo.sourcemapReference.url)
13
+ }
14
+ }
15
+ }
16
+
8
17
  const reuseOrCreateUrlInfo = (url) => {
9
18
  const existingUrlInfo = urlInfos[url]
10
19
  if (existingUrlInfo) return existingUrlInfo
@@ -15,15 +15,15 @@ import { createRequire } from "node:module"
15
15
  import { readFileSync, urlToFilename } from "@jsenv/filesystem"
16
16
 
17
17
  import { requireBabelPlugin } from "@jsenv/babel-plugins"
18
-
19
18
  import { applyBabelPlugins } from "@jsenv/utils/js_ast/apply_babel_plugins.js"
20
19
  import { injectQueryParams } from "@jsenv/utils/urls/url_utils.js"
21
20
  import { createMagicSource } from "@jsenv/utils/sourcemap/magic_source.js"
22
21
  import { composeTwoSourcemaps } from "@jsenv/utils/sourcemap/sourcemap_composition_v3.js"
22
+
23
+ import { fetchOriginalUrlInfo } from "../fetch_original_url_info.js"
23
24
  import { babelPluginTransformImportMetaUrl } from "./helpers/babel_plugin_transform_import_meta_url.js"
24
25
  import { jsenvPluginScriptTypeModuleAsClassic } from "./jsenv_plugin_script_type_module_as_classic.js"
25
26
  import { jsenvPluginWorkersTypeModuleAsClassic } from "./jsenv_plugin_workers_type_module_as_classic.js"
26
- import { jsenvPluginTopLevelAwait } from "./jsenv_plugin_top_level_await.js"
27
27
 
28
28
  const require = createRequire(import.meta.url)
29
29
 
@@ -40,7 +40,6 @@ export const jsenvPluginAsJsClassic = ({ systemJsInjection }) => {
40
40
  jsenvPluginWorkersTypeModuleAsClassic({
41
41
  generateJsClassicFilename,
42
42
  }),
43
- jsenvPluginTopLevelAwait(),
44
43
  ]
45
44
  }
46
45
 
@@ -72,28 +71,18 @@ const asJsClassic = ({ systemJsInjection, systemJsClientFileUrl }) => {
72
71
  return urlTransformed
73
72
  },
74
73
  fetchUrlContent: async (urlInfo, context) => {
75
- const urlObject = new URL(urlInfo.url)
76
- const { searchParams } = urlObject
77
- if (!searchParams.has("as_js_classic")) {
78
- return null
79
- }
80
- searchParams.delete("as_js_classic")
81
- const originalUrl = urlObject.href
82
- const originalReference = {
83
- ...(context.reference.original || context.reference),
74
+ const originalUrlInfo = await fetchOriginalUrlInfo({
75
+ urlInfo,
76
+ context,
77
+ searchParam: "as_js_classic",
84
78
  // override the expectedType to "js_module"
85
79
  // because when there is ?as_js_classic it means the underlying ressource
86
80
  // is a js_module
87
81
  expectedType: "js_module",
88
- }
89
- originalReference.url = originalUrl
90
- const originalUrlInfo = context.urlGraph.reuseOrCreateUrlInfo(
91
- originalReference.url,
92
- )
93
- await context.fetchUrlContent({
94
- reference: originalReference,
95
- urlInfo: originalUrlInfo,
96
82
  })
83
+ if (!originalUrlInfo) {
84
+ return null
85
+ }
97
86
  const isJsEntryPoint =
98
87
  // in general html files are entry points
99
88
  // but during build js can be sepcified as an entry point
@@ -2,7 +2,8 @@ import {
2
2
  getHtmlNodeAttributeByName,
3
3
  getHtmlNodeTextNode,
4
4
  parseHtmlString,
5
- removeHtmlNodeAttribute,
5
+ removeHtmlNodeAttributeByName,
6
+ assignHtmlNodeAttributes,
6
7
  stringifyHtmlAst,
7
8
  visitHtmlAst,
8
9
  htmlNodePosition,
@@ -30,51 +31,163 @@ export const jsenvPluginScriptTypeModuleAsClassic = ({
30
31
  return null
31
32
  }
32
33
  const htmlAst = parseHtmlString(urlInfo.content)
33
- const actions = []
34
- const jsModuleUrlInfos = []
35
- const visitScriptTypeModule = (node) => {
34
+ const preloadAsScriptNodes = []
35
+ const modulePreloadNodes = []
36
+ const moduleScriptNodes = []
37
+ const classicScriptNodes = []
38
+ const visitLinkNodes = (node) => {
39
+ if (node.nodeName !== "link") {
40
+ return
41
+ }
42
+ const relAttribute = getHtmlNodeAttributeByName(node, "rel")
43
+ const rel = relAttribute ? relAttribute.value : undefined
44
+ if (rel === "modulepreload") {
45
+ modulePreloadNodes.push(node)
46
+ return
47
+ }
48
+ if (rel === "preload") {
49
+ const asAttribute = getHtmlNodeAttributeByName(node, "as")
50
+ const as = asAttribute ? asAttribute.value : undefined
51
+ if (as === "script") {
52
+ preloadAsScriptNodes.push(node)
53
+ }
54
+ return
55
+ }
56
+ }
57
+ const visitScriptNodes = (node) => {
36
58
  if (node.nodeName !== "script") {
37
59
  return
38
60
  }
39
61
  const typeAttribute = getHtmlNodeAttributeByName(node, "type")
40
- if (!typeAttribute || typeAttribute.value !== "module") {
62
+ const type = typeAttribute ? typeAttribute.value : undefined
63
+ if (type === "module") {
64
+ moduleScriptNodes.push(node)
65
+ return
66
+ }
67
+ if (type === undefined || type === "text/javascript") {
68
+ classicScriptNodes.push(node)
41
69
  return
42
70
  }
43
- const srcAttribute = getHtmlNodeAttributeByName(node, "src")
71
+ }
72
+ visitHtmlAst(htmlAst, (node) => {
73
+ visitLinkNodes(node)
74
+ visitScriptNodes(node)
75
+ })
76
+
77
+ const classicScriptUrls = []
78
+ const moduleScriptUrls = []
79
+ classicScriptNodes.forEach((classicScriptNode) => {
80
+ const srcAttribute = getHtmlNodeAttributeByName(
81
+ classicScriptNode,
82
+ "src",
83
+ )
84
+ if (srcAttribute) {
85
+ const url = new URL(srcAttribute.value, urlInfo.url).href
86
+ classicScriptUrls.push(url)
87
+ }
88
+ })
89
+ moduleScriptNodes.forEach((moduleScriptNode) => {
90
+ const srcAttribute = getHtmlNodeAttributeByName(
91
+ moduleScriptNode,
92
+ "src",
93
+ )
94
+ if (srcAttribute) {
95
+ const url = new URL(srcAttribute.value, urlInfo.url).href
96
+ moduleScriptUrls.push(url)
97
+ }
98
+ })
99
+
100
+ const jsModuleUrls = []
101
+ const getReferenceAsJsClassic = async (reference) => {
102
+ const [newReference, newUrlInfo] = context.referenceUtils.update(
103
+ reference,
104
+ {
105
+ expectedType: "js_classic",
106
+ specifier: injectQueryParamsIntoSpecifier(reference.specifier, {
107
+ as_js_classic: "",
108
+ }),
109
+ filename: generateJsClassicFilename(reference.url),
110
+ },
111
+ )
112
+ const jsModuleUrl = newUrlInfo.url
113
+ if (!jsModuleUrls.includes(jsModuleUrl)) {
114
+ jsModuleUrls.push(newUrlInfo.url)
115
+ // during dev it means js modules will be cooked before server sends the HTML
116
+ // it's ok because:
117
+ // - during dev script_type_module are supported (dev use a recent browser)
118
+ // - even if browser is not supported it still works it's jus a bit slower
119
+ // because it needs to decide if systemjs will be injected or not
120
+ await context.cook({
121
+ reference: newReference,
122
+ urlInfo: newUrlInfo,
123
+ })
124
+ }
125
+ return [newReference, newUrlInfo]
126
+ }
127
+ const actions = []
128
+ preloadAsScriptNodes.forEach((preloadAsScriptNode) => {
129
+ const hrefAttribute = getHtmlNodeAttributeByName(
130
+ preloadAsScriptNode,
131
+ "href",
132
+ )
133
+ const href = hrefAttribute.value
134
+ const url = new URL(href, urlInfo.url).href
135
+ const expectedScriptType = moduleScriptUrls.includes(url)
136
+ ? "module"
137
+ : "classic"
138
+ // keep in mind:
139
+ // when the url is not referenced by a <script type="module">
140
+ // we assume we want to preload "classic" but it might not be the case
141
+ // but it's unlikely to happen and people should use "modulepreload" in that case anyway
142
+ if (expectedScriptType === "module") {
143
+ actions.push(async () => {
144
+ const [newReference] = await getReferenceAsJsClassic(
145
+ context.referenceUtils.findByGeneratedSpecifier(href),
146
+ )
147
+ assignHtmlNodeAttributes(preloadAsScriptNode, {
148
+ href: newReference.generatedSpecifier,
149
+ })
150
+ removeHtmlNodeAttributeByName(preloadAsScriptNode, "crossorigin")
151
+ })
152
+ }
153
+ })
154
+ modulePreloadNodes.forEach((modulePreloadNode) => {
155
+ const hrefAttribute = getHtmlNodeAttributeByName(
156
+ modulePreloadNode,
157
+ "href",
158
+ )
159
+ const href = hrefAttribute.value
160
+ actions.push(async () => {
161
+ const [newReference] = await getReferenceAsJsClassic(
162
+ context.referenceUtils.findByGeneratedSpecifier(href),
163
+ )
164
+ assignHtmlNodeAttributes(modulePreloadNode, {
165
+ rel: "preload",
166
+ as: "script",
167
+ href: newReference.generatedSpecifier,
168
+ })
169
+ })
170
+ })
171
+ moduleScriptNodes.forEach((moduleScriptNode) => {
172
+ const srcAttribute = getHtmlNodeAttributeByName(
173
+ moduleScriptNode,
174
+ "src",
175
+ )
44
176
  if (srcAttribute) {
45
177
  actions.push(async () => {
46
178
  const specifier = srcAttribute.value
47
- const reference =
48
- context.referenceUtils.findByGeneratedSpecifier(specifier)
49
- const [newReference, newUrlInfo] = context.referenceUtils.update(
50
- reference,
51
- {
52
- expectedType: "js_classic",
53
- specifier: injectQueryParamsIntoSpecifier(specifier, {
54
- as_js_classic: "",
55
- }),
56
- filename: generateJsClassicFilename(reference.url),
57
- },
179
+ const [newReference] = await getReferenceAsJsClassic(
180
+ context.referenceUtils.findByGeneratedSpecifier(specifier),
58
181
  )
59
- removeHtmlNodeAttribute(node, typeAttribute)
182
+ removeHtmlNodeAttributeByName(moduleScriptNode, "type")
60
183
  srcAttribute.value = newReference.generatedSpecifier
61
- // during dev it means js modules will be cooked before server sends the HTML
62
- // it's ok because:
63
- // - during dev script_type_module are supported (dev use a recent browser)
64
- // - even if browser is not supported it still works it's jus a bit slower
65
- // because it needs to decide if systemjs will be injected or not
66
- await context.cook({
67
- reference: newReference,
68
- urlInfo: newUrlInfo,
69
- })
70
- jsModuleUrlInfos.push(newUrlInfo)
71
184
  })
72
185
  return
73
186
  }
74
- const textNode = getHtmlNodeTextNode(node)
187
+ const textNode = getHtmlNodeTextNode(moduleScriptNode)
75
188
  actions.push(async () => {
76
189
  const { line, column, lineEnd, columnEnd, isOriginal } =
77
- htmlNodePosition.readNodePosition(node, {
190
+ htmlNodePosition.readNodePosition(moduleScriptNode, {
78
191
  preferOriginal: true,
79
192
  })
80
193
  let inlineScriptUrl = generateInlineContentUrl({
@@ -86,7 +199,7 @@ export const jsenvPluginScriptTypeModuleAsClassic = ({
86
199
  columnEnd,
87
200
  })
88
201
  const [inlineReference] = context.referenceUtils.foundInline({
89
- node,
202
+ node: moduleScriptNode,
90
203
  type: "script_src",
91
204
  expectedType: "js_module",
92
205
  // we remove 1 to the line because imagine the following html:
@@ -99,39 +212,26 @@ export const jsenvPluginScriptTypeModuleAsClassic = ({
99
212
  contentType: "application/javascript",
100
213
  content: textNode.value,
101
214
  })
102
- const [newReference, newUrlInfo] = context.referenceUtils.update(
215
+ const [, newUrlInfo] = await getReferenceAsJsClassic(
103
216
  inlineReference,
104
- {
105
- expectedType: "js_classic",
106
- specifier: injectQueryParamsIntoSpecifier(inlineScriptUrl, {
107
- as_js_classic: "",
108
- }),
109
- filename: generateJsClassicFilename(inlineReference.url),
110
- },
111
217
  )
112
- await context.cook({
113
- reference: newReference,
114
- urlInfo: newUrlInfo,
115
- })
116
- removeHtmlNodeAttribute(node, typeAttribute)
117
- setHtmlNodeGeneratedText(node, {
218
+ removeHtmlNodeAttributeByName(moduleScriptNode, "type")
219
+ setHtmlNodeGeneratedText(moduleScriptNode, {
118
220
  generatedText: newUrlInfo.content,
119
221
  generatedBy: "jsenv:script_type_module_as_classic",
120
222
  })
121
- jsModuleUrlInfos.push(newUrlInfo)
122
223
  })
123
- }
124
- visitHtmlAst(htmlAst, (node) => {
125
- visitScriptTypeModule(node)
126
224
  })
225
+
127
226
  if (actions.length === 0) {
128
227
  return null
129
228
  }
130
229
  await Promise.all(actions.map((action) => action()))
131
230
  if (systemJsInjection) {
132
- const needsSystemJs = jsModuleUrlInfos.some(
133
- (jsModuleUrlInfo) =>
134
- jsModuleUrlInfo.data.jsClassicFormat === "system",
231
+ const needsSystemJs = jsModuleUrls.some(
232
+ (jsModuleUrl) =>
233
+ context.urlGraph.getUrlInfo(jsModuleUrl).data.jsClassicFormat ===
234
+ "system",
135
235
  )
136
236
  if (needsSystemJs) {
137
237
  const [systemJsReference] = context.referenceUtils.inject({
@@ -31,9 +31,12 @@ export const getBaseBabelPluginStructure = ({ url, isSupported }) => {
31
31
  requireBabelPlugin("@babel/plugin-proposal-unicode-property-regex")
32
32
  }
33
33
  if (isBabelPluginNeeded("transform-async-to-promises")) {
34
- babelPluginStructure["transform-async-to-promises"] = requireBabelPlugin(
35
- "babel-plugin-transform-async-to-promises",
36
- )
34
+ babelPluginStructure["transform-async-to-promises"] = [
35
+ requireBabelPlugin("babel-plugin-transform-async-to-promises"),
36
+ {
37
+ topLevelAwait: "ignore", // will be handled by "jsenv:top_level_await" plugin
38
+ },
39
+ ]
37
40
  }
38
41
  if (isBabelPluginNeeded("transform-arrow-functions")) {
39
42
  babelPluginStructure["transform-arrow-functions"] = requireBabelPlugin(
@@ -0,0 +1,30 @@
1
+ export const fetchOriginalUrlInfo = async ({
2
+ urlInfo,
3
+ context,
4
+ searchParam,
5
+ expectedType,
6
+ }) => {
7
+ const urlObject = new URL(urlInfo.url)
8
+ const { searchParams } = urlObject
9
+ if (!searchParams.has(searchParam)) {
10
+ return null
11
+ }
12
+ searchParams.delete(searchParam)
13
+ const originalUrl = urlObject.href
14
+ const originalReference = {
15
+ ...(context.reference.original || context.reference),
16
+ expectedType,
17
+ }
18
+ originalReference.url = originalUrl
19
+ const originalUrlInfo = context.urlGraph.reuseOrCreateUrlInfo(
20
+ originalReference.url,
21
+ )
22
+ await context.fetchUrlContent({
23
+ reference: originalReference,
24
+ urlInfo: originalUrlInfo,
25
+ })
26
+ if (originalUrlInfo.dependents.size === 0) {
27
+ context.urlGraph.deleteUrlInfo(originalUrlInfo.url)
28
+ }
29
+ return originalUrlInfo
30
+ }
@@ -5,6 +5,7 @@ import { createMagicSource } from "@jsenv/utils/sourcemap/magic_source.js"
5
5
  import { injectQueryParamsIntoSpecifier } from "@jsenv/utils/urls/url_utils.js"
6
6
  import { JS_QUOTES } from "@jsenv/utils/string/js_quotes.js"
7
7
 
8
+ import { fetchOriginalUrlInfo } from "../fetch_original_url_info.js"
8
9
  import { babelPluginMetadataImportAssertions } from "./helpers/babel_plugin_metadata_import_assertions.js"
9
10
 
10
11
  export const jsenvPluginImportAssertions = () => {
@@ -95,125 +96,106 @@ const jsenvPluginAsModules = () => {
95
96
  const asJsonModule = {
96
97
  name: `jsenv:as_json_module`,
97
98
  appliesDuring: "*",
98
- fetchUrlContent: (urlInfo, context) => {
99
- return fetchOriginalUrl({
99
+ fetchUrlContent: async (urlInfo, context) => {
100
+ const originalUrlInfo = await fetchOriginalUrlInfo({
100
101
  urlInfo,
101
102
  context,
102
103
  searchParam: "as_json_module",
103
- convertToJsModule: (urlInfo) => {
104
- // here we could `export default ${jsonText}`:
105
- // but js engine are optimized to recognize JSON.parse
106
- // and use a faster parsing strategy
107
- return `export default JSON.parse(${JSON.stringify(
108
- urlInfo.content.trim(),
109
- )})`
110
- },
104
+ expectedType: "json",
111
105
  })
106
+ if (!originalUrlInfo) {
107
+ return null
108
+ }
109
+ const jsonText = JSON.stringify(originalUrlInfo.content.trim())
110
+ return {
111
+ type: "js_module",
112
+ contentType: "text/javascript",
113
+ // here we could `export default ${jsonText}`:
114
+ // but js engine are optimized to recognize JSON.parse
115
+ // and use a faster parsing strategy
116
+ content: `export default JSON.parse(${jsonText})`,
117
+ }
112
118
  },
113
119
  }
114
120
 
115
121
  const asCssModule = {
116
122
  name: `jsenv:as_css_module`,
117
123
  appliesDuring: "*",
118
- fetchUrlContent: (urlInfo, context) => {
119
- return fetchOriginalUrl({
124
+ fetchUrlContent: async (urlInfo, context) => {
125
+ const originalUrlInfo = await fetchOriginalUrlInfo({
120
126
  urlInfo,
121
127
  context,
122
128
  searchParam: "as_css_module",
123
- convertToJsModule: (urlInfo) => {
124
- const cssText = JS_QUOTES.escapeSpecialChars(urlInfo.content, {
125
- // If template string is choosen and runtime do not support template literals
126
- // it's ok because "jsenv:new_inline_content" plugin executes after this one
127
- // and convert template strings into raw strings
128
- canUseTemplateString: true,
129
- })
130
- return `import { InlineContent } from ${JSON.stringify(
131
- inlineContentClientFileUrl,
132
- )}
133
-
134
- const inlineContent = new InlineContent(${cssText}, { type: "text/css" })
135
- const stylesheet = new CSSStyleSheet()
136
- stylesheet.replaceSync(inlineContent.text)
137
- export default stylesheet`
138
- },
129
+ expectedType: "css",
130
+ })
131
+ if (!originalUrlInfo) {
132
+ return null
133
+ }
134
+ const cssText = JS_QUOTES.escapeSpecialChars(originalUrlInfo.content, {
135
+ // If template string is choosen and runtime do not support template literals
136
+ // it's ok because "jsenv:new_inline_content" plugin executes after this one
137
+ // and convert template strings into raw strings
138
+ canUseTemplateString: true,
139
139
  })
140
+ return {
141
+ type: "js_module",
142
+ contentType: "text/javascript",
143
+ content: `import { InlineContent } from ${JSON.stringify(
144
+ inlineContentClientFileUrl,
145
+ )}
146
+
147
+ const inlineContent = new InlineContent(${cssText}, { type: "text/css" })
148
+ const stylesheet = new CSSStyleSheet()
149
+ stylesheet.replaceSync(inlineContent.text)
150
+ export default stylesheet`,
151
+ }
140
152
  },
141
153
  }
142
154
 
143
155
  const asTextModule = {
144
156
  name: `jsenv:as_text_module`,
145
157
  appliesDuring: "*",
146
- fetchUrlContent: (urlInfo, context) => {
147
- return fetchOriginalUrl({
158
+ fetchUrlContent: async (urlInfo, context) => {
159
+ const originalUrlInfo = await fetchOriginalUrlInfo({
148
160
  urlInfo,
149
161
  context,
150
162
  searchParam: "as_text_module",
151
- convertToJsModule: (urlInfo) => {
152
- const textPlain = JS_QUOTES.escapeSpecialChars(urlInfo.content, {
153
- // If template string is choosen and runtime do not support template literals
154
- // it's ok because "jsenv:new_inline_content" plugin executes after this one
155
- // and convert template strings into raw strings
156
- canUseTemplateString: true,
157
- })
158
- return `import { InlineContent } from ${JSON.stringify(
159
- inlineContentClientFileUrl,
160
- )}
161
-
162
- const inlineContent = new InlineContent(${textPlain}, { type: "text/plain" })
163
- export default inlineContent.text`
164
- },
163
+ expectedType: "text",
164
+ })
165
+ if (!originalUrlInfo) {
166
+ return null
167
+ }
168
+ const textPlain = JS_QUOTES.escapeSpecialChars(urlInfo.content, {
169
+ // If template string is choosen and runtime do not support template literals
170
+ // it's ok because "jsenv:new_inline_content" plugin executes after this one
171
+ // and convert template strings into raw strings
172
+ canUseTemplateString: true,
165
173
  })
174
+ return {
175
+ type: "js_module",
176
+ contentType: "text/javascript",
177
+ content: `import { InlineContent } from ${JSON.stringify(
178
+ inlineContentClientFileUrl,
179
+ )}
180
+
181
+ const inlineContent = new InlineContent(${textPlain}, { type: "text/plain" })
182
+ export default inlineContent.text`,
183
+ }
166
184
  },
167
185
  }
168
186
 
169
187
  return [asJsonModule, asCssModule, asTextModule]
170
188
  }
171
189
 
172
- const fetchOriginalUrl = async ({
173
- urlInfo,
174
- context,
175
- searchParam,
176
- expectedType,
177
- convertToJsModule,
178
- }) => {
179
- const urlObject = new URL(urlInfo.url)
180
- const { searchParams } = urlObject
181
- if (!searchParams.has(searchParam)) {
182
- return null
183
- }
184
- searchParams.delete(searchParam)
185
- const originalUrl = urlObject.href
186
- const originalReference = {
187
- ...(context.reference.original || context.reference),
188
- expectedType,
189
- }
190
- originalReference.url = originalUrl
191
- const originalUrlInfo = context.urlGraph.reuseOrCreateUrlInfo(
192
- originalReference.url,
193
- )
194
- await context.fetchUrlContent({
195
- reference: originalReference,
196
- urlInfo: originalUrlInfo,
197
- })
198
- return {
199
- type: "js_module",
200
- contentType: "text/javascript",
201
- content: convertToJsModule(originalUrlInfo, context),
202
- }
203
- }
204
-
205
190
  const importAsInfos = {
206
191
  json: {
207
192
  searchParam: "as_json_module",
208
- expectedType: "json",
209
193
  },
210
194
  css: {
211
195
  searchParam: "as_css_module",
212
- expectedType: "css",
213
196
  },
214
197
  text: {
215
198
  searchParam: "as_text_module",
216
- expectedType: "text",
217
199
  },
218
200
  }
219
201
 
@@ -11,6 +11,7 @@ import { jsenvPluginCssParcel } from "./css_parcel/jsenv_plugin_css_parcel.js"
11
11
  import { jsenvPluginImportAssertions } from "./import_assertions/jsenv_plugin_import_assertions.js"
12
12
  import { jsenvPluginAsJsClassic } from "./as_js_classic/jsenv_plugin_as_js_classic.js"
13
13
  import { jsenvPluginBabel } from "./babel/jsenv_plugin_babel.js"
14
+ import { jsenvPluginTopLevelAwait } from "./jsenv_plugin_top_level_await.js"
14
15
 
15
16
  export const jsenvPluginTranspilation = ({
16
17
  importAssertions = true,
@@ -35,6 +36,9 @@ export const jsenvPluginTranspilation = ({
35
36
  ...(jsModuleAsJsClassic
36
37
  ? [jsenvPluginAsJsClassic({ systemJsInjection })]
37
38
  : []),
39
+ // topLevelAwait must come after js_module_as_js_classic because it's related to the module format
40
+ // so we want to wait to know the module format before transforming things related to top level await
41
+ ...(topLevelAwait ? [jsenvPluginTopLevelAwait(topLevelAwait)] : []),
38
42
  ...(css ? [jsenvPluginCssParcel()] : []),
39
43
  ]
40
44
  }