@jsenv/core 38.2.11 → 38.3.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.
@@ -4,6 +4,7 @@ import {
4
4
  getHtmlNodeText,
5
5
  setHtmlNodeText,
6
6
  removeHtmlNodeText,
7
+ removeHtmlNode,
7
8
  getHtmlNodeAttribute,
8
9
  getHtmlNodePosition,
9
10
  setHtmlNodeAttributes,
@@ -13,344 +14,538 @@ import {
13
14
  stringifyHtmlAst,
14
15
  getUrlForContentInsideHtml,
15
16
  } from "@jsenv/ast";
17
+ import {
18
+ resolveImport,
19
+ composeTwoImportMaps,
20
+ normalizeImportMap,
21
+ } from "@jsenv/importmap";
16
22
 
17
23
  export const jsenvPluginHtmlReferenceAnalysis = ({
18
24
  inlineContent,
19
25
  inlineConvertedScript,
20
26
  }) => {
27
+ /*
28
+ * About importmap found in HTML files:
29
+ * - feeds importmap files to jsenv kitchen
30
+ * - use importmap to resolve import (when there is one + fallback to other resolution mecanism)
31
+ * - inline importmap with [src=""]
32
+ *
33
+ * A correct importmap resolution should scope importmap resolution per html file.
34
+ * It would be doable by adding ?html_id to each js file in order to track
35
+ * the html file importing it.
36
+ * Considering it happens only when all the following conditions are met:
37
+ * - 2+ html files are using an importmap
38
+ * - the importmap used is not the same
39
+ * - the importmap contain conflicting mappings
40
+ * - these html files are both executed during the same scenario (dev, test, build)
41
+ * And that it would be ugly to see ?html_id all over the place
42
+ * -> The importmap resolution implemented here takes a shortcut and does the following:
43
+ * - All importmap found are merged into a single one that is applied to every import specifiers
44
+ */
45
+
46
+ let globalImportmap = null;
47
+ const importmaps = {};
48
+ let importmapLoadingCount = 0;
49
+ const allImportmapLoadedCallbackSet = new Set();
50
+ const startLoadingImportmap = (htmlUrlInfo) => {
51
+ importmapLoadingCount++;
52
+ return (importmapUrlInfo) => {
53
+ const htmlUrl = htmlUrlInfo.url;
54
+ if (importmapUrlInfo) {
55
+ // importmap was found in this HTML file and is known
56
+ const importmap = JSON.parse(importmapUrlInfo.content);
57
+ importmaps[htmlUrl] = normalizeImportMap(importmap, htmlUrl);
58
+ } else {
59
+ // no importmap in this HTML file
60
+ importmaps[htmlUrl] = null;
61
+ }
62
+ globalImportmap = Object.keys(importmaps).reduce((previous, url) => {
63
+ const importmap = importmaps[url];
64
+ if (!previous) {
65
+ return importmap;
66
+ }
67
+ if (!importmap) {
68
+ return previous;
69
+ }
70
+ return composeTwoImportMaps(previous, importmap);
71
+ }, null);
72
+
73
+ importmapLoadingCount--;
74
+ if (importmapLoadingCount === 0) {
75
+ allImportmapLoadedCallbackSet.forEach((callback) => {
76
+ callback();
77
+ });
78
+ allImportmapLoadedCallbackSet.clear();
79
+ }
80
+ };
81
+ };
82
+
21
83
  return {
22
84
  name: "jsenv:html_reference_analysis",
23
85
  appliesDuring: "*",
24
- transformUrlContent: {
25
- html: (urlInfo) =>
26
- parseAndTransformHtmlReferences(urlInfo, {
27
- inlineContent,
28
- inlineConvertedScript,
29
- }),
86
+ resolveReference: {
87
+ js_import: (reference) => {
88
+ if (!globalImportmap) {
89
+ return null;
90
+ }
91
+ try {
92
+ let fromMapping = false;
93
+ const result = resolveImport({
94
+ specifier: reference.specifier,
95
+ importer: reference.ownerUrlInfo.url,
96
+ importMap: globalImportmap,
97
+ onImportMapping: () => {
98
+ fromMapping = true;
99
+ },
100
+ });
101
+ if (fromMapping) {
102
+ return result;
103
+ }
104
+ return null;
105
+ } catch (e) {
106
+ if (e.message.includes("bare specifier")) {
107
+ // in theory we should throw to be compliant with web behaviour
108
+ // but for now it's simpler to return null
109
+ // and let a chance to other plugins to handle the bare specifier
110
+ // (node esm resolution)
111
+ // and we want importmap to be prio over node esm so we cannot put this plugin after
112
+ return null;
113
+ }
114
+ throw e;
115
+ }
116
+ },
30
117
  },
31
- };
32
- };
33
-
34
- const parseAndTransformHtmlReferences = async (
35
- urlInfo,
36
- { inlineContent, inlineConvertedScript },
37
- ) => {
38
- const content = urlInfo.content;
39
- const htmlAst = parseHtmlString(content);
118
+ transformUrlContent: {
119
+ js_module: async () => {
120
+ // wait for importmap if any
121
+ // so that resolveReference can happen with importmap
122
+ if (importmapLoadingCount) {
123
+ await new Promise((resolve) => {
124
+ allImportmapLoadedCallbackSet.add(resolve);
125
+ });
126
+ }
127
+ },
128
+ html: async (urlInfo) => {
129
+ let importmapFound = false;
130
+ const importmapLoaded = startLoadingImportmap(urlInfo);
40
131
 
41
- const mutations = [];
42
- const actions = [];
43
- const finalizeCallbacks = [];
132
+ const content = urlInfo.content;
133
+ const htmlAst = parseHtmlString(content);
44
134
 
45
- const createExternalReference = (
46
- node,
47
- attributeName,
48
- attributeValue,
49
- { type, subtype, expectedType, ...rest },
50
- ) => {
51
- let position;
52
- if (getHtmlNodeAttribute(node, "jsenv-cooked-by")) {
53
- // when generated from inline content,
54
- // line, column is not "src" nor "inlined-from-src" but "original-position"
55
- position = getHtmlNodePosition(node);
56
- } else {
57
- position = getHtmlNodeAttributePosition(node, attributeName);
58
- }
59
- const {
60
- line,
61
- column,
62
- // originalLine, originalColumn
63
- } = position;
64
- const debug = getHtmlNodeAttribute(node, "jsenv-debug") !== undefined;
135
+ const mutations = [];
136
+ const actions = [];
137
+ const finalizeCallbacks = [];
65
138
 
66
- const { crossorigin, integrity } = readFetchMetas(node);
67
- const isResourceHint = [
68
- "preconnect",
69
- "dns-prefetch",
70
- "prefetch",
71
- "preload",
72
- "modulepreload",
73
- ].includes(subtype);
74
- let attributeLocation = node.sourceCodeLocation.attrs[attributeName];
75
- if (
76
- !attributeLocation &&
77
- attributeName === "href" &&
78
- (node.tagName === "use" || node.tagName === "image")
79
- ) {
80
- attributeLocation = node.sourceCodeLocation.attrs["xlink:href"];
81
- }
82
- const attributeStart = attributeLocation.startOffset;
83
- const attributeValueStart = urlInfo.content.indexOf(
84
- attributeValue,
85
- attributeStart + `${attributeName}=`.length,
86
- );
87
- const attributeValueEnd = attributeValueStart + attributeValue.length;
88
- const reference = urlInfo.dependencies.found({
89
- type,
90
- subtype,
91
- expectedType,
92
- specifier: attributeValue,
93
- specifierLine: line,
94
- specifierColumn: column,
95
- specifierStart: attributeValueStart,
96
- specifierEnd: attributeValueEnd,
97
- isResourceHint,
98
- isWeak: isResourceHint,
99
- crossorigin,
100
- integrity,
101
- debug,
102
- astInfo: { node, attributeName },
103
- ...rest,
104
- });
105
- actions.push(async () => {
106
- await reference.readGeneratedSpecifier();
107
- mutations.push(() => {
108
- setHtmlNodeAttributes(node, {
109
- [attributeName]: reference.generatedSpecifier,
110
- });
111
- });
112
- });
113
- return reference;
114
- };
115
- const visitHref = (node, referenceProps) => {
116
- const href = getHtmlNodeAttribute(node, "href");
117
- if (href) {
118
- return createExternalReference(node, "href", href, referenceProps);
119
- }
120
- return null;
121
- };
122
- const visitSrc = (node, referenceProps) => {
123
- const src = getHtmlNodeAttribute(node, "src");
124
- if (src) {
125
- return createExternalReference(node, "src", src, referenceProps);
126
- }
127
- return null;
128
- };
129
- const visitSrcset = (node, referenceProps) => {
130
- const srcset = getHtmlNodeAttribute(node, "srcset");
131
- if (srcset) {
132
- const srcCandidates = parseSrcSet(srcset);
133
- return srcCandidates.map((srcCandidate) => {
134
- return createExternalReference(
139
+ const createExternalReference = (
135
140
  node,
136
- "srcset",
137
- srcCandidate.specifier,
138
- referenceProps,
139
- );
140
- });
141
- }
142
- return null;
143
- };
141
+ attributeName,
142
+ attributeValue,
143
+ { type, subtype, expectedType, ...rest },
144
+ ) => {
145
+ let position;
146
+ if (getHtmlNodeAttribute(node, "jsenv-cooked-by")) {
147
+ // when generated from inline content,
148
+ // line, column is not "src" nor "inlined-from-src" but "original-position"
149
+ position = getHtmlNodePosition(node);
150
+ } else {
151
+ position = getHtmlNodeAttributePosition(node, attributeName);
152
+ }
153
+ const {
154
+ line,
155
+ column,
156
+ // originalLine, originalColumn
157
+ } = position;
158
+ const debug = getHtmlNodeAttribute(node, "jsenv-debug") !== undefined;
144
159
 
145
- const createInlineReference = (
146
- node,
147
- inlineContent,
148
- { type, expectedType, contentType },
149
- ) => {
150
- const hotAccept = getHtmlNodeAttribute(node, "hot-accept") !== undefined;
151
- const { line, column, isOriginal } = getHtmlNodePosition(node, {
152
- preferOriginal: true,
153
- });
154
- const inlineContentUrl = getUrlForContentInsideHtml(node, {
155
- htmlUrl: urlInfo.url,
156
- });
157
- const debug = getHtmlNodeAttribute(node, "jsenv-debug") !== undefined;
158
- const inlineReference = urlInfo.dependencies.foundInline({
159
- type,
160
- expectedType,
161
- isOriginalPosition: isOriginal,
162
- // we remove 1 to the line because imagine the following html:
163
- // <style>body { color: red; }</style>
164
- // -> content starts same line as <style> (same for <script>)
165
- specifierLine: line - 1,
166
- specifierColumn: column,
167
- specifier: inlineContentUrl,
168
- contentType,
169
- content: inlineContent,
170
- debug,
171
- astInfo: { node },
172
- });
160
+ const { crossorigin, integrity } = readFetchMetas(node);
161
+ const isResourceHint = [
162
+ "preconnect",
163
+ "dns-prefetch",
164
+ "prefetch",
165
+ "preload",
166
+ "modulepreload",
167
+ ].includes(subtype);
168
+ let attributeLocation = node.sourceCodeLocation.attrs[attributeName];
169
+ if (
170
+ !attributeLocation &&
171
+ attributeName === "href" &&
172
+ (node.tagName === "use" || node.tagName === "image")
173
+ ) {
174
+ attributeLocation = node.sourceCodeLocation.attrs["xlink:href"];
175
+ }
176
+ const attributeStart = attributeLocation.startOffset;
177
+ const attributeValueStart = urlInfo.content.indexOf(
178
+ attributeValue,
179
+ attributeStart + `${attributeName}=`.length,
180
+ );
181
+ const attributeValueEnd = attributeValueStart + attributeValue.length;
182
+ const reference = urlInfo.dependencies.found({
183
+ type,
184
+ subtype,
185
+ expectedType,
186
+ specifier: attributeValue,
187
+ specifierLine: line,
188
+ specifierColumn: column,
189
+ specifierStart: attributeValueStart,
190
+ specifierEnd: attributeValueEnd,
191
+ isResourceHint,
192
+ isWeak: isResourceHint,
193
+ crossorigin,
194
+ integrity,
195
+ debug,
196
+ astInfo: { node, attributeName },
197
+ ...rest,
198
+ });
199
+ actions.push(async () => {
200
+ await reference.readGeneratedSpecifier();
201
+ mutations.push(() => {
202
+ setHtmlNodeAttributes(node, {
203
+ [attributeName]: reference.generatedSpecifier,
204
+ });
205
+ });
206
+ });
207
+ return reference;
208
+ };
209
+ const visitHref = (node, referenceProps) => {
210
+ const href = getHtmlNodeAttribute(node, "href");
211
+ if (href) {
212
+ return createExternalReference(node, "href", href, referenceProps);
213
+ }
214
+ return null;
215
+ };
216
+ const visitSrc = (node, referenceProps) => {
217
+ const src = getHtmlNodeAttribute(node, "src");
218
+ if (src) {
219
+ return createExternalReference(node, "src", src, referenceProps);
220
+ }
221
+ return null;
222
+ };
223
+ const visitSrcset = (node, referenceProps) => {
224
+ const srcset = getHtmlNodeAttribute(node, "srcset");
225
+ if (srcset) {
226
+ const srcCandidates = parseSrcSet(srcset);
227
+ return srcCandidates.map((srcCandidate) => {
228
+ return createExternalReference(
229
+ node,
230
+ "srcset",
231
+ srcCandidate.specifier,
232
+ referenceProps,
233
+ );
234
+ });
235
+ }
236
+ return null;
237
+ };
173
238
 
174
- actions.push(async () => {
175
- await inlineReference.urlInfo.cook();
176
- mutations.push(() => {
177
- if (hotAccept) {
178
- removeHtmlNodeText(node);
179
- setHtmlNodeAttributes(node, {
180
- "jsenv-cooked-by": "jsenv:html_inline_content_analysis",
239
+ const createInlineReference = (
240
+ node,
241
+ inlineContent,
242
+ { type, expectedType, contentType },
243
+ ) => {
244
+ const hotAccept =
245
+ getHtmlNodeAttribute(node, "hot-accept") !== undefined;
246
+ const { line, column, isOriginal } = getHtmlNodePosition(node, {
247
+ preferOriginal: true,
181
248
  });
182
- } else {
183
- setHtmlNodeText(node, inlineReference.urlInfo.content, {
184
- indentation: false, // indentation would decrease stack trace precision
249
+ const inlineContentUrl = getUrlForContentInsideHtml(node, {
250
+ htmlUrl: urlInfo.url,
185
251
  });
186
- setHtmlNodeAttributes(node, {
187
- "jsenv-cooked-by": "jsenv:html_inline_content_analysis",
252
+ const debug = getHtmlNodeAttribute(node, "jsenv-debug") !== undefined;
253
+ const inlineReference = urlInfo.dependencies.foundInline({
254
+ type,
255
+ expectedType,
256
+ isOriginalPosition: isOriginal,
257
+ // we remove 1 to the line because imagine the following html:
258
+ // <style>body { color: red; }</style>
259
+ // -> content starts same line as <style> (same for <script>)
260
+ specifierLine: line - 1,
261
+ specifierColumn: column,
262
+ specifier: inlineContentUrl,
263
+ contentType,
264
+ content: inlineContent,
265
+ debug,
266
+ astInfo: { node },
188
267
  });
189
- }
190
- });
191
- });
192
- return inlineReference;
193
- };
194
- const visitTextContent = (
195
- node,
196
- { type, subtype, expectedType, contentType },
197
- ) => {
198
- const inlineContent = getHtmlNodeText(node);
199
- if (!inlineContent) {
200
- return null;
201
- }
202
- return createInlineReference(node, inlineContent, {
203
- type,
204
- subtype,
205
- expectedType,
206
- contentType,
207
- });
208
- };
209
268
 
210
- visitHtmlNodes(htmlAst, {
211
- link: (linkNode) => {
212
- const rel = getHtmlNodeAttribute(linkNode, "rel");
213
- const type = getHtmlNodeAttribute(linkNode, "type");
214
- const ref = visitHref(linkNode, {
215
- type: "link_href",
216
- subtype: rel,
217
- // https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload#including_a_mime_type
218
- expectedContentType: type,
219
- });
220
- if (ref) {
221
- finalizeCallbacks.push(() => {
222
- if (ref.expectedType) {
223
- // might be set by other plugins, in that case respect it
224
- } else {
225
- ref.expectedType = decideLinkExpectedType(ref, urlInfo);
269
+ actions.push(async () => {
270
+ await inlineReference.urlInfo.cook();
271
+ mutations.push(() => {
272
+ if (hotAccept) {
273
+ removeHtmlNodeText(node);
274
+ setHtmlNodeAttributes(node, {
275
+ "jsenv-cooked-by": "jsenv:html_inline_content_analysis",
276
+ });
277
+ } else {
278
+ setHtmlNodeText(node, inlineReference.urlInfo.content, {
279
+ indentation: false, // indentation would decrease stack trace precision
280
+ });
281
+ setHtmlNodeAttributes(node, {
282
+ "jsenv-cooked-by": "jsenv:html_inline_content_analysis",
283
+ });
284
+ }
285
+ });
286
+ });
287
+ return inlineReference;
288
+ };
289
+ const visitTextContent = (
290
+ node,
291
+ { type, subtype, expectedType, contentType },
292
+ ) => {
293
+ const inlineContent = getHtmlNodeText(node);
294
+ if (!inlineContent) {
295
+ return null;
226
296
  }
227
- });
228
- }
229
- },
230
- style: inlineContent
231
- ? (styleNode) => {
232
- visitTextContent(styleNode, {
233
- type: "style",
234
- expectedType: "css",
235
- contentType: "text/css",
297
+ return createInlineReference(node, inlineContent, {
298
+ type,
299
+ subtype,
300
+ expectedType,
301
+ contentType,
236
302
  });
237
- }
238
- : null,
239
- script: (scriptNode) => {
240
- // during build the importmap is inlined
241
- // and shoud not be considered as a dependency anymore
242
- if (
243
- getHtmlNodeAttribute(scriptNode, "jsenv-inlined-by") ===
244
- "jsenv:importmap"
245
- ) {
246
- return;
247
- }
303
+ };
248
304
 
249
- const { type, subtype, contentType, extension } =
250
- analyzeScriptNode(scriptNode);
251
- // ignore <script type="whatever">foobar</script>
252
- // per HTML spec https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type
253
- if (type !== "text") {
254
- const externalRef = visitSrc(scriptNode, {
255
- type: "script",
256
- subtype: type,
257
- expectedType: type,
258
- });
259
- if (externalRef) {
260
- return;
261
- }
262
- }
305
+ visitHtmlNodes(htmlAst, {
306
+ link: (linkNode) => {
307
+ const rel = getHtmlNodeAttribute(linkNode, "rel");
308
+ const type = getHtmlNodeAttribute(linkNode, "type");
309
+ const ref = visitHref(linkNode, {
310
+ type: "link_href",
311
+ subtype: rel,
312
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload#including_a_mime_type
313
+ expectedContentType: type,
314
+ });
315
+ if (ref) {
316
+ finalizeCallbacks.push(() => {
317
+ if (ref.expectedType) {
318
+ // might be set by other plugins, in that case respect it
319
+ } else {
320
+ ref.expectedType = decideLinkExpectedType(ref, urlInfo);
321
+ }
322
+ });
323
+ }
324
+ },
325
+ style: inlineContent
326
+ ? (styleNode) => {
327
+ visitTextContent(styleNode, {
328
+ type: "style",
329
+ expectedType: "css",
330
+ contentType: "text/css",
331
+ });
332
+ }
333
+ : null,
334
+ script: (scriptNode) => {
335
+ const { type, subtype, contentType, extension } =
336
+ analyzeScriptNode(scriptNode);
337
+ if (type === "text") {
338
+ // ignore <script type="whatever">foobar</script>
339
+ // per HTML spec https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type
340
+ return;
341
+ }
342
+ if (type === "importmap") {
343
+ importmapFound = true;
263
344
 
264
- // now visit the content, if any
265
- if (!inlineContent) {
266
- return;
267
- }
268
- // If the inline script was already handled by an other plugin, ignore it
269
- // - we want to preserve inline scripts generated by html supervisor during dev
270
- // - we want to avoid cooking twice a script during build
271
- if (
272
- !inlineConvertedScript &&
273
- getHtmlNodeAttribute(scriptNode, "jsenv-injected-by") ===
274
- "jsenv:js_module_fallback"
275
- ) {
276
- return;
277
- }
345
+ const src = getHtmlNodeAttribute(scriptNode, "src");
346
+ if (src) {
347
+ // Browser would throw on remote importmap
348
+ // and won't sent a request to the server for it
349
+ // We must precook the importmap to know its content and inline it into the HTML
350
+ const importmapReference = createExternalReference(
351
+ scriptNode,
352
+ "src",
353
+ src,
354
+ {
355
+ type: "script",
356
+ subtype: "importmap",
357
+ expectedType: "importmap",
358
+ },
359
+ );
360
+ const { line, column, isOriginal } = getHtmlNodePosition(
361
+ scriptNode,
362
+ {
363
+ preferOriginal: true,
364
+ },
365
+ );
366
+ const importmapInlineUrl = getUrlForContentInsideHtml(
367
+ scriptNode,
368
+ {
369
+ htmlUrl: urlInfo.url,
370
+ },
371
+ );
372
+ const importmapReferenceInlined = importmapReference.inline({
373
+ line: line - 1,
374
+ column,
375
+ isOriginal,
376
+ specifier: importmapInlineUrl,
377
+ contentType: "application/importmap+json",
378
+ });
379
+ const importmapInlineUrlInfo =
380
+ importmapReferenceInlined.urlInfo;
381
+ actions.push(async () => {
382
+ await importmapInlineUrlInfo.cook();
383
+ importmapLoaded(importmapInlineUrlInfo);
384
+ mutations.push(() => {
385
+ setHtmlNodeText(
386
+ scriptNode,
387
+ importmapInlineUrlInfo.content,
388
+ {
389
+ indentation: "auto",
390
+ },
391
+ );
392
+ setHtmlNodeAttributes(scriptNode, {
393
+ "src": undefined,
394
+ "jsenv-inlined-by": "jsenv:html_reference_analysis",
395
+ "inlined-from-src": src,
396
+ });
397
+ });
398
+ });
399
+ } else {
400
+ const htmlNodeText = getHtmlNodeText(scriptNode);
401
+ if (htmlNodeText) {
402
+ const importmapReference = createInlineReference(
403
+ scriptNode,
404
+ htmlNodeText,
405
+ {
406
+ type: "script",
407
+ expectedType: "importmap",
408
+ contentType: "application/importmap+json",
409
+ },
410
+ );
411
+ const inlineImportmapUrlInfo = importmapReference.urlInfo;
412
+ actions.push(async () => {
413
+ await inlineImportmapUrlInfo.cook();
414
+ importmapLoaded(inlineImportmapUrlInfo);
415
+ mutations.push(() => {
416
+ setHtmlNodeText(
417
+ scriptNode,
418
+ inlineImportmapUrlInfo.content,
419
+ {
420
+ indentation: "auto",
421
+ },
422
+ );
423
+ setHtmlNodeAttributes(scriptNode, {
424
+ "jsenv-cooked-by": "jsenv:html_reference_analysis",
425
+ });
426
+ });
427
+ });
428
+ }
429
+ }
430
+ // once this plugin knows the importmap, it will use it
431
+ // to map imports. These import specifiers will be normalized
432
+ // by "formatReference" making the importmap presence useless.
433
+ // In dev/test we keep importmap into the HTML to see it even if useless
434
+ // Duing build we get rid of it
435
+ if (urlInfo.context.build) {
436
+ mutations.push(() => {
437
+ removeHtmlNode(scriptNode);
438
+ });
439
+ }
440
+ return;
441
+ }
442
+ const externalRef = visitSrc(scriptNode, {
443
+ type: "script",
444
+ subtype: type,
445
+ expectedType: type,
446
+ });
447
+ if (externalRef) {
448
+ return;
449
+ }
450
+
451
+ // now visit the content, if any
452
+ if (!inlineContent) {
453
+ return;
454
+ }
455
+ // If the inline script was already handled by an other plugin, ignore it
456
+ // - we want to preserve inline scripts generated by html supervisor during dev
457
+ // - we want to avoid cooking twice a script during build
458
+ if (
459
+ !inlineConvertedScript &&
460
+ getHtmlNodeAttribute(scriptNode, "jsenv-injected-by") ===
461
+ "jsenv:js_module_fallback"
462
+ ) {
463
+ return;
464
+ }
278
465
 
279
- const inlineRef = visitTextContent(scriptNode, {
280
- type: "script",
281
- subtype,
282
- expectedType: type,
283
- contentType,
284
- });
285
- if (inlineRef) {
286
- // 1. <script type="jsx"> becomes <script>
287
- if (type === "js_classic" && extension !== ".js") {
288
- mutations.push(() => {
289
- setHtmlNodeAttributes(scriptNode, {
290
- type: undefined,
466
+ const inlineRef = visitTextContent(scriptNode, {
467
+ type: "script",
468
+ subtype,
469
+ expectedType: type,
470
+ contentType,
291
471
  });
292
- });
293
- }
294
- // 2. <script type="module/jsx"> becomes <script type="module">
295
- if (type === "js_module" && extension !== ".js") {
296
- mutations.push(() => {
297
- setHtmlNodeAttributes(scriptNode, {
298
- type: "module",
472
+ if (inlineRef) {
473
+ // 1. <script type="jsx"> becomes <script>
474
+ if (type === "js_classic" && extension !== ".js") {
475
+ mutations.push(() => {
476
+ setHtmlNodeAttributes(scriptNode, {
477
+ type: undefined,
478
+ });
479
+ });
480
+ }
481
+ // 2. <script type="module/jsx"> becomes <script type="module">
482
+ if (type === "js_module" && extension !== ".js") {
483
+ mutations.push(() => {
484
+ setHtmlNodeAttributes(scriptNode, {
485
+ type: "module",
486
+ });
487
+ });
488
+ }
489
+ }
490
+ },
491
+ a: (aNode) => {
492
+ visitHref(aNode, {
493
+ type: "a_href",
299
494
  });
300
- });
495
+ },
496
+ iframe: (iframeNode) => {
497
+ visitSrc(iframeNode, {
498
+ type: "iframe_src",
499
+ });
500
+ },
501
+ img: (imgNode) => {
502
+ visitSrc(imgNode, {
503
+ type: "img_src",
504
+ });
505
+ visitSrcset(imgNode, {
506
+ type: "img_srcset",
507
+ });
508
+ },
509
+ source: (sourceNode) => {
510
+ visitSrc(sourceNode, {
511
+ type: "source_src",
512
+ });
513
+ visitSrcset(sourceNode, {
514
+ type: "source_srcset",
515
+ });
516
+ },
517
+ // svg <image> tag
518
+ image: (imageNode) => {
519
+ visitHref(imageNode, {
520
+ type: "image_href",
521
+ });
522
+ },
523
+ use: (useNode) => {
524
+ visitHref(useNode, {
525
+ type: "use_href",
526
+ });
527
+ },
528
+ });
529
+ if (!importmapFound) {
530
+ importmapLoaded();
301
531
  }
302
- }
303
- },
304
- a: (aNode) => {
305
- visitHref(aNode, {
306
- type: "a_href",
307
- });
308
- },
309
- iframe: (iframeNode) => {
310
- visitSrc(iframeNode, {
311
- type: "iframe_src",
312
- });
313
- },
314
- img: (imgNode) => {
315
- visitSrc(imgNode, {
316
- type: "img_src",
317
- });
318
- visitSrcset(imgNode, {
319
- type: "img_srcset",
320
- });
321
- },
322
- source: (sourceNode) => {
323
- visitSrc(sourceNode, {
324
- type: "source_src",
325
- });
326
- visitSrcset(sourceNode, {
327
- type: "source_srcset",
328
- });
329
- },
330
- // svg <image> tag
331
- image: (imageNode) => {
332
- visitHref(imageNode, {
333
- type: "image_href",
334
- });
335
- },
336
- use: (useNode) => {
337
- visitHref(useNode, {
338
- type: "use_href",
339
- });
340
- },
341
- });
342
- finalizeCallbacks.forEach((finalizeCallback) => {
343
- finalizeCallback();
344
- });
532
+ finalizeCallbacks.forEach((finalizeCallback) => {
533
+ finalizeCallback();
534
+ });
345
535
 
346
- if (actions.length > 0) {
347
- await Promise.all(actions.map((action) => action()));
348
- }
349
- if (mutations.length === 0) {
350
- return null;
351
- }
352
- mutations.forEach((mutation) => mutation());
353
- return stringifyHtmlAst(htmlAst);
536
+ if (actions.length > 0) {
537
+ await Promise.all(actions.map((action) => action()));
538
+ actions.length = 0;
539
+ }
540
+ if (mutations.length === 0) {
541
+ return null;
542
+ }
543
+ mutations.forEach((mutation) => mutation());
544
+ mutations.length = 0;
545
+ return stringifyHtmlAst(htmlAst);
546
+ },
547
+ },
548
+ };
354
549
  };
355
550
 
356
551
  const crossOriginCompatibleTagNames = ["script", "link", "img", "source"];