@jsenv/core 39.0.4 → 39.1.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.
@@ -1,10 +1,8 @@
1
- import { urlHotMetas } from "../../import_meta_hot/client/import_meta_hot.js";
2
- import { compareTwoUrlPaths } from "./url_helpers.js";
3
1
  import {
4
- reloadHtmlPage,
5
- reloadJsImport,
6
- getDOMNodesUsingUrl,
7
- } from "./reload.js";
2
+ parseSrcSet,
3
+ stringifySrcSet,
4
+ } from "@jsenv/ast/src/html/html_src_set.js";
5
+ import { urlHotMetas } from "../../import_meta_hot/client/import_meta_hot.js";
8
6
 
9
7
  export const initAutoreload = ({ mainFilePath }) => {
10
8
  let debug = false;
@@ -229,3 +227,154 @@ This could be due to syntax errors or importing non-existent modules (see errors
229
227
  },
230
228
  });
231
229
  };
230
+
231
+ const reloadHtmlPage = () => {
232
+ window.location.reload(true);
233
+ };
234
+ // This function can consider everything as hot reloadable:
235
+ // - no need to check [hot-accept]and [hot-decline] attributes for instance
236
+ // This is because if something should full reload, we receive "full_reload"
237
+ // from server and this function is not called
238
+ const getDOMNodesUsingUrl = (urlToReload) => {
239
+ const nodes = [];
240
+ const shouldReloadUrl = (urlCandidate) => {
241
+ return compareTwoUrlPaths(urlCandidate, urlToReload);
242
+ };
243
+ const visitNodeAttributeAsUrl = (node, attributeName) => {
244
+ let attribute = node[attributeName];
245
+ if (!attribute) {
246
+ return;
247
+ }
248
+ if (SVGAnimatedString && attribute instanceof SVGAnimatedString) {
249
+ attribute = attribute.animVal;
250
+ }
251
+ if (!shouldReloadUrl(attribute)) {
252
+ return;
253
+ }
254
+ nodes.push({
255
+ node,
256
+ reload: (hot) => {
257
+ if (node.nodeName === "SCRIPT") {
258
+ const copy = document.createElement("script");
259
+ Array.from(node.attributes).forEach((attribute) => {
260
+ copy.setAttribute(attribute.nodeName, attribute.nodeValue);
261
+ });
262
+ copy.src = injectQuery(node.src, { hot });
263
+ if (node.parentNode) {
264
+ node.parentNode.replaceChild(copy, node);
265
+ } else {
266
+ document.body.appendChild(copy);
267
+ }
268
+ } else {
269
+ node[attributeName] = injectQuery(attribute, { hot });
270
+ }
271
+ },
272
+ });
273
+ };
274
+ Array.from(document.querySelectorAll(`link[rel="stylesheet"]`)).forEach(
275
+ (link) => {
276
+ visitNodeAttributeAsUrl(link, "href");
277
+ },
278
+ );
279
+ Array.from(document.querySelectorAll(`link[rel="icon"]`)).forEach((link) => {
280
+ visitNodeAttributeAsUrl(link, "href");
281
+ });
282
+ Array.from(document.querySelectorAll("script")).forEach((script) => {
283
+ visitNodeAttributeAsUrl(script, "src");
284
+ const inlinedFromSrc = script.getAttribute("inlined-from-src");
285
+ if (inlinedFromSrc) {
286
+ const inlinedFromUrl = new URL(inlinedFromSrc, window.location.origin)
287
+ .href;
288
+ if (shouldReloadUrl(inlinedFromUrl)) {
289
+ nodes.push({
290
+ node: script,
291
+ reload: () =>
292
+ window.__supervisor__.reloadSupervisedScript(inlinedFromSrc),
293
+ });
294
+ }
295
+ }
296
+ });
297
+ // There is no real need to update a.href because the resource will be fetched when clicked.
298
+ // But in a scenario where the resource was already visited and is in browser cache, adding
299
+ // the dynamic query param ensure the cache is invalidated
300
+ Array.from(document.querySelectorAll("a")).forEach((a) => {
301
+ visitNodeAttributeAsUrl(a, "href");
302
+ });
303
+ // About iframes:
304
+ // - By default iframe itself and everything inside trigger a parent page full-reload
305
+ // - Adding [hot-accept] on the iframe means parent page won't reload when iframe full/hot reload
306
+ // In that case and if there is code in the iframe and parent doing post message communication:
307
+ // you must put import.meta.hot.decline() for code involved in communication.
308
+ // (both in parent and iframe)
309
+ Array.from(document.querySelectorAll("img")).forEach((img) => {
310
+ visitNodeAttributeAsUrl(img, "src");
311
+ const srcset = img.srcset;
312
+ if (srcset) {
313
+ nodes.push({
314
+ node: img,
315
+ reload: (hot) => {
316
+ const srcCandidates = parseSrcSet(srcset);
317
+ srcCandidates.forEach((srcCandidate) => {
318
+ const url = new URL(
319
+ srcCandidate.specifier,
320
+ `${window.location.href}`,
321
+ );
322
+ if (shouldReloadUrl(url)) {
323
+ srcCandidate.specifier = injectQuery(url, { hot });
324
+ }
325
+ });
326
+ img.srcset = stringifySrcSet(srcCandidates);
327
+ },
328
+ });
329
+ }
330
+ });
331
+ Array.from(document.querySelectorAll("source")).forEach((source) => {
332
+ visitNodeAttributeAsUrl(source, "src");
333
+ });
334
+ // svg image tag
335
+ Array.from(document.querySelectorAll("image")).forEach((image) => {
336
+ visitNodeAttributeAsUrl(image, "href");
337
+ });
338
+ // svg use
339
+ Array.from(document.querySelectorAll("use")).forEach((use) => {
340
+ visitNodeAttributeAsUrl(use, "href");
341
+ });
342
+ return nodes;
343
+ };
344
+ const reloadJsImport = async (url, hot) => {
345
+ const urlWithHotSearchParam = injectQuery(url, { hot });
346
+ const namespace = await import(urlWithHotSearchParam);
347
+ return namespace;
348
+ };
349
+ // const reloadAllCss = () => {
350
+ // const links = Array.from(document.getElementsByTagName("link"));
351
+ // links.forEach((link) => {
352
+ // if (link.rel === "stylesheet") {
353
+ // link.href = injectQuery(link.href, { hot: Date.now() });
354
+ // }
355
+ // });
356
+ // };
357
+
358
+ const compareTwoUrlPaths = (url, otherUrl) => {
359
+ if (url === otherUrl) {
360
+ return true;
361
+ }
362
+ const urlObject = new URL(url);
363
+ const otherUrlObject = new URL(otherUrl);
364
+ if (urlObject.origin !== otherUrlObject.origin) {
365
+ return false;
366
+ }
367
+ if (urlObject.pathname !== otherUrlObject.pathname) {
368
+ return false;
369
+ }
370
+ return true;
371
+ };
372
+
373
+ const injectQuery = (url, query) => {
374
+ const urlObject = new URL(url);
375
+ const { searchParams } = urlObject;
376
+ Object.keys(query).forEach((key) => {
377
+ searchParams.set(key, query[key]);
378
+ });
379
+ return String(urlObject);
380
+ };
@@ -1,9 +1,4 @@
1
- import {
2
- parseHtml,
3
- stringifyHtmlAst,
4
- injectHtmlNodeAsEarlyAsPossible,
5
- createHtmlNode,
6
- } from "@jsenv/ast";
1
+ import { parseHtml, injectJsenvScript, stringifyHtmlAst } from "@jsenv/ast";
7
2
 
8
3
  export const jsenvPluginAutoreloadClient = () => {
9
4
  const autoreloadClientFileUrl = new URL(
@@ -21,29 +16,22 @@ export const jsenvPluginAutoreloadClient = () => {
21
16
  url: htmlUrlInfo.url,
22
17
  });
23
18
  const autoreloadClientReference = htmlUrlInfo.dependencies.inject({
24
- type: "script",
19
+ type: "js_import",
25
20
  subtype: "js_module",
26
21
  expectedType: "js_module",
27
22
  specifier: autoreloadClientFileUrl,
28
23
  });
29
- const paramsJson = JSON.stringify(
30
- {
31
- mainFilePath: `/${htmlUrlInfo.kitchen.context.mainFilePath}`,
24
+ injectJsenvScript(htmlAst, {
25
+ type: "module",
26
+ src: autoreloadClientReference.generatedSpecifier,
27
+ initCall: {
28
+ callee: "initAutoreload",
29
+ params: {
30
+ mainFilePath: `/${htmlUrlInfo.kitchen.context.mainFilePath}`,
31
+ },
32
32
  },
33
- null,
34
- " ",
35
- );
36
- injectHtmlNodeAsEarlyAsPossible(
37
- htmlAst,
38
- createHtmlNode({
39
- tagName: "script",
40
- type: "module",
41
- textContent: `import { initAutoreload } from "${autoreloadClientReference.generatedSpecifier}";
42
-
43
- initAutoreload(${paramsJson});`,
44
- }),
45
- "jsenv:autoreload_client",
46
- );
33
+ pluginName: "jsenv:autoreload_client",
34
+ });
47
35
  const htmlModified = stringifyHtmlAst(htmlAst);
48
36
  return {
49
37
  content: htmlModified,
@@ -10,9 +10,10 @@ export const jsenvPluginCleanHTML = () => {
10
10
  html: urlInfo.content,
11
11
  url: urlInfo.url,
12
12
  });
13
- return stringifyHtmlAst(htmlAst, {
13
+ const htmlClean = stringifyHtmlAst(htmlAst, {
14
14
  cleanupPositionAttributes: true,
15
15
  });
16
+ return htmlClean;
16
17
  },
17
18
  },
18
19
  };
@@ -0,0 +1,93 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { urlToRelativeUrl } from "@jsenv/urls";
3
+ import { parseHtml } from "@jsenv/ast";
4
+
5
+ import { generateContentFrame } from "@jsenv/humanize";
6
+
7
+ export const jsenvPluginHtmlSyntaxErrorFallback = () => {
8
+ const htmlSyntaxErrorFileUrl = new URL(
9
+ "./client/html_syntax_error.html",
10
+ import.meta.url,
11
+ );
12
+
13
+ return {
14
+ mustStayFirst: true,
15
+ name: "jsenv:html_syntax_error_fallback",
16
+ appliesDuring: "dev",
17
+ transformUrlContent: {
18
+ html: (urlInfo) => {
19
+ try {
20
+ parseHtml({
21
+ html: urlInfo.content,
22
+ url: urlInfo.url,
23
+ });
24
+ return null;
25
+ } catch (e) {
26
+ if (e.code !== "PARSE_ERROR") {
27
+ return null;
28
+ }
29
+ const line = e.line;
30
+ const column = e.column;
31
+ const htmlErrorContentFrame = generateContentFrame({
32
+ content: urlInfo.content,
33
+ line,
34
+ column,
35
+ });
36
+ urlInfo.kitchen.context.logger
37
+ .error(`Error while handling ${urlInfo.context.request ? urlInfo.context.request.url : urlInfo.url}:
38
+ ${e.reasonCode}
39
+ ${urlInfo.url}:${line}:${column}
40
+ ${htmlErrorContentFrame}`);
41
+ const html = generateHtmlForSyntaxError(e, {
42
+ htmlUrl: urlInfo.url,
43
+ rootDirectoryUrl: urlInfo.context.rootDirectoryUrl,
44
+ htmlErrorContentFrame,
45
+ htmlSyntaxErrorFileUrl,
46
+ });
47
+ return html;
48
+ }
49
+ },
50
+ },
51
+ };
52
+ };
53
+
54
+ const generateHtmlForSyntaxError = (
55
+ htmlSyntaxError,
56
+ { htmlUrl, rootDirectoryUrl, htmlErrorContentFrame, htmlSyntaxErrorFileUrl },
57
+ ) => {
58
+ const htmlForSyntaxError = String(readFileSync(htmlSyntaxErrorFileUrl));
59
+ const htmlRelativeUrl = urlToRelativeUrl(htmlUrl, rootDirectoryUrl);
60
+ const { line, column } = htmlSyntaxError;
61
+ const urlWithLineAndColumn = `${htmlUrl}:${line}:${column}`;
62
+ const replacers = {
63
+ fileRelativeUrl: htmlRelativeUrl,
64
+ reasonCode: htmlSyntaxError.reasonCode,
65
+ errorLinkHref: `javascript:window.fetch('/__open_in_editor__/${encodeURIComponent(
66
+ urlWithLineAndColumn,
67
+ )}')`,
68
+ errorLinkText: `${htmlRelativeUrl}:${line}:${column}`,
69
+ syntaxError: escapeHtml(htmlErrorContentFrame),
70
+ };
71
+ const html = replacePlaceholders(htmlForSyntaxError, replacers);
72
+ return html;
73
+ };
74
+ const escapeHtml = (string) => {
75
+ return string
76
+ .replace(/&/g, "&")
77
+ .replace(/</g, "&lt;")
78
+ .replace(/>/g, "&gt;")
79
+ .replace(/"/g, "&quot;")
80
+ .replace(/'/g, "&#039;");
81
+ };
82
+ const replacePlaceholders = (html, replacers) => {
83
+ return html.replace(/\$\{(\w+)\}/g, (match, name) => {
84
+ const replacer = replacers[name];
85
+ if (replacer === undefined) {
86
+ return match;
87
+ }
88
+ if (typeof replacer === "function") {
89
+ return replacer();
90
+ }
91
+ return replacer;
92
+ });
93
+ };
@@ -1,10 +1,5 @@
1
1
  import { createMagicSource } from "@jsenv/sourcemap";
2
- import {
3
- parseHtml,
4
- injectHtmlNodeAsEarlyAsPossible,
5
- createHtmlNode,
6
- stringifyHtmlAst,
7
- } from "@jsenv/ast";
2
+ import { parseHtml, injectJsenvScript, stringifyHtmlAst } from "@jsenv/ast";
8
3
 
9
4
  export const injectGlobals = (content, globals, urlInfo) => {
10
5
  if (urlInfo.type === "html") {
@@ -28,14 +23,10 @@ const globalInjectorOnHtml = (content, globals, urlInfo) => {
28
23
  const clientCode = generateClientCodeForGlobals(globals, {
29
24
  isWebWorker: false,
30
25
  });
31
- injectHtmlNodeAsEarlyAsPossible(
32
- htmlAst,
33
- createHtmlNode({
34
- tagName: "script",
35
- textContent: clientCode,
36
- }),
37
- "jsenv:inject_globals",
38
- );
26
+ injectJsenvScript(htmlAst, {
27
+ content: clientCode,
28
+ pluginName: "jsenv:inject_globals",
29
+ });
39
30
  return stringifyHtmlAst(htmlAst);
40
31
  };
41
32
 
@@ -1,10 +1,4 @@
1
1
  import { performance } from "node:perf_hooks";
2
- import {
3
- parseHtml,
4
- stringifyHtmlAst,
5
- injectHtmlNodeAsEarlyAsPossible,
6
- createHtmlNode,
7
- } from "@jsenv/ast";
8
2
 
9
3
  const HOOK_NAMES = [
10
4
  "init",
@@ -81,7 +75,8 @@ export const createPluginController = (
81
75
  key === "name" ||
82
76
  key === "appliesDuring" ||
83
77
  key === "init" ||
84
- key === "serverEvents"
78
+ key === "serverEvents" ||
79
+ key === "mustStayFirst"
85
80
  ) {
86
81
  continue;
87
82
  }
@@ -100,7 +95,15 @@ export const createPluginController = (
100
95
  value: hookValue,
101
96
  };
102
97
  if (position === "start") {
103
- group.unshift(hook);
98
+ let i = 0;
99
+ while (i < group.length) {
100
+ const before = group[i];
101
+ if (!before.plugin.mustStayFirst) {
102
+ break;
103
+ }
104
+ i++;
105
+ }
106
+ group.splice(i, 0, hook);
104
107
  } else {
105
108
  group.push(hook);
106
109
  }
@@ -225,11 +228,11 @@ export const createPluginController = (
225
228
  }
226
229
  }
227
230
  };
228
- const callAsyncHooks = async (hookName, info, callback) => {
231
+ const callAsyncHooks = async (hookName, info, callback, options) => {
229
232
  const hooks = hookGroups[hookName];
230
233
  if (hooks) {
231
234
  for (const hook of hooks) {
232
- const returnValue = await callAsyncHook(hook, info);
235
+ const returnValue = await callAsyncHook(hook, info, options);
233
236
  if (returnValue && callback) {
234
237
  await callback(returnValue, hook.plugin);
235
238
  }
@@ -249,7 +252,7 @@ export const createPluginController = (
249
252
  }
250
253
  return null;
251
254
  };
252
- const callAsyncHooksUntil = (hookName, info) => {
255
+ const callAsyncHooksUntil = async (hookName, info, options) => {
253
256
  const hooks = hookGroups[hookName];
254
257
  if (!hooks) {
255
258
  return null;
@@ -257,22 +260,23 @@ export const createPluginController = (
257
260
  if (hooks.length === 0) {
258
261
  return null;
259
262
  }
260
- return new Promise((resolve, reject) => {
261
- const visit = (index) => {
262
- if (index >= hooks.length) {
263
- return resolve();
264
- }
265
- const hook = hooks[index];
266
- const returnValue = callAsyncHook(hook, info);
267
- return Promise.resolve(returnValue).then((output) => {
268
- if (output) {
269
- return resolve(output);
270
- }
271
- return visit(index + 1);
272
- }, reject);
273
- };
274
- visit(0);
275
- });
263
+ let result;
264
+ let index = 0;
265
+ const visit = async () => {
266
+ if (index >= hooks.length) {
267
+ return;
268
+ }
269
+ const hook = hooks[index];
270
+ const returnValue = await callAsyncHook(hook, info, options);
271
+ if (returnValue) {
272
+ result = returnValue;
273
+ return;
274
+ }
275
+ index++;
276
+ await visit();
277
+ };
278
+ await visit(0);
279
+ return result;
276
280
  };
277
281
 
278
282
  return {
@@ -320,11 +324,7 @@ const assertAndNormalizeReturnValue = (hook, returnValue, info) => {
320
324
  if (!returnValueAssertion.appliesTo.includes(hook.name)) {
321
325
  continue;
322
326
  }
323
- const assertionResult = returnValueAssertion.assertion(
324
- returnValue,
325
- info,
326
- hook,
327
- );
327
+ const assertionResult = returnValueAssertion.assertion(returnValue, info);
328
328
  if (assertionResult !== undefined) {
329
329
  // normalization
330
330
  returnValue = assertionResult;
@@ -358,7 +358,7 @@ const returnValueAssertions = [
358
358
  "finalizeUrlContent",
359
359
  "optimizeUrlContent",
360
360
  ],
361
- assertion: (valueReturned, urlInfo, hook) => {
361
+ assertion: (valueReturned, urlInfo) => {
362
362
  if (typeof valueReturned === "string" || Buffer.isBuffer(valueReturned)) {
363
363
  return { content: valueReturned };
364
364
  }
@@ -367,12 +367,6 @@ const returnValueAssertions = [
367
367
  if (urlInfo.url.startsWith("ignore:")) {
368
368
  return undefined;
369
369
  }
370
- if (urlInfo.type === "html") {
371
- const { scriptInjections } = valueReturned;
372
- if (scriptInjections) {
373
- return applyScriptInjections(urlInfo, scriptInjections, hook);
374
- }
375
- }
376
370
  if (typeof content !== "string" && !Buffer.isBuffer(content) && !body) {
377
371
  throw new Error(
378
372
  `Unexpected "content" returned by plugin: it must be a string or a buffer; got ${content}`,
@@ -386,60 +380,3 @@ const returnValueAssertions = [
386
380
  },
387
381
  },
388
382
  ];
389
-
390
- const applyScriptInjections = (htmlUrlInfo, scriptInjections, hook) => {
391
- const htmlAst = parseHtml({
392
- html: htmlUrlInfo.content,
393
- url: htmlUrlInfo.url,
394
- });
395
-
396
- scriptInjections.reverse().forEach((scriptInjection) => {
397
- const { setup } = scriptInjection;
398
- if (setup) {
399
- const setupGlobalName = setup.name;
400
- const setupParamSource = stringifyParams(setup.param, " ");
401
- const inlineJs = `${setupGlobalName}({${setupParamSource}})`;
402
- injectHtmlNodeAsEarlyAsPossible(
403
- htmlAst,
404
- createHtmlNode({
405
- tagName: "script",
406
- textContent: inlineJs,
407
- }),
408
- hook.plugin.name,
409
- );
410
- }
411
- const scriptReference = htmlUrlInfo.dependencies.inject({
412
- type: "script",
413
- subtype: scriptInjection.type === "module" ? "js_module" : "js_classic",
414
- expectedType:
415
- scriptInjection.type === "module" ? "js_module" : "js_classic",
416
- specifier: scriptInjection.src,
417
- });
418
- injectHtmlNodeAsEarlyAsPossible(
419
- htmlAst,
420
- createHtmlNode({
421
- tagName: "script",
422
- ...(scriptInjection.type === "module" ? { type: "module" } : {}),
423
- src: scriptReference.generatedSpecifier,
424
- }),
425
- hook.plugin.name,
426
- );
427
- });
428
- const htmlModified = stringifyHtmlAst(htmlAst);
429
- return {
430
- content: htmlModified,
431
- };
432
- };
433
-
434
- const stringifyParams = (params, prefix = "") => {
435
- const source = JSON.stringify(params, null, prefix);
436
- if (prefix.length) {
437
- // remove leading "{\n"
438
- // remove leading prefix
439
- // remove trailing "\n}"
440
- return source.slice(2 + prefix.length, -2);
441
- }
442
- // remove leading "{"
443
- // remove trailing "}"
444
- return source.slice(1, -1);
445
- };
@@ -1,6 +1,3 @@
1
- import { readFileSync } from "node:fs";
2
- import { urlToRelativeUrl } from "@jsenv/urls";
3
- import { generateContentFrame } from "@jsenv/humanize";
4
1
  import {
5
2
  parseHtml,
6
3
  visitHtmlNodes,
@@ -23,11 +20,6 @@ import {
23
20
  normalizeImportMap,
24
21
  } from "@jsenv/importmap";
25
22
 
26
- const htmlSyntaxErrorFileUrl = new URL(
27
- "./html_syntax_error.html",
28
- import.meta.url,
29
- );
30
-
31
23
  export const jsenvPluginHtmlReferenceAnalysis = ({
32
24
  inlineContent,
33
25
  inlineConvertedScript,
@@ -135,41 +127,10 @@ export const jsenvPluginHtmlReferenceAnalysis = ({
135
127
  },
136
128
  html: async (urlInfo) => {
137
129
  let importmapFound = false;
138
-
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
- urlInfo.kitchen.context.logger
155
- .error(`Error while handling ${urlInfo.context.request ? urlInfo.context.request.url : urlInfo.url}:
156
- ${e.reasonCode}
157
- ${urlInfo.url}:${line}:${column}
158
- ${htmlErrorContentFrame}`);
159
- const html = generateHtmlForSyntaxError(e, {
160
- htmlUrl: urlInfo.url,
161
- rootDirectoryUrl: urlInfo.context.rootDirectoryUrl,
162
- htmlErrorContentFrame,
163
- });
164
- htmlAst = parseHtml({
165
- html,
166
- url: htmlSyntaxErrorFileUrl,
167
- });
168
- } else {
169
- throw e;
170
- }
171
- }
172
-
130
+ const htmlAst = parseHtml({
131
+ html: urlInfo.content,
132
+ url: urlInfo.url,
133
+ });
173
134
  const importmapLoaded = startLoadingImportmap(urlInfo);
174
135
 
175
136
  try {
@@ -621,47 +582,6 @@ const visitNonIgnoredHtmlNode = (htmlAst, visitors) => {
621
582
  visitHtmlNodes(htmlAst, visitorsInstrumented);
622
583
  };
623
584
 
624
- const generateHtmlForSyntaxError = (
625
- htmlSyntaxError,
626
- { htmlUrl, rootDirectoryUrl, htmlErrorContentFrame },
627
- ) => {
628
- const htmlForSyntaxError = String(readFileSync(htmlSyntaxErrorFileUrl));
629
- const htmlRelativeUrl = urlToRelativeUrl(htmlUrl, rootDirectoryUrl);
630
- const { line, column } = htmlSyntaxError;
631
- const urlWithLineAndColumn = `${htmlUrl}:${line}:${column}`;
632
- const replacers = {
633
- fileRelativeUrl: htmlRelativeUrl,
634
- reasonCode: htmlSyntaxError.reasonCode,
635
- errorLinkHref: `javascript:window.fetch('/__open_in_editor__/${encodeURIComponent(
636
- urlWithLineAndColumn,
637
- )}')`,
638
- errorLinkText: `${htmlRelativeUrl}:${line}:${column}`,
639
- syntaxError: escapeHtml(htmlErrorContentFrame),
640
- };
641
- const html = replacePlaceholders(htmlForSyntaxError, replacers);
642
- return html;
643
- };
644
- const escapeHtml = (string) => {
645
- return string
646
- .replace(/&/g, "&amp;")
647
- .replace(/</g, "&lt;")
648
- .replace(/>/g, "&gt;")
649
- .replace(/"/g, "&quot;")
650
- .replace(/'/g, "&#039;");
651
- };
652
- const replacePlaceholders = (html, replacers) => {
653
- return html.replace(/\$\{(\w+)\}/g, (match, name) => {
654
- const replacer = replacers[name];
655
- if (replacer === undefined) {
656
- return match;
657
- }
658
- if (typeof replacer === "function") {
659
- return replacer();
660
- }
661
- return replacer;
662
- });
663
- };
664
-
665
585
  const crossOriginCompatibleTagNames = ["script", "link", "img", "source"];
666
586
  const integrityCompatibleTagNames = ["script", "link", "img", "source"];
667
587
  const readFetchMetas = (node) => {