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