@jsenv/ast 6.10.4 → 6.10.6
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/package.json +2 -2
- package/src/html/html_inline_content_url.js +6 -0
- package/src/html/html_node.js +48 -34
- package/src/js/js_static_analysis/css_template_substitutions.js +41 -18
- package/src/js/js_static_analysis/import_meta_css.js +15 -2
- package/src/js/parse_js_urls.js +8 -0
- package/src/main.js +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/ast",
|
|
3
|
-
"version": "6.10.
|
|
3
|
+
"version": "6.10.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Parse and transform HTML, CSS and JS",
|
|
6
6
|
"repository": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"acorn-import-attributes": "1.9.5",
|
|
37
37
|
"acorn-walk": "8.3.5",
|
|
38
38
|
"parse5": "8.0.1",
|
|
39
|
-
"postcss": "8.5.
|
|
39
|
+
"postcss": "8.5.28",
|
|
40
40
|
"postcss-value-parser": "4.2.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
@@ -34,6 +34,12 @@ export const getUrlForContentInsideHtml = (node, htmlUrlInfo, reference) => {
|
|
|
34
34
|
return inlineContentUrl;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
// a node injected by jsenv has no place in the original html, so its position
|
|
38
|
+
// is read in the generated one: a coordinate space the page own nodes are not
|
|
39
|
+
// numbered in. Both spaces would mint the same url for different content.
|
|
40
|
+
if (getHtmlNodeAttribute(node, "jsenv-injected-by")) {
|
|
41
|
+
basename = "injected";
|
|
42
|
+
}
|
|
37
43
|
const { line, column, lineEnd, columnEnd } = getHtmlNodePosition(node, {
|
|
38
44
|
preferOriginal: true,
|
|
39
45
|
});
|
package/src/html/html_node.js
CHANGED
|
@@ -54,7 +54,6 @@ export const injectJsenvScript = (
|
|
|
54
54
|
htmlAst,
|
|
55
55
|
{ type, src, content, initCall, pluginName = "jsenv", ...attributes },
|
|
56
56
|
) => {
|
|
57
|
-
const jsenvScriptsNode = getJsenvScriptsNode(htmlAst);
|
|
58
57
|
if (type === "module") {
|
|
59
58
|
if (src) {
|
|
60
59
|
if (initCall) {
|
|
@@ -67,7 +66,7 @@ export const injectJsenvScript = (
|
|
|
67
66
|
|
|
68
67
|
${stringifyCall(initCall)};`,
|
|
69
68
|
});
|
|
70
|
-
|
|
69
|
+
injectScriptNode(htmlAst, inlineScriptNode);
|
|
71
70
|
return;
|
|
72
71
|
}
|
|
73
72
|
const remoteScriptNode = createHtmlNode({
|
|
@@ -77,7 +76,7 @@ ${stringifyCall(initCall)};`,
|
|
|
77
76
|
"jsenv-injected-by": pluginName,
|
|
78
77
|
...attributes,
|
|
79
78
|
});
|
|
80
|
-
|
|
79
|
+
injectScriptNode(htmlAst, remoteScriptNode);
|
|
81
80
|
return;
|
|
82
81
|
}
|
|
83
82
|
const inlineScriptNode = createHtmlNode({
|
|
@@ -87,7 +86,7 @@ ${stringifyCall(initCall)};`,
|
|
|
87
86
|
"children": content,
|
|
88
87
|
...attributes,
|
|
89
88
|
});
|
|
90
|
-
|
|
89
|
+
injectScriptNode(htmlAst, inlineScriptNode);
|
|
91
90
|
return;
|
|
92
91
|
}
|
|
93
92
|
if (src) {
|
|
@@ -97,7 +96,7 @@ ${stringifyCall(initCall)};`,
|
|
|
97
96
|
"jsenv-injected-by": pluginName,
|
|
98
97
|
...attributes,
|
|
99
98
|
});
|
|
100
|
-
|
|
99
|
+
injectScriptNode(htmlAst, remoteScriptNode);
|
|
101
100
|
if (initCall) {
|
|
102
101
|
const inlineScriptNode = createHtmlNode({
|
|
103
102
|
"tagName": "script",
|
|
@@ -105,7 +104,7 @@ ${stringifyCall(initCall)};`,
|
|
|
105
104
|
"children": `${stringifyCall(initCall)};`,
|
|
106
105
|
...attributes,
|
|
107
106
|
});
|
|
108
|
-
|
|
107
|
+
injectScriptNode(htmlAst, inlineScriptNode);
|
|
109
108
|
}
|
|
110
109
|
return;
|
|
111
110
|
}
|
|
@@ -115,54 +114,69 @@ ${stringifyCall(initCall)};`,
|
|
|
115
114
|
"children": content,
|
|
116
115
|
...attributes,
|
|
117
116
|
});
|
|
118
|
-
|
|
117
|
+
injectScriptNode(htmlAst, inlineScriptNode);
|
|
119
118
|
};
|
|
120
119
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
// Scripts injected by jsenv are kept together between two marker comments, so
|
|
121
|
+
// the served html shows at a glance what the page wrote and what jsenv added.
|
|
122
|
+
// Comments are used rather than a wrapper element because the group lives in
|
|
123
|
+
// <head>, where an unknown tag would end <head> and reparent everything below
|
|
124
|
+
// it into <body>.
|
|
125
|
+
const JSENV_SCRIPTS_START = " jsenv scripts ";
|
|
126
|
+
const JSENV_SCRIPTS_END = " /jsenv scripts ";
|
|
127
|
+
|
|
128
|
+
const injectScriptNode = (htmlAst, scriptNode) => {
|
|
129
|
+
const endComment = findHtmlNode(
|
|
124
130
|
htmlAst,
|
|
125
|
-
(node) => node.nodeName === "
|
|
131
|
+
(node) => node.nodeName === "#comment" && node.data === JSENV_SCRIPTS_END,
|
|
126
132
|
);
|
|
127
|
-
if (
|
|
128
|
-
|
|
133
|
+
if (endComment) {
|
|
134
|
+
insertHtmlNodeBefore(scriptNode, endComment);
|
|
135
|
+
return;
|
|
129
136
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
insertFirstScriptNode(htmlAst, scriptNode);
|
|
138
|
+
insertHtmlNodeBefore(createHtmlComment(JSENV_SCRIPTS_START), scriptNode);
|
|
139
|
+
insertHtmlNodeAfter(createHtmlComment(JSENV_SCRIPTS_END), scriptNode);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// the group opens in <head> after any <link>, <meta> and <script
|
|
143
|
+
// type="importmap">, and before the first script the page wrote itself
|
|
144
|
+
const insertFirstScriptNode = (htmlAst, scriptNode) => {
|
|
133
145
|
const headNode = findChild(htmlAst, (node) => node.nodeName === "html")
|
|
134
146
|
.childNodes[0];
|
|
135
147
|
let after = headNode.childNodes[0];
|
|
136
148
|
for (const child of headNode.childNodes) {
|
|
137
|
-
if (child.nodeName === "link") {
|
|
149
|
+
if (child.nodeName === "link" || child.nodeName === "meta") {
|
|
138
150
|
after = child;
|
|
139
151
|
continue;
|
|
140
152
|
}
|
|
141
153
|
if (child.nodeName === "script") {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return jsenvScripts;
|
|
154
|
+
if (getHtmlNodeAttribute(child, "jsenv-injected-by")) {
|
|
155
|
+
after = child;
|
|
156
|
+
continue;
|
|
146
157
|
}
|
|
158
|
+
const { type } = analyzeScriptNode(child);
|
|
147
159
|
if (type === "importmap") {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
if (type === "js_module") {
|
|
152
|
-
insertHtmlNodeBefore(jsenvScripts, child);
|
|
153
|
-
return jsenvScripts;
|
|
160
|
+
after = child;
|
|
161
|
+
continue;
|
|
154
162
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
after = child;
|
|
163
|
+
insertHtmlNodeBefore(scriptNode, child);
|
|
164
|
+
return;
|
|
158
165
|
}
|
|
159
166
|
}
|
|
160
167
|
if (after) {
|
|
161
|
-
insertHtmlNodeAfter(
|
|
162
|
-
return
|
|
168
|
+
insertHtmlNodeAfter(scriptNode, after);
|
|
169
|
+
return;
|
|
163
170
|
}
|
|
164
|
-
injectHtmlNode(
|
|
165
|
-
|
|
171
|
+
injectHtmlNode(
|
|
172
|
+
htmlAst,
|
|
173
|
+
scriptNode,
|
|
174
|
+
getHtmlNodeAttribute(scriptNode, "jsenv-injected-by"),
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const createHtmlComment = (data) => {
|
|
179
|
+
return parseFragment(`<!--${data}-->`).childNodes[0];
|
|
166
180
|
};
|
|
167
181
|
const stringifyCall = (initCall, { pretty = true } = {}) => {
|
|
168
182
|
if (!Object.hasOwn(initCall, "params")) {
|
|
@@ -10,11 +10,21 @@
|
|
|
10
10
|
|
|
11
11
|
const PLACEHOLDER = (index) => `var(--jsenv-css-substitution-${index})`;
|
|
12
12
|
|
|
13
|
+
// A css comment an author writes in a template that cannot be made readable,
|
|
14
|
+
// to say so and stop being told. Kept as plain css: it costs nothing when the
|
|
15
|
+
// template does become readable, the css pipeline strips it like any comment.
|
|
16
|
+
const OPAQUE_DIRECTIVE = "jsenv-css-opaque";
|
|
17
|
+
|
|
18
|
+
export const hasCssOpaqueDirective = (templateSource) => {
|
|
19
|
+
return templateSource.includes(OPAQUE_DIRECTIVE);
|
|
20
|
+
};
|
|
21
|
+
|
|
13
22
|
/**
|
|
14
23
|
* @param {Object} templateLiteralNode a TemplateLiteral acorn node
|
|
15
24
|
* @param {string} js the source the node comes from
|
|
16
|
-
* @returns {{ content: string, substitutions: Array<{placeholder: string, expression: string}> }
|
|
17
|
-
*
|
|
25
|
+
* @returns {{ content: string, substitutions: Array<{placeholder: string, expression: string}> }
|
|
26
|
+
* | { opaque: { reason: string } }}
|
|
27
|
+
* "opaque" when the css cannot be read: the template is left alone
|
|
18
28
|
*/
|
|
19
29
|
export const buildCssTemplateSubstitutions = (templateLiteralNode, js) => {
|
|
20
30
|
const { quasis, expressions } = templateLiteralNode;
|
|
@@ -22,8 +32,12 @@ export const buildCssTemplateSubstitutions = (templateLiteralNode, js) => {
|
|
|
22
32
|
for (const quasi of quasis) {
|
|
23
33
|
const { cooked } = quasi.value;
|
|
24
34
|
if (cooked === undefined) {
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
return {
|
|
36
|
+
opaque: {
|
|
37
|
+
reason:
|
|
38
|
+
"an escape sequence cannot be cooked into the css it stands for",
|
|
39
|
+
},
|
|
40
|
+
};
|
|
27
41
|
}
|
|
28
42
|
parts.push(cooked);
|
|
29
43
|
}
|
|
@@ -39,9 +53,14 @@ export const buildCssTemplateSubstitutions = (templateLiteralNode, js) => {
|
|
|
39
53
|
});
|
|
40
54
|
content += placeholder + parts[i + 1];
|
|
41
55
|
}
|
|
42
|
-
for (
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
for (let i = 0; i < positions.length; i++) {
|
|
57
|
+
const nonValuePosition = readNonValuePosition(content, positions[i]);
|
|
58
|
+
if (nonValuePosition) {
|
|
59
|
+
return {
|
|
60
|
+
opaque: {
|
|
61
|
+
reason: `"\${${substitutions[i].expression}}" stands in ${nonValuePosition}`,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
45
64
|
}
|
|
46
65
|
}
|
|
47
66
|
return { content, substitutions };
|
|
@@ -49,7 +68,8 @@ export const buildCssTemplateSubstitutions = (templateLiteralNode, js) => {
|
|
|
49
68
|
|
|
50
69
|
// Reads the css from its start up to "position", keeping just enough state to
|
|
51
70
|
// tell what the placeholder sitting there would be part of.
|
|
52
|
-
|
|
71
|
+
// Returns what it is part of when that is not a value, null when it is one.
|
|
72
|
+
const readNonValuePosition = (css, position) => {
|
|
53
73
|
let braceDepth = 0;
|
|
54
74
|
let stringQuote = null;
|
|
55
75
|
let inComment = false;
|
|
@@ -130,29 +150,32 @@ const isCssValuePosition = (css, position) => {
|
|
|
130
150
|
}
|
|
131
151
|
i++;
|
|
132
152
|
}
|
|
133
|
-
if (inComment
|
|
134
|
-
return
|
|
153
|
+
if (inComment) {
|
|
154
|
+
return "a comment";
|
|
155
|
+
}
|
|
156
|
+
if (stringQuote) {
|
|
157
|
+
return "a string";
|
|
135
158
|
}
|
|
136
159
|
if (braceDepth === 0) {
|
|
137
|
-
|
|
138
|
-
return false;
|
|
160
|
+
return "a selector, or an at-rule prelude";
|
|
139
161
|
}
|
|
140
162
|
if (!colonSeen) {
|
|
141
|
-
|
|
142
|
-
return false;
|
|
163
|
+
return "a property name, or a nested selector";
|
|
143
164
|
}
|
|
144
165
|
if (css.slice(statementStart, position).trimStart().startsWith("@")) {
|
|
145
|
-
|
|
146
|
-
return false;
|
|
166
|
+
return "a nested at-rule prelude";
|
|
147
167
|
}
|
|
148
168
|
if (functionStack[functionStack.length - 1] === "url") {
|
|
149
169
|
// url() takes an url, and an url built at runtime is not one the build
|
|
150
170
|
// can follow to a file
|
|
151
|
-
return
|
|
171
|
+
return "url()";
|
|
152
172
|
}
|
|
153
173
|
// A ":" also opens a pseudo class, so what looks like a value here can still
|
|
154
174
|
// be the end of a nested selector; that is settled by what closes it.
|
|
155
|
-
|
|
175
|
+
if (!closedByDeclarationEnd(css, position)) {
|
|
176
|
+
return "a nested selector";
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
156
179
|
};
|
|
157
180
|
|
|
158
181
|
const readFunctionNameBefore = (css, parenthesisIndex) => {
|
|
@@ -12,7 +12,7 @@ export const isImportMetaCssAssignment = (node) => {
|
|
|
12
12
|
|
|
13
13
|
export const analyzeImportMetaCssAssignment = (
|
|
14
14
|
node,
|
|
15
|
-
{ js, ast, ancestors, onInlineContent },
|
|
15
|
+
{ js, ast, ancestors, onInlineContent, onOpaqueContent },
|
|
16
16
|
) => {
|
|
17
17
|
// "import.meta.css = [css, url]": the array form a pre-built file ships
|
|
18
18
|
// (see jsenv:import_meta_css); the css is the first element.
|
|
@@ -21,6 +21,9 @@ export const analyzeImportMetaCssAssignment = (
|
|
|
21
21
|
if (!assignedNode) {
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
|
+
// a pre-built package ships "[css, url]": its css was written in a source
|
|
25
|
+
// this build does not have, so this build is not where it can be fixed
|
|
26
|
+
const alreadyBuilt = node.right.type === "ArrayExpression";
|
|
24
27
|
// "const css = `...`; import.meta.css = css;" — the css is where the constant
|
|
25
28
|
// is declared, and that is where it is read and rewritten.
|
|
26
29
|
const nodeHoldingContent =
|
|
@@ -58,7 +61,16 @@ export const analyzeImportMetaCssAssignment = (
|
|
|
58
61
|
nodeHoldingContent,
|
|
59
62
|
js,
|
|
60
63
|
);
|
|
61
|
-
if (
|
|
64
|
+
if (substitutionInfo.opaque) {
|
|
65
|
+
onOpaqueContent({
|
|
66
|
+
...position,
|
|
67
|
+
...substitutionInfo.opaque,
|
|
68
|
+
alreadyBuilt,
|
|
69
|
+
templateSource: js.slice(
|
|
70
|
+
nodeHoldingContent.start,
|
|
71
|
+
nodeHoldingContent.end,
|
|
72
|
+
),
|
|
73
|
+
});
|
|
62
74
|
return;
|
|
63
75
|
}
|
|
64
76
|
onInlineContent({
|
|
@@ -69,6 +81,7 @@ export const analyzeImportMetaCssAssignment = (
|
|
|
69
81
|
quote: "`",
|
|
70
82
|
content: substitutionInfo.content,
|
|
71
83
|
substitutions: substitutionInfo.substitutions,
|
|
84
|
+
alreadyBuilt,
|
|
72
85
|
astInfo: { node: nodeHoldingContent },
|
|
73
86
|
});
|
|
74
87
|
};
|
package/src/js/parse_js_urls.js
CHANGED
|
@@ -78,6 +78,13 @@ export const parseJsUrls = ({
|
|
|
78
78
|
...inlineContentInfo,
|
|
79
79
|
});
|
|
80
80
|
};
|
|
81
|
+
// css the build cannot read: no url info comes out of it, only a way to tell
|
|
82
|
+
const onOpaqueContent = (opaqueContentInfo) => {
|
|
83
|
+
jsUrls.push({
|
|
84
|
+
isOpaqueCss: true,
|
|
85
|
+
...opaqueContentInfo,
|
|
86
|
+
});
|
|
87
|
+
};
|
|
81
88
|
|
|
82
89
|
const getCommentBeforeClosingParenthesis = (
|
|
83
90
|
// either new InlineContent() or JSON.parse() for instance
|
|
@@ -141,6 +148,7 @@ export const parseJsUrls = ({
|
|
|
141
148
|
ast,
|
|
142
149
|
ancestors,
|
|
143
150
|
onInlineContent,
|
|
151
|
+
onOpaqueContent,
|
|
144
152
|
});
|
|
145
153
|
}
|
|
146
154
|
},
|
package/src/main.js
CHANGED
|
@@ -59,6 +59,9 @@ export { visitJsAstUntil } from "./js/visit_js_ast_until.js";
|
|
|
59
59
|
export { visitJsAst } from "./js/visit_js_ast.js";
|
|
60
60
|
export { getImportMetaPropertyName } from "./js/import_meta.js";
|
|
61
61
|
export { getUrlForContentInsideJs } from "./js/js_inline_content_url.js";
|
|
62
|
-
export {
|
|
62
|
+
export {
|
|
63
|
+
hasCssOpaqueDirective,
|
|
64
|
+
renderCssTemplateLiteral,
|
|
65
|
+
} from "./js/js_static_analysis/css_template_substitutions.js";
|
|
63
66
|
|
|
64
67
|
export { generateUrlForInlineContent } from "./inline_content_url.js";
|