@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/ast",
3
- "version": "6.10.4",
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.27",
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
  });
@@ -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
- insertHtmlNodeInside(inlineScriptNode, jsenvScriptsNode);
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
- insertHtmlNodeInside(remoteScriptNode, jsenvScriptsNode);
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
- insertHtmlNodeInside(inlineScriptNode, jsenvScriptsNode);
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
- insertHtmlNodeInside(remoteScriptNode, jsenvScriptsNode);
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
- insertHtmlNodeInside(inlineScriptNode, jsenvScriptsNode);
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
- insertHtmlNodeInside(inlineScriptNode, jsenvScriptsNode);
117
+ injectScriptNode(htmlAst, inlineScriptNode);
119
118
  };
120
119
 
121
- const getJsenvScriptsNode = (htmlAst) => {
122
- // get or insert <jsenv-scripts>
123
- let jsenvScripts = findHtmlNode(
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 === "jsenv-scripts",
131
+ (node) => node.nodeName === "#comment" && node.data === JSENV_SCRIPTS_END,
126
132
  );
127
- if (jsenvScripts) {
128
- return jsenvScripts;
133
+ if (endComment) {
134
+ insertHtmlNodeBefore(scriptNode, endComment);
135
+ return;
129
136
  }
130
- jsenvScripts = createHtmlNode({
131
- tagName: "jsenv-scripts",
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
- const { type } = analyzeScriptNode(child);
143
- if (type === "js_classic") {
144
- insertHtmlNodeBefore(jsenvScripts, child);
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
- insertHtmlNodeAfter(jsenvScripts, child);
149
- return jsenvScripts;
150
- }
151
- if (type === "js_module") {
152
- insertHtmlNodeBefore(jsenvScripts, child);
153
- return jsenvScripts;
160
+ after = child;
161
+ continue;
154
162
  }
155
- }
156
- if (child.nodeName === "meta") {
157
- after = child;
163
+ insertHtmlNodeBefore(scriptNode, child);
164
+ return;
158
165
  }
159
166
  }
160
167
  if (after) {
161
- insertHtmlNodeAfter(jsenvScripts, after);
162
- return jsenvScripts;
168
+ insertHtmlNodeAfter(scriptNode, after);
169
+ return;
163
170
  }
164
- injectHtmlNode(htmlAst, jsenvScripts);
165
- return jsenvScripts;
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}> }|null}
17
- * null when at least one expression is not in a css value position
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
- // an escape sequence acorn could not cook: what the css is cannot be told
26
- return null;
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 (const position of positions) {
43
- if (!isCssValuePosition(content, position)) {
44
- return null;
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
- const isCssValuePosition = (css, position) => {
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 || stringQuote) {
134
- return false;
153
+ if (inComment) {
154
+ return "a comment";
155
+ }
156
+ if (stringQuote) {
157
+ return "a string";
135
158
  }
136
159
  if (braceDepth === 0) {
137
- // outside any rule: a selector, or an at-rule prelude
138
- return false;
160
+ return "a selector, or an at-rule prelude";
139
161
  }
140
162
  if (!colonSeen) {
141
- // a property name, or a nested selector
142
- return false;
163
+ return "a property name, or a nested selector";
143
164
  }
144
165
  if (css.slice(statementStart, position).trimStart().startsWith("@")) {
145
- // an at-rule prelude nested in a rule, "@media (min-width: ...)"
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 false;
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
- return closedByDeclarationEnd(css, position);
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 (!substitutionInfo) {
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
  };
@@ -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 { renderCssTemplateLiteral } from "./js/js_static_analysis/css_template_substitutions.js";
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";