@jsenv/core 38.4.16 → 38.4.18

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.
@@ -1,3 +1,6 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { urlToRelativeUrl } from "@jsenv/urls";
3
+ import { generateContentFrame } from "@jsenv/humanize";
1
4
  import {
2
5
  parseHtml,
3
6
  visitHtmlNodes,
@@ -20,6 +23,11 @@ import {
20
23
  normalizeImportMap,
21
24
  } from "@jsenv/importmap";
22
25
 
26
+ const htmlSyntaxErrorFileUrl = new URL(
27
+ "./html_syntax_error.html",
28
+ import.meta.url,
29
+ );
30
+
23
31
  export const jsenvPluginHtmlReferenceAnalysis = ({
24
32
  inlineContent,
25
33
  inlineConvertedScript,
@@ -127,429 +135,532 @@ export const jsenvPluginHtmlReferenceAnalysis = ({
127
135
  },
128
136
  html: async (urlInfo) => {
129
137
  let importmapFound = false;
130
- const importmapLoaded = startLoadingImportmap(urlInfo);
131
-
132
- const htmlAst = parseHtml({
133
- html: urlInfo.content,
134
- url: urlInfo.url,
135
- });
136
-
137
- const mutations = [];
138
- const actions = [];
139
- const finalizeCallbacks = [];
140
138
 
141
- const createExternalReference = (
142
- node,
143
- attributeName,
144
- attributeValue,
145
- { type, subtype, expectedType, ...rest },
146
- ) => {
147
- let position;
148
- if (getHtmlNodeAttribute(node, "jsenv-cooked-by")) {
149
- // when generated from inline content,
150
- // line, column is not "src" nor "inlined-from-src" but "original-position"
151
- position = getHtmlNodePosition(node);
139
+ let htmlAst;
140
+ try {
141
+ htmlAst = parseHtml({
142
+ html: urlInfo.content,
143
+ url: urlInfo.url,
144
+ });
145
+ } catch (e) {
146
+ if (e.code === "PARSE_ERROR") {
147
+ const line = e.line;
148
+ const column = e.column;
149
+ const htmlErrorContentFrame = generateContentFrame({
150
+ content: urlInfo.content,
151
+ line,
152
+ column,
153
+ });
154
+ console.error(`Error while handling ${urlInfo.context.request ? urlInfo.context.request.url : urlInfo.url}:
155
+ ${e.reasonCode}
156
+ ${urlInfo.url}:${line}:${column}
157
+ ${htmlErrorContentFrame}`);
158
+ const html = generateHtmlForSyntaxError(e, {
159
+ htmlUrl: urlInfo.url,
160
+ rootDirectoryUrl: urlInfo.context.rootDirectoryUrl,
161
+ htmlErrorContentFrame,
162
+ });
163
+ htmlAst = parseHtml({
164
+ html,
165
+ url: htmlSyntaxErrorFileUrl,
166
+ });
152
167
  } else {
153
- position = getHtmlNodeAttributePosition(node, attributeName);
168
+ throw e;
154
169
  }
155
- const {
156
- line,
157
- column,
158
- // originalLine, originalColumn
159
- } = position;
160
- const debug = getHtmlNodeAttribute(node, "jsenv-debug") !== undefined;
170
+ }
161
171
 
162
- const { crossorigin, integrity } = readFetchMetas(node);
163
- const isResourceHint = [
164
- "preconnect",
165
- "dns-prefetch",
166
- "prefetch",
167
- "preload",
168
- "modulepreload",
169
- ].includes(subtype);
170
- let attributeLocation = node.sourceCodeLocation.attrs[attributeName];
171
- if (
172
- !attributeLocation &&
173
- attributeName === "href" &&
174
- (node.tagName === "use" || node.tagName === "image")
175
- ) {
176
- attributeLocation = node.sourceCodeLocation.attrs["xlink:href"];
177
- }
178
- const attributeStart = attributeLocation.startOffset;
179
- const attributeValueStart = urlInfo.content.indexOf(
172
+ const importmapLoaded = startLoadingImportmap(urlInfo);
173
+
174
+ try {
175
+ const mutations = [];
176
+ const actions = [];
177
+ const finalizeCallbacks = [];
178
+
179
+ const createExternalReference = (
180
+ node,
181
+ attributeName,
180
182
  attributeValue,
181
- attributeStart + `${attributeName}=`.length,
182
- );
183
- const attributeValueEnd = attributeValueStart + attributeValue.length;
184
- const reference = urlInfo.dependencies.found({
185
- type,
186
- subtype,
187
- expectedType,
188
- specifier: attributeValue,
189
- specifierLine: line,
190
- specifierColumn: column,
191
- specifierStart: attributeValueStart,
192
- specifierEnd: attributeValueEnd,
193
- isResourceHint,
194
- isWeak: isResourceHint,
195
- crossorigin,
196
- integrity,
197
- debug,
198
- astInfo: { node, attributeName },
199
- ...rest,
200
- });
201
- actions.push(async () => {
202
- await reference.readGeneratedSpecifier();
203
- mutations.push(() => {
204
- setHtmlNodeAttributes(node, {
205
- [attributeName]: reference.generatedSpecifier,
183
+ { type, subtype, expectedType, ...rest },
184
+ ) => {
185
+ let position;
186
+ if (getHtmlNodeAttribute(node, "jsenv-cooked-by")) {
187
+ // when generated from inline content,
188
+ // line, column is not "src" nor "inlined-from-src" but "original-position"
189
+ position = getHtmlNodePosition(node);
190
+ } else {
191
+ position = getHtmlNodeAttributePosition(node, attributeName);
192
+ }
193
+ const {
194
+ line,
195
+ column,
196
+ // originalLine, originalColumn
197
+ } = position;
198
+ const debug =
199
+ getHtmlNodeAttribute(node, "jsenv-debug") !== undefined;
200
+
201
+ const { crossorigin, integrity } = readFetchMetas(node);
202
+ const isResourceHint = [
203
+ "preconnect",
204
+ "dns-prefetch",
205
+ "prefetch",
206
+ "preload",
207
+ "modulepreload",
208
+ ].includes(subtype);
209
+ let attributeLocation =
210
+ node.sourceCodeLocation.attrs[attributeName];
211
+ if (
212
+ !attributeLocation &&
213
+ attributeName === "href" &&
214
+ (node.tagName === "use" || node.tagName === "image")
215
+ ) {
216
+ attributeLocation = node.sourceCodeLocation.attrs["xlink:href"];
217
+ }
218
+ const attributeStart = attributeLocation.startOffset;
219
+ const attributeValueStart = urlInfo.content.indexOf(
220
+ attributeValue,
221
+ attributeStart + `${attributeName}=`.length,
222
+ );
223
+ const attributeValueEnd =
224
+ attributeValueStart + attributeValue.length;
225
+ const reference = urlInfo.dependencies.found({
226
+ type,
227
+ subtype,
228
+ expectedType,
229
+ specifier: attributeValue,
230
+ specifierLine: line,
231
+ specifierColumn: column,
232
+ specifierStart: attributeValueStart,
233
+ specifierEnd: attributeValueEnd,
234
+ isResourceHint,
235
+ isWeak: isResourceHint,
236
+ crossorigin,
237
+ integrity,
238
+ debug,
239
+ astInfo: { node, attributeName },
240
+ ...rest,
241
+ });
242
+ actions.push(async () => {
243
+ await reference.readGeneratedSpecifier();
244
+ mutations.push(() => {
245
+ setHtmlNodeAttributes(node, {
246
+ [attributeName]: reference.generatedSpecifier,
247
+ });
206
248
  });
207
249
  });
208
- });
209
- return reference;
210
- };
211
- const visitHref = (node, referenceProps) => {
212
- const href = getHtmlNodeAttribute(node, "href");
213
- if (href) {
214
- return createExternalReference(node, "href", href, referenceProps);
215
- }
216
- return null;
217
- };
218
- const visitSrc = (node, referenceProps) => {
219
- const src = getHtmlNodeAttribute(node, "src");
220
- if (src) {
221
- return createExternalReference(node, "src", src, referenceProps);
222
- }
223
- return null;
224
- };
225
- const visitSrcset = (node, referenceProps) => {
226
- const srcset = getHtmlNodeAttribute(node, "srcset");
227
- if (srcset) {
228
- const srcCandidates = parseSrcSet(srcset);
229
- return srcCandidates.map((srcCandidate) => {
250
+ return reference;
251
+ };
252
+ const visitHref = (node, referenceProps) => {
253
+ const href = getHtmlNodeAttribute(node, "href");
254
+ if (href) {
230
255
  return createExternalReference(
231
256
  node,
232
- "srcset",
233
- srcCandidate.specifier,
257
+ "href",
258
+ href,
234
259
  referenceProps,
235
260
  );
261
+ }
262
+ return null;
263
+ };
264
+ const visitSrc = (node, referenceProps) => {
265
+ const src = getHtmlNodeAttribute(node, "src");
266
+ if (src) {
267
+ return createExternalReference(node, "src", src, referenceProps);
268
+ }
269
+ return null;
270
+ };
271
+ const visitSrcset = (node, referenceProps) => {
272
+ const srcset = getHtmlNodeAttribute(node, "srcset");
273
+ if (srcset) {
274
+ const srcCandidates = parseSrcSet(srcset);
275
+ return srcCandidates.map((srcCandidate) => {
276
+ return createExternalReference(
277
+ node,
278
+ "srcset",
279
+ srcCandidate.specifier,
280
+ referenceProps,
281
+ );
282
+ });
283
+ }
284
+ return null;
285
+ };
286
+ const createInlineReference = (
287
+ node,
288
+ inlineContent,
289
+ { type, expectedType, contentType },
290
+ ) => {
291
+ const hotAccept =
292
+ getHtmlNodeAttribute(node, "hot-accept") !== undefined;
293
+ const { line, column, isOriginal } = getHtmlNodePosition(node, {
294
+ preferOriginal: true,
236
295
  });
237
- }
238
- return null;
239
- };
240
-
241
- const createInlineReference = (
242
- node,
243
- inlineContent,
244
- { type, expectedType, contentType },
245
- ) => {
246
- const hotAccept =
247
- getHtmlNodeAttribute(node, "hot-accept") !== undefined;
248
- const { line, column, isOriginal } = getHtmlNodePosition(node, {
249
- preferOriginal: true,
250
- });
251
- const inlineContentUrl = getUrlForContentInsideHtml(node, {
252
- htmlUrl: urlInfo.url,
253
- });
254
- const debug = getHtmlNodeAttribute(node, "jsenv-debug") !== undefined;
255
- const inlineReference = urlInfo.dependencies.foundInline({
256
- type,
257
- expectedType,
258
- isOriginalPosition: isOriginal,
259
- // we remove 1 to the line because imagine the following html:
260
- // <style>body { color: red; }</style>
261
- // -> content starts same line as <style> (same for <script>)
262
- specifierLine: line - 1,
263
- specifierColumn: column,
264
- specifier: inlineContentUrl,
265
- contentType,
266
- content: inlineContent,
267
- debug,
268
- astInfo: { node },
269
- });
270
-
271
- actions.push(async () => {
272
- await inlineReference.urlInfo.cook();
273
- mutations.push(() => {
274
- if (hotAccept) {
275
- removeHtmlNodeText(node);
276
- setHtmlNodeAttributes(node, {
277
- "jsenv-cooked-by": "jsenv:html_inline_content_analysis",
278
- });
279
- } else {
280
- setHtmlNodeText(node, inlineReference.urlInfo.content, {
281
- indentation: false, // indentation would decrease stack trace precision
282
- });
283
- setHtmlNodeAttributes(node, {
284
- "jsenv-cooked-by": "jsenv:html_inline_content_analysis",
285
- });
286
- }
296
+ const inlineContentUrl = getUrlForContentInsideHtml(node, {
297
+ htmlUrl: urlInfo.url,
287
298
  });
288
- });
289
- return inlineReference;
290
- };
291
- const visitTextContent = (
292
- node,
293
- { type, subtype, expectedType, contentType },
294
- ) => {
295
- const inlineContent = getHtmlNodeText(node);
296
- if (!inlineContent) {
297
- return null;
298
- }
299
- return createInlineReference(node, inlineContent, {
300
- type,
301
- subtype,
302
- expectedType,
303
- contentType,
304
- });
305
- };
306
-
307
- visitHtmlNodes(htmlAst, {
308
- link: (linkNode) => {
309
- const rel = getHtmlNodeAttribute(linkNode, "rel");
310
- const type = getHtmlNodeAttribute(linkNode, "type");
311
- const ref = visitHref(linkNode, {
312
- type: "link_href",
313
- subtype: rel,
314
- // https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload#including_a_mime_type
315
- expectedContentType: type,
299
+ const debug =
300
+ getHtmlNodeAttribute(node, "jsenv-debug") !== undefined;
301
+ const inlineReference = urlInfo.dependencies.foundInline({
302
+ type,
303
+ expectedType,
304
+ isOriginalPosition: isOriginal,
305
+ // we remove 1 to the line because imagine the following html:
306
+ // <style>body { color: red; }</style>
307
+ // -> content starts same line as <style> (same for <script>)
308
+ specifierLine: line - 1,
309
+ specifierColumn: column,
310
+ specifier: inlineContentUrl,
311
+ contentType,
312
+ content: inlineContent,
313
+ debug,
314
+ astInfo: { node },
316
315
  });
317
- if (ref) {
318
- finalizeCallbacks.push(() => {
319
- if (ref.expectedType) {
320
- // might be set by other plugins, in that case respect it
316
+
317
+ actions.push(async () => {
318
+ await inlineReference.urlInfo.cook();
319
+ mutations.push(() => {
320
+ if (hotAccept) {
321
+ removeHtmlNodeText(node);
322
+ setHtmlNodeAttributes(node, {
323
+ "jsenv-cooked-by": "jsenv:html_inline_content_analysis",
324
+ });
321
325
  } else {
322
- ref.expectedType = decideLinkExpectedType(ref, urlInfo);
326
+ setHtmlNodeText(node, inlineReference.urlInfo.content, {
327
+ indentation: false, // indentation would decrease stack trace precision
328
+ });
329
+ setHtmlNodeAttributes(node, {
330
+ "jsenv-cooked-by": "jsenv:html_inline_content_analysis",
331
+ });
323
332
  }
324
333
  });
334
+ });
335
+ return inlineReference;
336
+ };
337
+ const visitTextContent = (
338
+ node,
339
+ { type, subtype, expectedType, contentType },
340
+ ) => {
341
+ const inlineContent = getHtmlNodeText(node);
342
+ if (!inlineContent) {
343
+ return null;
325
344
  }
326
- },
327
- style: inlineContent
328
- ? (styleNode) => {
329
- visitTextContent(styleNode, {
330
- type: "style",
331
- expectedType: "css",
332
- contentType: "text/css",
333
- });
334
- }
335
- : null,
336
- script: (scriptNode) => {
337
- const { type, subtype, contentType, extension } =
338
- analyzeScriptNode(scriptNode);
339
- if (type === "text") {
340
- // ignore <script type="whatever">foobar</script>
341
- // per HTML spec https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type
342
- return;
343
- }
344
- if (type === "importmap") {
345
- importmapFound = true;
345
+ return createInlineReference(node, inlineContent, {
346
+ type,
347
+ subtype,
348
+ expectedType,
349
+ contentType,
350
+ });
351
+ };
346
352
 
347
- const src = getHtmlNodeAttribute(scriptNode, "src");
348
- if (src) {
349
- // Browser would throw on remote importmap
350
- // and won't sent a request to the server for it
351
- // We must precook the importmap to know its content and inline it into the HTML
352
- const importmapReference = createExternalReference(
353
- scriptNode,
354
- "src",
355
- src,
356
- {
357
- type: "script",
358
- subtype: "importmap",
359
- expectedType: "importmap",
360
- },
361
- );
362
- const { line, column, isOriginal } = getHtmlNodePosition(
363
- scriptNode,
364
- {
365
- preferOriginal: true,
366
- },
367
- );
368
- const importmapInlineUrl = getUrlForContentInsideHtml(
369
- scriptNode,
370
- {
371
- htmlUrl: urlInfo.url,
372
- },
373
- );
374
- const importmapReferenceInlined = importmapReference.inline({
375
- line: line - 1,
376
- column,
377
- isOriginal,
378
- specifier: importmapInlineUrl,
379
- contentType: "application/importmap+json",
353
+ visitNonIgnoredHtmlNode(htmlAst, {
354
+ link: (linkNode) => {
355
+ const rel = getHtmlNodeAttribute(linkNode, "rel");
356
+ const type = getHtmlNodeAttribute(linkNode, "type");
357
+ const ref = visitHref(linkNode, {
358
+ type: "link_href",
359
+ subtype: rel,
360
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload#including_a_mime_type
361
+ expectedContentType: type,
362
+ });
363
+ if (ref) {
364
+ finalizeCallbacks.push(() => {
365
+ if (ref.expectedType) {
366
+ // might be set by other plugins, in that case respect it
367
+ } else {
368
+ ref.expectedType = decideLinkExpectedType(ref, urlInfo);
369
+ }
380
370
  });
381
- const importmapInlineUrlInfo =
382
- importmapReferenceInlined.urlInfo;
383
- actions.push(async () => {
384
- await importmapInlineUrlInfo.cook();
385
- importmapLoaded(importmapInlineUrlInfo);
386
- mutations.push(() => {
387
- setHtmlNodeText(
388
- scriptNode,
389
- importmapInlineUrlInfo.content,
390
- {
391
- indentation: "auto",
392
- },
393
- );
394
- setHtmlNodeAttributes(scriptNode, {
395
- "src": undefined,
396
- "jsenv-inlined-by": "jsenv:html_reference_analysis",
397
- "inlined-from-src": src,
398
- });
371
+ }
372
+ },
373
+ style: inlineContent
374
+ ? (styleNode) => {
375
+ visitTextContent(styleNode, {
376
+ type: "style",
377
+ expectedType: "css",
378
+ contentType: "text/css",
399
379
  });
400
- });
401
- } else {
402
- const htmlNodeText = getHtmlNodeText(scriptNode);
403
- if (htmlNodeText) {
404
- const importmapReference = createInlineReference(
380
+ }
381
+ : null,
382
+ script: (scriptNode) => {
383
+ const { type, subtype, contentType, extension } =
384
+ analyzeScriptNode(scriptNode);
385
+ if (type === "text") {
386
+ // ignore <script type="whatever">foobar</script>
387
+ // per HTML spec https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type
388
+ return;
389
+ }
390
+ if (type === "importmap") {
391
+ importmapFound = true;
392
+
393
+ const src = getHtmlNodeAttribute(scriptNode, "src");
394
+ if (src) {
395
+ // Browser would throw on remote importmap
396
+ // and won't sent a request to the server for it
397
+ // We must precook the importmap to know its content and inline it into the HTML
398
+ const importmapReference = createExternalReference(
405
399
  scriptNode,
406
- htmlNodeText,
400
+ "src",
401
+ src,
407
402
  {
408
403
  type: "script",
404
+ subtype: "importmap",
409
405
  expectedType: "importmap",
410
- contentType: "application/importmap+json",
411
406
  },
412
407
  );
413
- const inlineImportmapUrlInfo = importmapReference.urlInfo;
408
+ const { line, column, isOriginal } = getHtmlNodePosition(
409
+ scriptNode,
410
+ {
411
+ preferOriginal: true,
412
+ },
413
+ );
414
+ const importmapInlineUrl = getUrlForContentInsideHtml(
415
+ scriptNode,
416
+ {
417
+ htmlUrl: urlInfo.url,
418
+ },
419
+ );
420
+ const importmapReferenceInlined = importmapReference.inline({
421
+ line: line - 1,
422
+ column,
423
+ isOriginal,
424
+ specifier: importmapInlineUrl,
425
+ contentType: "application/importmap+json",
426
+ });
427
+ const importmapInlineUrlInfo =
428
+ importmapReferenceInlined.urlInfo;
414
429
  actions.push(async () => {
415
- await inlineImportmapUrlInfo.cook();
416
- importmapLoaded(inlineImportmapUrlInfo);
430
+ try {
431
+ await importmapInlineUrlInfo.cook();
432
+ } finally {
433
+ importmapLoaded(importmapInlineUrlInfo);
434
+ }
417
435
  mutations.push(() => {
418
436
  setHtmlNodeText(
419
437
  scriptNode,
420
- inlineImportmapUrlInfo.content,
438
+ importmapInlineUrlInfo.content,
421
439
  {
422
440
  indentation: "auto",
423
441
  },
424
442
  );
425
443
  setHtmlNodeAttributes(scriptNode, {
426
- "jsenv-cooked-by": "jsenv:html_reference_analysis",
444
+ "src": undefined,
445
+ "jsenv-inlined-by": "jsenv:html_reference_analysis",
446
+ "inlined-from-src": src,
427
447
  });
428
448
  });
429
449
  });
450
+ } else {
451
+ const htmlNodeText = getHtmlNodeText(scriptNode);
452
+ if (htmlNodeText) {
453
+ const importmapReference = createInlineReference(
454
+ scriptNode,
455
+ htmlNodeText,
456
+ {
457
+ type: "script",
458
+ expectedType: "importmap",
459
+ contentType: "application/importmap+json",
460
+ },
461
+ );
462
+ const inlineImportmapUrlInfo = importmapReference.urlInfo;
463
+ actions.push(async () => {
464
+ try {
465
+ await inlineImportmapUrlInfo.cook();
466
+ } finally {
467
+ importmapLoaded(inlineImportmapUrlInfo);
468
+ }
469
+ mutations.push(() => {
470
+ setHtmlNodeText(
471
+ scriptNode,
472
+ inlineImportmapUrlInfo.content,
473
+ {
474
+ indentation: "auto",
475
+ },
476
+ );
477
+ setHtmlNodeAttributes(scriptNode, {
478
+ "jsenv-cooked-by": "jsenv:html_reference_analysis",
479
+ });
480
+ });
481
+ });
482
+ }
430
483
  }
484
+ // once this plugin knows the importmap, it will use it
485
+ // to map imports. These import specifiers will be normalized
486
+ // by "formatReference" making the importmap presence useless.
487
+ // In dev/test we keep importmap into the HTML to see it even if useless
488
+ // Duing build we get rid of it
489
+ if (urlInfo.context.build) {
490
+ mutations.push(() => {
491
+ removeHtmlNode(scriptNode);
492
+ });
493
+ }
494
+ return;
431
495
  }
432
- // once this plugin knows the importmap, it will use it
433
- // to map imports. These import specifiers will be normalized
434
- // by "formatReference" making the importmap presence useless.
435
- // In dev/test we keep importmap into the HTML to see it even if useless
436
- // Duing build we get rid of it
437
- if (urlInfo.context.build) {
438
- mutations.push(() => {
439
- removeHtmlNode(scriptNode);
440
- });
496
+ const externalRef = visitSrc(scriptNode, {
497
+ type: "script",
498
+ subtype: type,
499
+ expectedType: type,
500
+ });
501
+ if (externalRef) {
502
+ return;
441
503
  }
442
- return;
443
- }
444
- const externalRef = visitSrc(scriptNode, {
445
- type: "script",
446
- subtype: type,
447
- expectedType: type,
448
- });
449
- if (externalRef) {
450
- return;
451
- }
452
504
 
453
- // now visit the content, if any
454
- if (!inlineContent) {
455
- return;
456
- }
457
- // If the inline script was already handled by an other plugin, ignore it
458
- // - we want to preserve inline scripts generated by html supervisor during dev
459
- // - we want to avoid cooking twice a script during build
460
- if (
461
- !inlineConvertedScript &&
462
- getHtmlNodeAttribute(scriptNode, "jsenv-injected-by") ===
463
- "jsenv:js_module_fallback"
464
- ) {
465
- return;
466
- }
505
+ // now visit the content, if any
506
+ if (!inlineContent) {
507
+ return;
508
+ }
509
+ // If the inline script was already handled by an other plugin, ignore it
510
+ // - we want to preserve inline scripts generated by html supervisor during dev
511
+ // - we want to avoid cooking twice a script during build
512
+ if (
513
+ !inlineConvertedScript &&
514
+ getHtmlNodeAttribute(scriptNode, "jsenv-injected-by") ===
515
+ "jsenv:js_module_fallback"
516
+ ) {
517
+ return;
518
+ }
467
519
 
468
- const inlineRef = visitTextContent(scriptNode, {
469
- type: "script",
470
- subtype,
471
- expectedType: type,
472
- contentType,
473
- });
474
- if (inlineRef) {
475
- // 1. <script type="jsx"> becomes <script>
476
- if (type === "js_classic" && extension !== ".js") {
477
- mutations.push(() => {
478
- setHtmlNodeAttributes(scriptNode, {
479
- type: undefined,
520
+ const inlineRef = visitTextContent(scriptNode, {
521
+ type: "script",
522
+ subtype,
523
+ expectedType: type,
524
+ contentType,
525
+ });
526
+ if (inlineRef) {
527
+ // 1. <script type="jsx"> becomes <script>
528
+ if (type === "js_classic" && extension !== ".js") {
529
+ mutations.push(() => {
530
+ setHtmlNodeAttributes(scriptNode, {
531
+ type: undefined,
532
+ });
480
533
  });
481
- });
482
- }
483
- // 2. <script type="module/jsx"> becomes <script type="module">
484
- if (type === "js_module" && extension !== ".js") {
485
- mutations.push(() => {
486
- setHtmlNodeAttributes(scriptNode, {
487
- type: "module",
534
+ }
535
+ // 2. <script type="module/jsx"> becomes <script type="module">
536
+ if (type === "js_module" && extension !== ".js") {
537
+ mutations.push(() => {
538
+ setHtmlNodeAttributes(scriptNode, {
539
+ type: "module",
540
+ });
488
541
  });
489
- });
542
+ }
490
543
  }
491
- }
492
- },
493
- a: (aNode) => {
494
- visitHref(aNode, {
495
- type: "a_href",
496
- });
497
- },
498
- iframe: (iframeNode) => {
499
- visitSrc(iframeNode, {
500
- type: "iframe_src",
501
- });
502
- },
503
- img: (imgNode) => {
504
- visitSrc(imgNode, {
505
- type: "img_src",
506
- });
507
- visitSrcset(imgNode, {
508
- type: "img_srcset",
509
- });
510
- },
511
- source: (sourceNode) => {
512
- visitSrc(sourceNode, {
513
- type: "source_src",
514
- });
515
- visitSrcset(sourceNode, {
516
- type: "source_srcset",
517
- });
518
- },
519
- // svg <image> tag
520
- image: (imageNode) => {
521
- visitHref(imageNode, {
522
- type: "image_href",
523
- });
524
- },
525
- use: (useNode) => {
526
- visitHref(useNode, {
527
- type: "use_href",
528
- });
529
- },
530
- });
531
- if (!importmapFound) {
532
- importmapLoaded();
533
- }
534
- finalizeCallbacks.forEach((finalizeCallback) => {
535
- finalizeCallback();
536
- });
544
+ },
545
+ a: (aNode) => {
546
+ visitHref(aNode, {
547
+ type: "a_href",
548
+ });
549
+ },
550
+ iframe: (iframeNode) => {
551
+ visitSrc(iframeNode, {
552
+ type: "iframe_src",
553
+ });
554
+ },
555
+ img: (imgNode) => {
556
+ visitSrc(imgNode, {
557
+ type: "img_src",
558
+ });
559
+ visitSrcset(imgNode, {
560
+ type: "img_srcset",
561
+ });
562
+ },
563
+ source: (sourceNode) => {
564
+ visitSrc(sourceNode, {
565
+ type: "source_src",
566
+ });
567
+ visitSrcset(sourceNode, {
568
+ type: "source_srcset",
569
+ });
570
+ },
571
+ // svg <image> tag
572
+ image: (imageNode) => {
573
+ visitHref(imageNode, {
574
+ type: "image_href",
575
+ });
576
+ },
577
+ use: (useNode) => {
578
+ visitHref(useNode, {
579
+ type: "use_href",
580
+ });
581
+ },
582
+ });
583
+ if (!importmapFound) {
584
+ importmapLoaded();
585
+ }
586
+ finalizeCallbacks.forEach((finalizeCallback) => {
587
+ finalizeCallback();
588
+ });
537
589
 
538
- if (actions.length > 0) {
539
- await Promise.all(actions.map((action) => action()));
540
- actions.length = 0;
541
- }
542
- if (mutations.length === 0) {
543
- return null;
590
+ if (actions.length > 0) {
591
+ await Promise.all(actions.map((action) => action()));
592
+ actions.length = 0;
593
+ }
594
+ if (mutations.length === 0) {
595
+ return null;
596
+ }
597
+ mutations.forEach((mutation) => mutation());
598
+ mutations.length = 0;
599
+ return stringifyHtmlAst(htmlAst);
600
+ } catch (e) {
601
+ importmapLoaded();
602
+ throw e;
544
603
  }
545
- mutations.forEach((mutation) => mutation());
546
- mutations.length = 0;
547
- return stringifyHtmlAst(htmlAst);
548
604
  },
549
605
  },
550
606
  };
551
607
  };
552
608
 
609
+ const visitNonIgnoredHtmlNode = (htmlAst, visitors) => {
610
+ const visitorsInstrumented = {};
611
+ for (const key of Object.keys(visitors)) {
612
+ visitorsInstrumented[key] = (node) => {
613
+ const jsenvIgnoreAttribute = getHtmlNodeAttribute(node, "jsenv-ignore");
614
+ if (jsenvIgnoreAttribute !== undefined) {
615
+ return;
616
+ }
617
+ visitors[key](node);
618
+ };
619
+ }
620
+ visitHtmlNodes(htmlAst, visitorsInstrumented);
621
+ };
622
+
623
+ const generateHtmlForSyntaxError = (
624
+ htmlSyntaxError,
625
+ { htmlUrl, rootDirectoryUrl, htmlErrorContentFrame },
626
+ ) => {
627
+ const htmlForSyntaxError = String(readFileSync(htmlSyntaxErrorFileUrl));
628
+ const htmlRelativeUrl = urlToRelativeUrl(htmlUrl, rootDirectoryUrl);
629
+ const { line, column } = htmlSyntaxError;
630
+ const urlWithLineAndColumn = `${htmlUrl}:${line}:${column}`;
631
+ const replacers = {
632
+ fileRelativeUrl: htmlRelativeUrl,
633
+ reasonCode: htmlSyntaxError.reasonCode,
634
+ errorLinkHref: `javascript:window.fetch('/__open_in_editor__/${encodeURIComponent(
635
+ urlWithLineAndColumn,
636
+ )}')`,
637
+ errorLinkText: `${htmlRelativeUrl}:${line}:${column}`,
638
+ syntaxError: escapeHtml(htmlErrorContentFrame),
639
+ };
640
+ const html = replacePlaceholders(htmlForSyntaxError, replacers);
641
+ return html;
642
+ };
643
+ const escapeHtml = (string) => {
644
+ return string
645
+ .replace(/&/g, "&amp;")
646
+ .replace(/</g, "&lt;")
647
+ .replace(/>/g, "&gt;")
648
+ .replace(/"/g, "&quot;")
649
+ .replace(/'/g, "&#039;");
650
+ };
651
+ const replacePlaceholders = (html, replacers) => {
652
+ return html.replace(/\${([\w]+)}/g, (match, name) => {
653
+ const replacer = replacers[name];
654
+ if (replacer === undefined) {
655
+ return match;
656
+ }
657
+ if (typeof replacer === "function") {
658
+ return replacer();
659
+ }
660
+ return replacer;
661
+ });
662
+ };
663
+
553
664
  const crossOriginCompatibleTagNames = ["script", "link", "img", "source"];
554
665
  const integrityCompatibleTagNames = ["script", "link", "img", "source"];
555
666
  const readFetchMetas = (node) => {