@jsenv/ast 5.0.0 → 5.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/ast",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "Parse and transform HTML,CSS and JS",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,11 +34,12 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@babel/parser": "7.22.7",
37
+ "@jsenv/urls": "2.2.0",
37
38
  "acorn": "8.10.0",
38
39
  "acorn-import-assertions": "1.9.0",
39
40
  "acorn-walk": "8.2.0",
40
41
  "parse5": "7.1.2",
41
- "postcss": "8.4.25",
42
+ "postcss": "8.4.27",
42
43
  "postcss-value-parser": "4.2.0"
43
44
  }
44
45
  }
@@ -6,18 +6,21 @@ export const analyzeScriptNode = (scriptNode) => {
6
6
  return {
7
7
  type: "js_classic",
8
8
  contentType: "text/javascript",
9
+ extension: ".js",
9
10
  };
10
11
  }
11
12
  if (typeAttribute === "module") {
12
13
  return {
13
14
  type: "js_module",
14
15
  contentType: "text/javascript",
16
+ extension: ".js",
15
17
  };
16
18
  }
17
19
  if (typeAttribute === "importmap") {
18
20
  return {
19
21
  type: "importmap",
20
22
  contentType: "application/importmap+json",
23
+ extension: ".importmap",
21
24
  };
22
25
  }
23
26
  // jsx
@@ -74,6 +77,7 @@ export const analyzeScriptNode = (scriptNode) => {
74
77
  return {
75
78
  type: "text",
76
79
  contentType: "text/plain",
80
+ extension: ".txt",
77
81
  };
78
82
  };
79
83
 
@@ -0,0 +1,50 @@
1
+ import { injectQueryParams, urlToExtension, urlToBasename } from "@jsenv/urls";
2
+
3
+ import { generateUrlForInlineContent } from "../inline_content_url.js";
4
+ import { getHtmlNodeAttribute } from "./html_node_attributes.js";
5
+ import { getHtmlNodePosition } from "./html_node_position.js";
6
+ import { analyzeScriptNode } from "./html_analysis.js";
7
+
8
+ export const getUrlForContentInsideHtml = (node, { htmlUrl, url }) => {
9
+ let externalSpecifierAttributeName;
10
+ let basename;
11
+ let extension;
12
+
13
+ if (node.nodeName === "script") {
14
+ externalSpecifierAttributeName = "inlined-from-src";
15
+ const scriptAnalysisResult = analyzeScriptNode(node);
16
+ extension = scriptAnalysisResult.extension;
17
+ } else if (node.nodeName === "style") {
18
+ externalSpecifierAttributeName = "inlined-from-href";
19
+ extension = ".css";
20
+ } else if (node.nodeName === "link") {
21
+ basename = urlToBasename(url);
22
+ extension = urlToExtension(url);
23
+ }
24
+
25
+ if (externalSpecifierAttributeName) {
26
+ const externalSpecifier = getHtmlNodeAttribute(
27
+ node,
28
+ externalSpecifierAttributeName,
29
+ );
30
+ if (externalSpecifier) {
31
+ const inlineContentUrl = injectQueryParams(externalSpecifier, {
32
+ inlined: "",
33
+ });
34
+ return inlineContentUrl;
35
+ }
36
+ }
37
+ const { line, column, lineEnd, columnEnd } = getHtmlNodePosition(node, {
38
+ preferOriginal: true,
39
+ });
40
+ const inlineContentUrl = generateUrlForInlineContent({
41
+ url: htmlUrl,
42
+ basename,
43
+ extension,
44
+ line,
45
+ column,
46
+ lineEnd,
47
+ columnEnd,
48
+ });
49
+ return inlineContentUrl;
50
+ };
@@ -0,0 +1,33 @@
1
+ import { urlToFilename, urlToExtension } from "@jsenv/urls";
2
+
3
+ export const generateUrlForInlineContent = ({
4
+ url,
5
+ extension,
6
+ basename,
7
+ line,
8
+ column,
9
+ lineEnd,
10
+ columnEnd,
11
+ }) => {
12
+ if (extension === undefined) {
13
+ extension = urlToExtension(url);
14
+ }
15
+
16
+ let generatedName = "";
17
+ if (basename !== undefined) {
18
+ generatedName += basename;
19
+ }
20
+ if (line !== undefined && column !== undefined) {
21
+ generatedName += `L${line}C${column}`;
22
+ if (lineEnd !== undefined && columnEnd !== undefined) {
23
+ generatedName += `-L${lineEnd}C${columnEnd}`;
24
+ }
25
+ }
26
+
27
+ const filenameRaw = urlToFilename(url);
28
+ const filename = `${filenameRaw}@${generatedName}${extension}`;
29
+ // ideally we should keep query params from url
30
+ // maybe we could use a custom scheme like "inline:"
31
+ const inlineContentUrl = new URL(filename, url).href;
32
+ return inlineContentUrl;
33
+ };
@@ -3,7 +3,7 @@
3
3
  * - https://astexplorer.net/
4
4
  * - https://bvaughn.github.io/babel-repl
5
5
  */
6
- import { fileURLToPath } from "node:url";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
7
 
8
8
  import { createJsParseError } from "./js_parse_error.js";
9
9
 
@@ -63,10 +63,10 @@ export const applyBabelPlugins = async ({
63
63
  try {
64
64
  if (ast) {
65
65
  const result = await transformFromAstAsync(ast, input, options);
66
- return result;
66
+ return normalizeResult(result);
67
67
  }
68
68
  const result = await transformAsync(input, options);
69
- return result;
69
+ return normalizeResult(result);
70
70
  } catch (error) {
71
71
  if (error && error.code === "BABEL_PARSE_ERROR") {
72
72
  throw createJsParseError({
@@ -82,6 +82,16 @@ export const applyBabelPlugins = async ({
82
82
  }
83
83
  };
84
84
 
85
+ const normalizeResult = (result) => {
86
+ const { map } = result;
87
+ if (map) {
88
+ map.sources.forEach((source, index) => {
89
+ map.sources[index] = pathToFileURL(source).href;
90
+ });
91
+ }
92
+ return result;
93
+ };
94
+
85
95
  const useTypeScriptExtension = (url) => {
86
96
  const { pathname } = new URL(url);
87
97
  return pathname.endsWith(".ts") || pathname.endsWith(".tsx");
@@ -0,0 +1,19 @@
1
+ import { injectQueryParams } from "@jsenv/urls";
2
+ import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
3
+
4
+ import { generateUrlForInlineContent } from "../inline_content_url.js";
5
+
6
+ export const getUrlForContentInsideJs = (inlineReferenceInfo, { url }) => {
7
+ const { inlinedFromUrl } = inlineReferenceInfo;
8
+ if (inlinedFromUrl) {
9
+ return injectQueryParams(inlinedFromUrl, { inlined: "" });
10
+ }
11
+ return generateUrlForInlineContent({
12
+ url,
13
+ extension: CONTENT_TYPE.asFileExtension(inlineReferenceInfo.contentType),
14
+ line: inlineReferenceInfo.line,
15
+ column: inlineReferenceInfo.column,
16
+ lineEnd: inlineReferenceInfo.lineEnd,
17
+ columnEnd: inlineReferenceInfo.columnEnd,
18
+ });
19
+ };
@@ -1,4 +1,4 @@
1
- export const getTypePropertyNode = (node) => {
1
+ export const findPropertyNodeByName = (node, name) => {
2
2
  if (node.type !== "ObjectExpression") {
3
3
  return null;
4
4
  }
@@ -7,7 +7,7 @@ export const getTypePropertyNode = (node) => {
7
7
  return (
8
8
  property.type === "Property" &&
9
9
  property.key.type === "Identifier" &&
10
- property.key.name === "type"
10
+ property.key.name === name
11
11
  );
12
12
  });
13
13
  };
@@ -11,13 +11,17 @@ export const isJSONParseCall = (node) => {
11
11
  );
12
12
  };
13
13
 
14
- export const analyzeJSONParseCall = (node, { onInlineContent }) => {
14
+ export const analyzeJSONParseCall = (
15
+ node,
16
+ { onInlineContent, readInlinedFromUrl },
17
+ ) => {
15
18
  const nodeHoldingContent = node.arguments[0];
16
19
  const contentInfo = extractContentInfo(nodeHoldingContent);
17
20
  if (contentInfo) {
18
21
  onInlineContent({
19
22
  type: "json_parse_first_arg",
20
23
  contentType: "application/json",
24
+ inlinedFromUrl: readInlinedFromUrl(node),
21
25
  start: nodeHoldingContent.start,
22
26
  end: nodeHoldingContent.end,
23
27
  line: nodeHoldingContent.loc.start.line,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  isStringLiteralNode,
3
- getTypePropertyNode,
3
+ findPropertyNodeByName,
4
4
  extractContentInfo,
5
5
  } from "./helpers.js";
6
6
 
@@ -11,7 +11,10 @@ export const isNewBlobCall = (node) => {
11
11
  node.callee.name === "Blob"
12
12
  );
13
13
  };
14
- export const analyzeNewBlobCall = (node, { onInlineContent }) => {
14
+ export const analyzeNewBlobCall = (
15
+ node,
16
+ { onInlineContent, readInlinedFromUrl },
17
+ ) => {
15
18
  const [firstArg, secondArg] = node.arguments;
16
19
  if (!firstArg) {
17
20
  return;
@@ -25,7 +28,7 @@ export const analyzeNewBlobCall = (node, { onInlineContent }) => {
25
28
  if (firstArg.elements.length !== 1) {
26
29
  return;
27
30
  }
28
- const typePropertyNode = getTypePropertyNode(secondArg);
31
+ const typePropertyNode = findPropertyNodeByName(secondArg, "type");
29
32
  if (!typePropertyNode) {
30
33
  return;
31
34
  }
@@ -40,6 +43,7 @@ export const analyzeNewBlobCall = (node, { onInlineContent }) => {
40
43
  onInlineContent({
41
44
  type: "new_blob_first_arg",
42
45
  contentType,
46
+ inlinedFromUrl: readInlinedFromUrl(node),
43
47
  start: nodeHoldingContent.start,
44
48
  end: nodeHoldingContent.end,
45
49
  line: nodeHoldingContent.loc.start.line,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  isStringLiteralNode,
3
- getTypePropertyNode,
3
+ findPropertyNodeByName,
4
4
  extractContentInfo,
5
5
  } from "./helpers.js";
6
6
 
@@ -11,7 +11,10 @@ export const isNewInlineContentCall = (node) => {
11
11
  node.callee.name === "__InlineContent__"
12
12
  );
13
13
  };
14
- export const analyzeNewInlineContentCall = (node, { onInlineContent }) => {
14
+ export const analyzeNewInlineContentCall = (
15
+ node,
16
+ { onInlineContent, readInlinedFromUrl },
17
+ ) => {
15
18
  const [firstArg, secondArg] = node.arguments;
16
19
  if (!firstArg) {
17
20
  return;
@@ -19,7 +22,7 @@ export const analyzeNewInlineContentCall = (node, { onInlineContent }) => {
19
22
  if (!secondArg) {
20
23
  return;
21
24
  }
22
- const typePropertyNode = getTypePropertyNode(secondArg);
25
+ const typePropertyNode = findPropertyNodeByName(secondArg, "type");
23
26
  if (!typePropertyNode) {
24
27
  return;
25
28
  }
@@ -27,6 +30,7 @@ export const analyzeNewInlineContentCall = (node, { onInlineContent }) => {
27
30
  if (!isStringLiteralNode(typePropertyValueNode)) {
28
31
  return;
29
32
  }
33
+
30
34
  const nodeHoldingContent = firstArg;
31
35
  const contentType = typePropertyValueNode.value;
32
36
  const contentInfo = extractContentInfo(nodeHoldingContent);
@@ -34,6 +38,7 @@ export const analyzeNewInlineContentCall = (node, { onInlineContent }) => {
34
38
  onInlineContent({
35
39
  type: "new_inline_content_first_arg",
36
40
  contentType,
41
+ inlinedFromUrl: readInlinedFromUrl(node),
37
42
  start: nodeHoldingContent.start,
38
43
  end: nodeHoldingContent.end,
39
44
  line: nodeHoldingContent.loc.start.line,
@@ -1,4 +1,4 @@
1
- import { getTypePropertyNode, isStringLiteralNode } from "./helpers.js";
1
+ import { findPropertyNodeByName, isStringLiteralNode } from "./helpers.js";
2
2
  import { isNewUrlCall, analyzeNewUrlCall } from "./new_url.js";
3
3
  import {
4
4
  isImportMetaResolveCall,
@@ -102,7 +102,7 @@ const analyzeWorkerCallArguments = (
102
102
  let typePropertyNode;
103
103
  const secondArgNode = node.arguments[1];
104
104
  if (secondArgNode) {
105
- typePropertyNode = getTypePropertyNode(secondArgNode);
105
+ typePropertyNode = findPropertyNodeByName(secondArgNode, "type");
106
106
  if (typePropertyNode) {
107
107
  const typePropertyValueNode = typePropertyNode.value;
108
108
  if (isStringLiteralNode(typePropertyValueNode)) {
@@ -73,6 +73,49 @@ export const parseJsUrls = ({
73
73
  ...inlineContentInfo,
74
74
  });
75
75
  };
76
+
77
+ const getCommentBeforeClosingParenthesis = (
78
+ // either new InlineContent() or JSON.parse() for instance
79
+ callNode,
80
+ ) => {
81
+ const args = callNode.arguments;
82
+ let commentMustStartAfter;
83
+ let commentMustEndBefore;
84
+ if (args.length === 0) {
85
+ commentMustStartAfter = callNode.start;
86
+ commentMustEndBefore = callNode.end - 1;
87
+ } else {
88
+ const lastArg = args[args.length - 1];
89
+ commentMustStartAfter = lastArg.start;
90
+ commentMustEndBefore = callNode.end - 1;
91
+ }
92
+ for (const comment of ast.comments) {
93
+ if (
94
+ comment.start > commentMustStartAfter &&
95
+ comment.end < commentMustEndBefore
96
+ ) {
97
+ return comment;
98
+ }
99
+ }
100
+ return null;
101
+ };
102
+
103
+ const readInlinedFromUrl = (node) => {
104
+ let inlinedFromUrl;
105
+ const commentBeforeClosingParenthesis =
106
+ getCommentBeforeClosingParenthesis(node);
107
+ if (commentBeforeClosingParenthesis) {
108
+ const text = commentBeforeClosingParenthesis.text;
109
+ const inlinedFromUrlIndex = text.indexOf("inlinedFromUrl=");
110
+ if (inlinedFromUrlIndex > -1) {
111
+ inlinedFromUrl = text.slice(
112
+ inlinedFromUrlIndex + "inlinedFromUrl=".length,
113
+ );
114
+ }
115
+ }
116
+ return inlinedFromUrl;
117
+ };
118
+
76
119
  ancestor(ast, {
77
120
  ImportDeclaration: (node) => {
78
121
  analyzeImportDeclaration(node, { onUrl });
@@ -125,6 +168,7 @@ export const parseJsUrls = ({
125
168
  if (inlineContent && isJSONParseCall(node)) {
126
169
  analyzeJSONParseCall(node, {
127
170
  onInlineContent,
171
+ readInlinedFromUrl,
128
172
  });
129
173
  return;
130
174
  }
@@ -163,12 +207,14 @@ export const parseJsUrls = ({
163
207
  if (inlineContent && isNewInlineContentCall(node)) {
164
208
  analyzeNewInlineContentCall(node, {
165
209
  onInlineContent,
210
+ readInlinedFromUrl,
166
211
  });
167
212
  return;
168
213
  }
169
214
  if (inlineContent && isNewBlobCall(node)) {
170
215
  analyzeNewBlobCall(node, {
171
216
  onInlineContent,
217
+ readInlinedFromUrl,
172
218
  });
173
219
  return;
174
220
  }
@@ -5,6 +5,7 @@ import { createJsParseError } from "./js_parse_error.js";
5
5
 
6
6
  export const parseJsWithAcorn = ({ js, url, isJsModule }) => {
7
7
  const AcornParser = Parser.extend(importAssertions);
8
+ const comments = [];
8
9
 
9
10
  try {
10
11
  // https://github.com/acornjs/acorn/tree/master/acorn#interface
@@ -13,7 +14,16 @@ export const parseJsWithAcorn = ({ js, url, isJsModule }) => {
13
14
  allowAwaitOutsideFunction: true,
14
15
  sourceType: isJsModule ? "module" : "script",
15
16
  ecmaVersion: 2022,
17
+ onComment: (block, text, start, end) => {
18
+ comments.push({
19
+ block,
20
+ text,
21
+ start,
22
+ end,
23
+ });
24
+ },
16
25
  });
26
+ jsAst.comments = comments;
17
27
  return jsAst;
18
28
  } catch (e) {
19
29
  if (e && e.name === "SyntaxError") {
package/src/main.js CHANGED
@@ -33,6 +33,7 @@ export {
33
33
  getHtmlNodePosition,
34
34
  getHtmlNodeAttributePosition,
35
35
  } from "./html/html_node_position.js";
36
+ export { getUrlForContentInsideHtml } from "./html/html_inline_content_url.js";
36
37
 
37
38
  // css
38
39
  export { parseCssUrls } from "./css/parse_css_urls.js";
@@ -43,3 +44,6 @@ export { injectJsImport } from "./js/babel_utils.js";
43
44
  export { parseJsWithAcorn } from "./js/parse_js_with_acorn.js";
44
45
  export { parseJsUrls } from "./js/parse_js_urls.js";
45
46
  export { visitJsAstUntil } from "./js/visit_js_ast_until.js";
47
+ export { getUrlForContentInsideJs } from "./js/js_inline_content_url.js";
48
+
49
+ export { generateUrlForInlineContent } from "./inline_content_url.js";