@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.
- package/README.md +3 -3
- package/dist/jsenv_core.js +1388 -1371
- package/package.json +1 -1
- package/src/dev/file_service.js +1 -1
- package/src/kitchen/url_graph/references.js +2 -2
- package/src/kitchen/url_graph/url_graph.js +14 -4
- package/src/plugins/autoreload/jsenv_plugin_autoreload_server.js +10 -2
- package/src/plugins/plugins.js +0 -2
- package/src/plugins/reference_analysis/html/jsenv_plugin_html_reference_analysis.js +505 -310
- package/src/plugins/ribbon/jsenv_plugin_ribbon.js +2 -2
- package/src/plugins/importmap/jsenv_plugin_importmap.js +0 -205
|
@@ -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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
const finalizeCallbacks = [];
|
|
132
|
+
const content = urlInfo.content;
|
|
133
|
+
const htmlAst = parseHtmlString(content);
|
|
44
134
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
"
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
indentation: false, // indentation would decrease stack trace precision
|
|
249
|
+
const inlineContentUrl = getUrlForContentInsideHtml(node, {
|
|
250
|
+
htmlUrl: urlInfo.url,
|
|
185
251
|
});
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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"];
|