@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.
- package/dist/html/directory.html +18 -0
- package/dist/html/html_404_and_parent_dir.html +19 -0
- package/dist/html/html_404_and_parent_dir_is_empty.html +16 -0
- package/dist/html/html_syntax_error.html +14 -0
- package/dist/js/autoreload.js +135 -132
- package/dist/jsenv_core.js +708 -406
- package/package.json +6 -7
- package/src/dev/start_dev_server.js +9 -1
- package/src/kitchen/errors.js +8 -5
- package/src/kitchen/url_graph/references.js +7 -6
- package/src/plugins/autoreload/client/autoreload.js +138 -137
- package/src/plugins/autoreload/jsenv_plugin_autoreload_client.js +10 -1
- package/src/plugins/autoreload/jsenv_plugin_autoreload_server.js +4 -1
- package/src/plugins/protocol_file/directory.html +20 -0
- package/src/plugins/protocol_file/html_404_and_parent_dir.html +21 -0
- package/src/plugins/protocol_file/html_404_and_parent_dir_is_empty.html +18 -0
- package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +173 -13
- package/src/plugins/reference_analysis/html/html_syntax_error.html +14 -0
- package/src/plugins/reference_analysis/html/jsenv_plugin_html_reference_analysis.js +480 -369
- package/src/plugins/ribbon/jsenv_plugin_ribbon.js +1 -1
|
@@ -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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
168
|
+
throw e;
|
|
154
169
|
}
|
|
155
|
-
|
|
156
|
-
line,
|
|
157
|
-
column,
|
|
158
|
-
// originalLine, originalColumn
|
|
159
|
-
} = position;
|
|
160
|
-
const debug = getHtmlNodeAttribute(node, "jsenv-debug") !== undefined;
|
|
170
|
+
}
|
|
161
171
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
"
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
416
|
-
|
|
430
|
+
try {
|
|
431
|
+
await importmapInlineUrlInfo.cook();
|
|
432
|
+
} finally {
|
|
433
|
+
importmapLoaded(importmapInlineUrlInfo);
|
|
434
|
+
}
|
|
417
435
|
mutations.push(() => {
|
|
418
436
|
setHtmlNodeText(
|
|
419
437
|
scriptNode,
|
|
420
|
-
|
|
438
|
+
importmapInlineUrlInfo.content,
|
|
421
439
|
{
|
|
422
440
|
indentation: "auto",
|
|
423
441
|
},
|
|
424
442
|
);
|
|
425
443
|
setHtmlNodeAttributes(scriptNode, {
|
|
426
|
-
"
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (
|
|
438
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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, "&")
|
|
646
|
+
.replace(/</g, "<")
|
|
647
|
+
.replace(/>/g, ">")
|
|
648
|
+
.replace(/"/g, """)
|
|
649
|
+
.replace(/'/g, "'");
|
|
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) => {
|