@jsenv/core 24.0.2 → 24.1.0-alpha.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.
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "24.0.2",
3
+ "version": "24.1.0-alpha.0",
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": ">=14.9.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
  "exports": {
@@ -127,8 +127,7 @@ const computeCompileReport = async ({
127
127
  logger.warn(`WARNING: meta.sources is empty for ${compiledFileUrl}`)
128
128
  }
129
129
 
130
- const metaIsValid = cacheValidity.meta.isValid
131
-
130
+ const metaIsValid = cacheValidity.meta ? cacheValidity.meta.isValid : false
132
131
  const [compileTiming, compileResult] = await timeFunction("compile", () =>
133
132
  callCompile({
134
133
  logger,
@@ -20,6 +20,11 @@ export const validateCache = async ({
20
20
  }) => {
21
21
  const validity = { isValid: true }
22
22
 
23
+ // disable cahce for html files so that we always parse the importmap file
24
+ if (compiledFileUrl.endsWith(".html")) {
25
+ return { isValid: false }
26
+ }
27
+
23
28
  const metaJsonFileUrl = `${compiledFileUrl}__asset__meta.json`
24
29
  const metaValidity = await validateMetaFile(metaJsonFileUrl)
25
30
  mergeValidity(validity, "meta", metaValidity)
@@ -5,6 +5,7 @@ import {
5
5
  bufferToEtag,
6
6
  urlIsInsideOf,
7
7
  } from "@jsenv/filesystem"
8
+ import { normalizeImportMap, resolveImport } from "@jsenv/importmap"
8
9
  import { convertFileSystemErrorToResponseProperties } from "@jsenv/server/src/internal/convertFileSystemErrorToResponseProperties.js"
9
10
 
10
11
  import { getOrGenerateCompiledFile } from "./compile-directory/getOrGenerateCompiledFile.js"
@@ -19,6 +20,7 @@ export const compileFile = async ({
19
20
  projectFileRequestedCallback = () => {},
20
21
  request,
21
22
  pushResponse,
23
+ importmapInfos,
22
24
  compile,
23
25
  compileCacheStrategy,
24
26
  compileCacheSourcesValidation,
@@ -106,30 +108,34 @@ export const compileFile = async ({
106
108
  )
107
109
  })
108
110
 
109
- if (
110
- request.http2 &&
111
+ if (request.http2) {
111
112
  // js resolution is special, we cannot just do resolveUrl of the import specifier
112
113
  // because there can be importmap (bare specifier, import without extension, custom remapping, ...)
113
114
  // And we would push 404 to the browser
114
115
  // Until we implement import map resolution pushing ressource for js imports is disabled
115
- compileResult.contentType !== "application/javascript"
116
- ) {
117
- compileResult.dependencies.forEach((dependency) => {
118
- const requestUrl = resolveUrl(request.ressource, request.origin)
119
- const dependencyUrl = resolveUrl(dependency, requestUrl)
120
- if (!urlIsInsideOf(dependencyUrl, request.origin)) {
121
- // ignore external urls
122
- return
123
- }
124
- if (dependencyUrl.startsWith("data:")) {
125
- return
126
- }
127
- const dependencyRelativeUrl = urlToRelativeUrl(
128
- dependencyUrl,
129
- request.origin,
130
- )
131
- pushResponse({ path: `/${dependencyRelativeUrl}` })
116
+ const dependencyResolver = getDependencyResolver({
117
+ compileResult,
118
+ importmapInfos,
119
+ request,
120
+ projectDirectoryUrl,
132
121
  })
122
+ if (dependencyResolver) {
123
+ compileResult.dependencies.forEach((dependency) => {
124
+ const dependencyUrl = dependencyResolver.resolve(dependency)
125
+ if (!urlIsInsideOf(dependencyUrl, request.origin)) {
126
+ // ignore external urls
127
+ return
128
+ }
129
+ if (dependencyUrl.startsWith("data:")) {
130
+ return
131
+ }
132
+ const dependencyRelativeUrl = urlToRelativeUrl(
133
+ dependencyUrl,
134
+ request.origin,
135
+ )
136
+ pushResponse({ path: `/${dependencyRelativeUrl}` })
137
+ })
138
+ }
133
139
  }
134
140
 
135
141
  // when a compiled version of the source file was just created or updated
@@ -229,3 +235,62 @@ export const compileFile = async ({
229
235
  return convertFileSystemErrorToResponseProperties(error)
230
236
  }
231
237
  }
238
+
239
+ const getDependencyResolver = ({
240
+ compileResult,
241
+ importmapInfos,
242
+ request,
243
+ projectDirectoryUrl,
244
+ }) => {
245
+ const importmapKeys = Object.keys(importmapInfos)
246
+ const requestUrl = resolveUrl(request.ressource, request.origin)
247
+
248
+ if (
249
+ compileResult.contentType !== "application/javascript" ||
250
+ importmapKeys.length === 0
251
+ ) {
252
+ return {
253
+ type: "url_resolution",
254
+ resolve: (dependency) => {
255
+ const dependencyUrl = resolveUrl(dependency, requestUrl)
256
+ return dependencyUrl
257
+ },
258
+ }
259
+ }
260
+
261
+ const firstImportmapInfo = importmapInfos[importmapKeys[0]]
262
+ if (!firstImportmapInfo.text) {
263
+ return null
264
+ }
265
+
266
+ if (
267
+ // we are aware only of 1 importmap
268
+ importmapKeys.length === 1 ||
269
+ // all importmaps are the same
270
+ importmapKeys.slice(1).every(({ url }) => url === firstImportmapInfo.url)
271
+ ) {
272
+ const importMapBaseUrl = resolveUrl(
273
+ urlToRelativeUrl(firstImportmapInfo.url, projectDirectoryUrl),
274
+ `${request.origin}/`,
275
+ )
276
+ const importMap = normalizeImportMap(
277
+ JSON.parse(firstImportmapInfo.text),
278
+ importMapBaseUrl,
279
+ )
280
+ return {
281
+ type: "importmap_resolution",
282
+ resolve: (dependency) => {
283
+ const dependencyUrl = resolveImport({
284
+ specifier: dependency,
285
+ importer: requestUrl,
286
+ importMap,
287
+ })
288
+ return dependencyUrl
289
+ },
290
+ }
291
+ }
292
+
293
+ // there is more than 2 importmaps, we cannot know which one to pick
294
+ // (not supposed ot happen because during dev you usually use a single importmap)
295
+ return null
296
+ }
@@ -80,6 +80,8 @@ export const createCompiledFileService = ({
80
80
  projectDirectoryUrl,
81
81
  )
82
82
 
83
+ const importmapInfos = {}
84
+
83
85
  return (request, { pushResponse, redirectRequest }) => {
84
86
  const { origin, ressource } = request
85
87
  // we use "ressourceToPathname" to remove eventual query param from the url
@@ -167,6 +169,7 @@ export const createCompiledFileService = ({
167
169
  projectFileRequestedCallback,
168
170
  request,
169
171
  pushResponse,
172
+ importmapInfos,
170
173
  compile: ({ code }) => {
171
174
  return compiler({
172
175
  logger,
@@ -195,6 +198,9 @@ export const createCompiledFileService = ({
195
198
  sourcemapMethod,
196
199
  sourcemapExcludeSources,
197
200
  jsenvToolbarInjection,
201
+ onHtmlImportmapInfo: ({ htmlUrl, importmapInfo }) => {
202
+ importmapInfos[htmlUrl] = importmapInfo
203
+ },
198
204
  })
199
205
  },
200
206
  })
@@ -50,6 +50,7 @@ export const compileHtml = async ({
50
50
 
51
51
  jsenvScriptInjection = true,
52
52
  jsenvToolbarInjection,
53
+ onHtmlImportmapInfo,
53
54
  }) => {
54
55
  const jsenvBrowserBuildUrlRelativeToProject = urlToRelativeUrl(
55
56
  jsenvBrowserSystemFileInfo.jsenvBuildUrl,
@@ -86,55 +87,127 @@ export const compileHtml = async ({
86
87
  ],
87
88
  })
88
89
 
90
+ let sources = []
91
+ let sourcesContent = []
89
92
  const { scripts } = parseHtmlAstRessources(htmlAst)
90
- const htmlDependencies = collectHtmlDependenciesFromAst(htmlAst)
91
-
92
- let hasImportmap = false
93
- const inlineScriptsContentMap = {}
94
- const importmapsToInline = []
93
+ let importmapInfo = null
95
94
  scripts.forEach((script) => {
96
95
  const typeAttribute = getHtmlNodeAttributeByName(script, "type")
97
- const srcAttribute = getHtmlNodeAttributeByName(script, "src")
98
-
99
- // importmap
100
96
  if (typeAttribute && typeAttribute.value === "importmap") {
101
- hasImportmap = true
102
-
103
- if (srcAttribute) {
104
- if (moduleOutFormat === "systemjs") {
105
- typeAttribute.value = "jsenv-importmap"
106
- return // no need to inline
97
+ if (importmapInfo) {
98
+ console.error("HTML file must contain max 1 importmap")
99
+ } else {
100
+ const srcAttribute = getHtmlNodeAttributeByName(script, "src")
101
+ const src = srcAttribute ? srcAttribute.value : ""
102
+ if (src) {
103
+ importmapInfo = {
104
+ script,
105
+ url: resolveUrl(src, url),
106
+ loadAsText: async () => {
107
+ const importMapResponse = await fetchUrl(importmapInfo.url)
108
+ if (importMapResponse.status !== 200) {
109
+ logger.warn(
110
+ createDetailedMessage(
111
+ importMapResponse.status === 404
112
+ ? `importmap script file cannot be found.`
113
+ : `importmap script file unexpected response status (${importMapResponse.status}).`,
114
+ {
115
+ "importmap url": importmapInfo.url,
116
+ "html url": url,
117
+ },
118
+ ),
119
+ )
120
+ return "{}"
121
+ }
122
+ const importmapAsText = await importMapResponse.text()
123
+ sources.push(importmapInfo.url)
124
+ sourcesContent.push(importmapAsText)
125
+
126
+ const importMapMoved = moveImportMap(
127
+ JSON.parse(importmapAsText),
128
+ importmapInfo.url,
129
+ url,
130
+ )
131
+ const compiledImportmapAsText = JSON.stringify(
132
+ importMapMoved,
133
+ null,
134
+ " ",
135
+ )
136
+ return compiledImportmapAsText
137
+ },
138
+ }
139
+ } else {
140
+ importmapInfo = {
141
+ script,
142
+ url: compiledUrl,
143
+ loadAsText: () => getHtmlNodeTextNode(script).value,
144
+ }
107
145
  }
108
-
109
- // we force inline because browsers supporting importmap supports only when they are inline
110
- importmapsToInline.push({
111
- script,
112
- src: srcAttribute.value,
113
- })
114
- return
115
- }
116
-
117
- const defaultImportMap = getDefaultImportMap({
118
- importMapFileUrl: compiledUrl,
119
- projectDirectoryUrl,
120
- compileDirectoryRelativeUrl: `${outDirectoryRelativeUrl}${compileId}/`,
121
- })
122
- const inlineImportMap = JSON.parse(getHtmlNodeTextNode(script).value)
123
- const mappings = composeTwoImportMaps(defaultImportMap, inlineImportMap)
124
- if (moduleOutFormat === "systemjs") {
125
- typeAttribute.value = "jsenv-importmap"
126
146
  }
127
- setHtmlNodeText(script, JSON.stringify(mappings, null, " "))
128
- return
129
147
  }
148
+ })
149
+ if (importmapInfo) {
150
+ const htmlImportMap = JSON.parse(await importmapInfo.loadAsText())
151
+ const importMapFromJsenv = getDefaultImportMap({
152
+ importMapFileUrl: compiledUrl,
153
+ projectDirectoryUrl,
154
+ compileDirectoryRelativeUrl: `${outDirectoryRelativeUrl}${compileId}/`,
155
+ })
156
+ const mappings = composeTwoImportMaps(importMapFromJsenv, htmlImportMap)
157
+ const importmapAsText = JSON.stringify(mappings, null, " ")
158
+ replaceHtmlNode(
159
+ importmapInfo.script,
160
+ `<script type="${
161
+ moduleOutFormat === "systemjs" ? "jsenv-importmap" : "importmap"
162
+ }">${importmapAsText}</script>`,
163
+ {
164
+ attributesToIgnore: ["src"],
165
+ },
166
+ )
167
+ importmapInfo.inlinedFrom = importmapInfo.url
168
+ importmapInfo.url = compiledUrl
169
+ importmapInfo.text = importmapAsText
170
+ } else {
171
+ // inject a default importmap
172
+ const defaultImportMap = getDefaultImportMap({
173
+ importMapFileUrl: compiledUrl,
174
+ projectDirectoryUrl,
175
+ compileDirectoryRelativeUrl: `${outDirectoryRelativeUrl}${compileId}/`,
176
+ })
177
+ const importmapAsText = JSON.stringify(defaultImportMap, null, " ")
178
+ manipulateHtmlAst(htmlAst, {
179
+ scriptInjections: [
180
+ {
181
+ type:
182
+ moduleOutFormat === "systemjs" ? "jsenv-importmap" : "importmap",
183
+ // in case there is no importmap, force the presence
184
+ // so that '@jsenv/core/' are still remapped
185
+ text: importmapAsText,
186
+ },
187
+ ],
188
+ })
189
+ importmapInfo = {
190
+ url: compiledUrl,
191
+ text: importmapAsText,
192
+ }
193
+ }
194
+ onHtmlImportmapInfo({
195
+ htmlUrl: url,
196
+ importmapInfo,
197
+ })
130
198
 
199
+ const htmlDependencies = collectHtmlDependenciesFromAst(htmlAst)
200
+ const inlineScriptsContentMap = {}
201
+ scripts.forEach((script) => {
202
+ const typeAttribute = getHtmlNodeAttributeByName(script, "type")
203
+ const srcAttribute = getHtmlNodeAttributeByName(script, "src")
204
+ const src = srcAttribute ? srcAttribute.value : ""
131
205
  // remote module script
132
- if (typeAttribute && typeAttribute.value === "module" && srcAttribute) {
206
+ if (typeAttribute && typeAttribute.value === "module" && src) {
133
207
  if (moduleOutFormat === "systemjs") {
134
208
  removeHtmlNodeAttribute(script, typeAttribute)
135
209
  }
136
210
  removeHtmlNodeAttribute(script, srcAttribute)
137
- const src = srcAttribute.value
138
211
  const jsenvMethod =
139
212
  moduleOutFormat === "systemjs"
140
213
  ? "executeFileUsingSystemJs"
@@ -174,66 +247,6 @@ export const compileHtml = async ({
174
247
  return
175
248
  }
176
249
  })
177
-
178
- if (hasImportmap === false) {
179
- const defaultImportMap = getDefaultImportMap({
180
- importMapFileUrl: compiledUrl,
181
- projectDirectoryUrl,
182
- compileDirectoryRelativeUrl: `${outDirectoryRelativeUrl}${compileId}/`,
183
- })
184
- manipulateHtmlAst(htmlAst, {
185
- scriptInjections: [
186
- {
187
- type:
188
- moduleOutFormat === "systemjs" ? "jsenv-importmap" : "importmap",
189
- // in case there is no importmap, force the presence
190
- // so that '@jsenv/core/' are still remapped
191
- text: JSON.stringify(defaultImportMap, null, " "),
192
- },
193
- ],
194
- })
195
- }
196
-
197
- await Promise.all(
198
- importmapsToInline.map(async ({ script, src }) => {
199
- const importMapUrl = resolveUrl(src, url)
200
- const importMapResponse = await fetchUrl(importMapUrl)
201
- if (importMapResponse.status !== 200) {
202
- logger.warn(
203
- createDetailedMessage(
204
- importMapResponse.status === 404
205
- ? `Cannot inline importmap script because file cannot be found.`
206
- : `Cannot inline importmap script due to unexpected response status (${importMapResponse.status}).`,
207
- {
208
- "importmap script src": src,
209
- "importmap url": importMapUrl,
210
- "html url": url,
211
- },
212
- ),
213
- )
214
- return
215
- }
216
-
217
- const importMapContent = await importMapResponse.json()
218
- const importMapInlined = moveImportMap(
219
- importMapContent,
220
- importMapUrl,
221
- url,
222
- )
223
- replaceHtmlNode(
224
- script,
225
- `<script type="importmap">${JSON.stringify(
226
- importMapInlined,
227
- null,
228
- " ",
229
- )}</script>`,
230
- {
231
- attributesToIgnore: ["src"],
232
- },
233
- )
234
- }),
235
- )
236
-
237
250
  const htmlAfterTransformation = stringifyHtmlAst(htmlAst)
238
251
 
239
252
  let assets = []
@@ -303,12 +316,14 @@ export const compileHtml = async ({
303
316
  }
304
317
  }),
305
318
  )
319
+ sources.push(url)
320
+ sourcesContent.push(code)
306
321
 
307
322
  return {
308
323
  contentType: "text/html",
309
324
  compiledSource: htmlAfterTransformation,
310
- sources: [url],
311
- sourcesContent: [code],
325
+ sources,
326
+ sourcesContent,
312
327
  assets,
313
328
  assetsContent,
314
329
  dependencies: htmlDependencies.map(({ specifier }) => {
@@ -583,6 +583,9 @@ const setupServerSentEventsForLivereload = ({
583
583
  const projectFileAdded = createCallbackList()
584
584
 
585
585
  const projectFileRequestedCallback = (relativeUrl, request) => {
586
+ if (relativeUrl[0] === "/") {
587
+ relativeUrl = relativeUrl.slice(1)
588
+ }
586
589
  const url = `${projectDirectoryUrl}${relativeUrl}`
587
590
 
588
591
  if (
@@ -669,6 +672,7 @@ const setupServerSentEventsForLivereload = ({
669
672
  return
670
673
  }
671
674
 
675
+ livereloadLogger.debug(`${rootRelativeUrl} requested -> start tracking it`)
672
676
  // when a file is requested, always rebuild its dependency in case it has changed
673
677
  // since the last time it was requested
674
678
  startTrackingRoot(rootRelativeUrl)
@@ -699,6 +703,7 @@ const setupServerSentEventsForLivereload = ({
699
703
  const removeRootRemovedCallback = projectFileRemoved.add((relativeUrl) => {
700
704
  if (relativeUrl === rootRelativeUrl) {
701
705
  stopTrackingRoot(rootRelativeUrl)
706
+ livereloadLogger.debug(`${rootRelativeUrl} removed -> stop tracking it`)
702
707
  }
703
708
  })
704
709
  addStopTrackingCalback(rootRelativeUrl, removeRootRemovedCallback)