@jsenv/ast 6.1.3 → 6.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/ast",
3
- "version": "6.1.3",
3
+ "version": "6.2.1",
4
4
  "description": "Parse and transform HTML, CSS and JS",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -33,8 +33,8 @@
33
33
  "test": "node --conditions=development ./scripts/test.mjs"
34
34
  },
35
35
  "dependencies": {
36
- "@babel/parser": "7.24.7",
37
- "@jsenv/urls": "2.2.9",
36
+ "@babel/parser": "7.24.8",
37
+ "@jsenv/urls": "2.2.10",
38
38
  "acorn": "8.12.1",
39
39
  "acorn-import-attributes": "1.9.5",
40
40
  "acorn-walk": "8.3.3",
@@ -18,10 +18,10 @@ export const removeHtmlNode = (htmlNode) => {
18
18
  childNodes.splice(childNodes.indexOf(htmlNode), 1);
19
19
  };
20
20
 
21
- export const createHtmlNode = ({ tagName, textContent = "", ...rest }) => {
21
+ export const createHtmlNode = ({ tagName, children = "", ...rest }) => {
22
22
  const html = `<${tagName} ${stringifyAttributes(
23
23
  rest,
24
- )}>${textContent}</${tagName}>`;
24
+ )}>${children}</${tagName}>`;
25
25
  const fragment = parseFragment(html);
26
26
  return fragment.childNodes[0];
27
27
  };
@@ -46,10 +46,108 @@ export const injectHtmlNode = (htmlAst, node, jsenvPluginName = "jsenv") => {
46
46
  if (after) {
47
47
  insertHtmlNodeAfter(node, after);
48
48
  } else {
49
- injectWithWhitespaces(node, bodyNode, 0);
49
+ injectWithLineBreakAndIndent(node, bodyNode, 0);
50
50
  }
51
51
  };
52
52
 
53
+ export const injectJsenvScript = (
54
+ htmlAst,
55
+ { type, src, content, initCall, pluginName = "jsenv" },
56
+ ) => {
57
+ const jsenvScriptsNode = getJsenvScriptsNode(htmlAst);
58
+ if (type === "module") {
59
+ if (src) {
60
+ if (initCall) {
61
+ const paramsSource = stringifyParams(initCall.params, " ");
62
+ const inlineScriptNode = createHtmlNode({
63
+ "tagName": "script",
64
+ "type": "module",
65
+ "jsenv-injected-by": pluginName,
66
+ "children": `import { ${initCall.callee} } from ${src};
67
+
68
+ ${initCall.callee}({
69
+ ${paramsSource}
70
+ });`,
71
+ });
72
+ insertHtmlNodeInside(inlineScriptNode, jsenvScriptsNode);
73
+ return;
74
+ }
75
+ const remoteScriptNode = createHtmlNode({
76
+ "tagName": "script",
77
+ "type": "module",
78
+ src,
79
+ "jsenv-injected-by": pluginName,
80
+ });
81
+ insertHtmlNodeInside(remoteScriptNode, jsenvScriptsNode);
82
+ return;
83
+ }
84
+ const inlineScriptNode = createHtmlNode({
85
+ "tagName": "script",
86
+ "type": "module",
87
+ "jsenv-injected-by": pluginName,
88
+ "children": content,
89
+ });
90
+ insertHtmlNodeInside(jsenvScriptsNode, inlineScriptNode);
91
+ return;
92
+ }
93
+ if (src) {
94
+ const remoteScriptNode = createHtmlNode({
95
+ "tagName": "script",
96
+ src,
97
+ "jsenv-injected-by": pluginName,
98
+ });
99
+ insertHtmlNodeInside(remoteScriptNode, jsenvScriptsNode);
100
+ if (initCall) {
101
+ const paramsSource = stringifyParams(initCall.params, " ");
102
+ const inlineScriptNode = createHtmlNode({
103
+ "tagName": "script",
104
+ "jsenv-injected-by": pluginName,
105
+ "children": `${initCall.callee}({
106
+ ${paramsSource}
107
+ });`,
108
+ });
109
+ insertHtmlNodeInside(inlineScriptNode, jsenvScriptsNode);
110
+ }
111
+ return;
112
+ }
113
+ const inlineScriptNode = createHtmlNode({
114
+ "tagName": "script",
115
+ "jsenv-injected-by": pluginName,
116
+ "children": content,
117
+ });
118
+ insertHtmlNodeInside(inlineScriptNode, jsenvScriptsNode);
119
+ };
120
+
121
+ const getJsenvScriptsNode = (htmlAst) => {
122
+ // get or insert <jsenv-scripts>
123
+ let jsenvScripts = findHtmlNode(
124
+ htmlAst,
125
+ (node) => node.nodeName === "jsenv-scripts",
126
+ );
127
+ if (jsenvScripts) {
128
+ return jsenvScripts;
129
+ }
130
+ jsenvScripts = createHtmlNode({
131
+ tagName: "jsenv-scripts",
132
+ });
133
+ getAsFirstJsModuleInjector(htmlAst, {
134
+ anyScript: true,
135
+ })(jsenvScripts);
136
+ return jsenvScripts;
137
+ };
138
+ const stringifyParams = (params, prefix = "") => {
139
+ const source = JSON.stringify(params, null, prefix);
140
+ if (prefix.length) {
141
+ // remove leading "{\n"
142
+ // remove leading prefix
143
+ // remove trailing "\n}"
144
+ return source.slice(2 + prefix.length, -2);
145
+ }
146
+ // remove leading "{"
147
+ // remove trailing "}"
148
+ return source.slice(1, -1);
149
+ };
150
+
53
151
  export const injectHtmlNodeAsEarlyAsPossible = (
54
152
  htmlAst,
55
153
  node,
@@ -68,6 +166,7 @@ export const injectHtmlNodeAsEarlyAsPossible = (
68
166
  // - after any <link>
69
167
  // - but before first <link rel="modulepreload">
70
168
  // - and before <script type="module">
169
+ // - and after any <meta>
71
170
  const isImportmap = type === "importmap";
72
171
  if (isImportmap) {
73
172
  let after = headNode.childNodes[0];
@@ -85,48 +184,23 @@ export const injectHtmlNodeAsEarlyAsPossible = (
85
184
  ) {
86
185
  return insertHtmlNodeBefore(node, child);
87
186
  }
187
+ if (child.nodeName === "meta") {
188
+ after = child;
189
+ }
88
190
  }
89
191
  if (after) {
90
192
  return insertHtmlNodeAfter(node, after);
91
193
  }
92
194
  return injectHtmlNode(htmlAst, node);
93
195
  }
94
- // <script type="module">
95
- // - after <script type="importmap">
96
- // - and after any <link>
97
- // - and before first <script type="module">
98
196
  const isJsModule = type === "js_module";
99
197
  if (isJsModule) {
100
- const firstImportmapScript = findHtmlNode(htmlAst, (node) => {
101
- return (
102
- node.nodeName === "script" &&
103
- analyzeScriptNode(node).type === "importmap"
104
- );
105
- });
106
- if (firstImportmapScript) {
107
- return insertHtmlNodeAfter(node, firstImportmapScript);
108
- }
109
- let after = headNode.childNodes[0];
110
- for (const child of headNode.childNodes) {
111
- if (child.nodeName === "link") {
112
- after = child;
113
- continue;
114
- }
115
- if (
116
- child.nodeName === "script" &&
117
- analyzeScriptNode(child).type === "module"
118
- ) {
119
- return insertHtmlNodeBefore(node, child);
120
- }
121
- }
122
- if (after) {
123
- return insertHtmlNodeAfter(node, after);
124
- }
125
- return injectHtmlNode(htmlAst, node);
198
+ return getAsFirstJsModuleInjector(htmlAst)(node);
126
199
  }
127
200
  // <script> or <script type="text/jsx">, ...
128
201
  // - after any <link>
129
202
  // - before any <script>
203
+ // - after any <meta>
130
204
  let after = headNode.childNodes[0];
131
205
  for (const child of headNode.childNodes) {
132
206
  if (child.nodeName === "link") {
@@ -136,6 +210,9 @@ export const injectHtmlNodeAsEarlyAsPossible = (
136
210
  if (child.nodeName === "script") {
137
211
  return insertHtmlNodeBefore(node, child);
138
212
  }
213
+ if (child.nodeName === "meta") {
214
+ after = child;
215
+ }
139
216
  }
140
217
  if (after) {
141
218
  return insertHtmlNodeAfter(node, after);
@@ -145,49 +222,139 @@ export const injectHtmlNodeAsEarlyAsPossible = (
145
222
  return injectHtmlNode(htmlAst, node);
146
223
  };
147
224
 
225
+ // <script type="module">
226
+ // - after <script type="importmap">
227
+ // - and after any <link>
228
+ // - and before first <script type="module">
229
+ // - after any <meta>
230
+ const getAsFirstJsModuleInjector = (htmlAst, { anyScript } = {}) => {
231
+ const headNode = findChild(htmlAst, (node) => node.nodeName === "html")
232
+ .childNodes[0];
233
+ const firstImportmapScript = findHtmlNode(htmlAst, (node) => {
234
+ return (
235
+ node.nodeName === "script" && analyzeScriptNode(node).type === "importmap"
236
+ );
237
+ });
238
+ if (firstImportmapScript) {
239
+ return (node) => insertHtmlNodeAfter(node, firstImportmapScript);
240
+ }
241
+ let after = headNode.childNodes[0];
242
+ for (const child of headNode.childNodes) {
243
+ if (child.nodeName === "link") {
244
+ after = child;
245
+ continue;
246
+ }
247
+ if (
248
+ child.nodeName === "script" &&
249
+ (anyScript || analyzeScriptNode(child).type === "module")
250
+ ) {
251
+ return (node) => insertHtmlNodeBefore(node, child);
252
+ }
253
+ if (child.nodeName === "meta") {
254
+ after = child;
255
+ }
256
+ }
257
+ if (after) {
258
+ return (node) => insertHtmlNodeAfter(node, after);
259
+ }
260
+ return (node) => injectHtmlNode(htmlAst, node);
261
+ };
262
+
263
+ export const insertHtmlNodeInside = (nodeToInsert, futureParentNode) => {
264
+ const { childNodes = [] } = futureParentNode;
265
+ return injectWithLineBreakAndIndent(
266
+ nodeToInsert,
267
+ futureParentNode,
268
+ childNodes.length,
269
+ );
270
+ };
271
+
148
272
  export const insertHtmlNodeBefore = (nodeToInsert, futureNextSibling) => {
149
273
  const futureParentNode = futureNextSibling.parentNode;
150
274
  const { childNodes = [] } = futureParentNode;
151
275
  const futureIndex = childNodes.indexOf(futureNextSibling);
152
- injectWithWhitespaces(nodeToInsert, futureParentNode, futureIndex);
276
+ injectWithLineBreakAndIndent(nodeToInsert, futureParentNode, futureIndex);
153
277
  };
154
278
 
155
279
  export const insertHtmlNodeAfter = (nodeToInsert, futurePrevSibling) => {
156
280
  const futureParentNode = futurePrevSibling.parentNode;
157
281
  const { childNodes = [] } = futureParentNode;
158
282
  const futureIndex = childNodes.indexOf(futurePrevSibling) + 1;
159
- injectWithWhitespaces(nodeToInsert, futureParentNode, futureIndex);
283
+ injectWithLineBreakAndIndent(nodeToInsert, futureParentNode, futureIndex);
160
284
  };
161
285
 
162
- const injectWithWhitespaces = (nodeToInsert, futureParentNode, futureIndex) => {
286
+ const injectWithLineBreakAndIndent = (
287
+ nodeToInsert,
288
+ futureParentNode,
289
+ futureIndex,
290
+ ) => {
163
291
  const { childNodes = [] } = futureParentNode;
164
292
  const previousSiblings = childNodes.slice(0, futureIndex);
165
293
  const nextSiblings = childNodes.slice(futureIndex);
166
294
  const futureChildNodes = [];
167
295
 
296
+ const previousSibling = childNodes[futureIndex - 1];
297
+ const parentIndentation = getIndentation(futureParentNode);
298
+ const nextSibling = childNodes[futureIndex];
299
+ let childIndentation;
300
+ if (previousSibling) {
301
+ childIndentation = getIndentation(previousSibling);
302
+ } else if (nextSibling) {
303
+ childIndentation = getIndentation(nextSibling);
304
+ } else {
305
+ childIndentation = increaseIndentation(parentIndentation, 2);
306
+ }
168
307
  if (previousSiblings.length) {
169
308
  futureChildNodes.push(...previousSiblings);
170
309
  }
171
- const previousSibling = childNodes[futureIndex - 1];
172
- if (
173
- nodeToInsert.nodeName !== "#text" &&
174
- (!previousSibling || !isWhitespaceNode(previousSibling))
175
- ) {
176
- let indentation;
177
- if (childNodes.length) {
178
- indentation = getIndentation(childNodes[childNodes.length - 1]);
179
- } else {
180
- const parentIndentation = getIndentation(futureParentNode);
181
- indentation = increaseIndentation(parentIndentation, 2);
310
+ line_break_and_indent_before: {
311
+ if (nodeToInsert.nodeName === "#text") {
312
+ break line_break_and_indent_before;
313
+ }
314
+ if (!previousSibling) {
315
+ futureChildNodes.push({
316
+ nodeName: "#text",
317
+ value: `\n${childIndentation}`,
318
+ parentNode: futureParentNode,
319
+ });
320
+ break line_break_and_indent_before;
321
+ }
322
+ if (isLineBreakAndIndent(previousSibling)) {
323
+ if (!nextSibling) {
324
+ previousSibling.value = `\n${childIndentation}`;
325
+ }
326
+ break line_break_and_indent_before;
182
327
  }
183
328
  futureChildNodes.push({
184
329
  nodeName: "#text",
185
- value: `\n${indentation}`,
330
+ value: `\n${childIndentation}`,
186
331
  parentNode: futureParentNode,
187
332
  });
188
333
  }
189
334
  futureChildNodes.push(nodeToInsert);
190
335
  nodeToInsert.parentNode = futureParentNode;
336
+ line_break_and_indent_after: {
337
+ if (nodeToInsert.nodeName === "#text") {
338
+ break line_break_and_indent_after;
339
+ }
340
+ if (!nextSibling) {
341
+ futureChildNodes.push({
342
+ nodeName: "#text",
343
+ value: `\n${parentIndentation}`,
344
+ parentNode: futureParentNode,
345
+ });
346
+ break line_break_and_indent_after;
347
+ }
348
+ if (isLineBreakAndIndent(nextSibling)) {
349
+ // nextSibling.value = `\n${indentation}`;
350
+ break line_break_and_indent_after;
351
+ }
352
+ futureChildNodes.push({
353
+ nodeName: "#text",
354
+ value: `\n${childIndentation}`,
355
+ parentNode: futureParentNode,
356
+ });
357
+ }
191
358
  if (nextSiblings.length) {
192
359
  futureChildNodes.push(...nextSiblings);
193
360
  }
@@ -200,10 +367,15 @@ const injectWithWhitespaces = (nodeToInsert, futureParentNode, futureIndex) => {
200
367
  }
201
368
  };
202
369
 
203
- const isWhitespaceNode = (node) => {
204
- if (node.nodeName !== "#text") return false;
205
- if (node.value.length === 0) return false;
206
- return /^\s+$/.test(node.value);
370
+ const isLineBreakAndIndent = (htmlNode) => {
371
+ if (htmlNode.nodeName !== "#text") {
372
+ return false;
373
+ }
374
+ const { value } = htmlNode;
375
+ if (value[0] !== "\n") {
376
+ return false;
377
+ }
378
+ return value.slice(1).trim() === "";
207
379
  };
208
380
 
209
381
  const findChild = ({ childNodes = [] }, predicate) =>
@@ -66,26 +66,25 @@ export const getIndentation = (htmlNode) => {
66
66
  const index = siblings.indexOf(htmlNode);
67
67
  if (index === 0) {
68
68
  if (htmlNode.nodeName === "#text") {
69
- return extractIndentation(htmlNode);
69
+ if (htmlNode.value.length) {
70
+ return extractIndentation(htmlNode);
71
+ }
72
+ return increaseIndentation(getIndentation(parentNode), 2);
70
73
  }
71
- return "";
72
74
  }
73
75
 
74
76
  let textNodeIndex = 0;
75
- let textNode;
76
77
  while (textNodeIndex !== index) {
77
78
  const previousSibling = siblings[textNodeIndex];
78
79
  textNodeIndex++;
79
80
  if (previousSibling.nodeName === "#text") {
80
- textNode = previousSibling;
81
- break;
81
+ if (previousSibling.value.length) {
82
+ return extractIndentation(previousSibling);
83
+ }
84
+ return increaseIndentation(getIndentation(parentNode), 2);
82
85
  }
83
86
  }
84
- if (!textNode) {
85
- return "";
86
- }
87
-
88
- return extractIndentation(textNode);
87
+ return "";
89
88
  };
90
89
 
91
90
  const extractIndentation = (textNode) => {
@@ -116,3 +115,11 @@ export const increaseIndentation = (indentation, size) => {
116
115
  const char = indentation[0];
117
116
  return char ? `${indentation}${char.repeat(size)}` : " ".repeat(size);
118
117
  };
118
+
119
+ export const decreaseIndentation = (indentation, size) => {
120
+ const char = indentation[0];
121
+ if (char) {
122
+ return char.slice(0, -size);
123
+ }
124
+ return "";
125
+ };
package/src/main.js CHANGED
@@ -23,6 +23,8 @@ export {
23
23
  injectHtmlNode,
24
24
  injectHtmlNodeAsEarlyAsPossible,
25
25
  insertHtmlNodeAfter,
26
+ insertHtmlNodeInside,
27
+ injectJsenvScript,
26
28
  } from "./html/html_node.js";
27
29
  export {
28
30
  inlineScriptNode,