@jsenv/ast 1.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.
@@ -0,0 +1,122 @@
1
+ import {
2
+ getHtmlNodeAttribute,
3
+ setHtmlNodeAttributes,
4
+ } from "./html_node_attributes.js"
5
+
6
+ export const storeHtmlNodePosition = (node) => {
7
+ const originalPositionAttributeName = `original-position`
8
+ const originalPosition = getHtmlNodeAttribute(
9
+ node,
10
+ originalPositionAttributeName,
11
+ )
12
+ if (originalPosition !== undefined) {
13
+ return true
14
+ }
15
+ const { sourceCodeLocation } = node
16
+ if (!sourceCodeLocation) {
17
+ return false
18
+ }
19
+ const { startLine, startCol, endLine, endCol } = sourceCodeLocation
20
+ setHtmlNodeAttributes(node, {
21
+ [originalPositionAttributeName]: `${startLine}:${startCol};${endLine}:${endCol}`,
22
+ })
23
+ return true
24
+ }
25
+ export const storeHtmlNodeAttributePosition = (node, attributeName) => {
26
+ const { sourceCodeLocation } = node
27
+ if (!sourceCodeLocation) {
28
+ return false
29
+ }
30
+ const attributeValue = getHtmlNodeAttribute(node, attributeName)
31
+ if (attributeValue === undefined) {
32
+ return false
33
+ }
34
+ const attributeLocation = sourceCodeLocation.attrs[attributeName]
35
+ if (!attributeLocation) {
36
+ return false
37
+ }
38
+ const originalPositionAttributeName = `original-${attributeName}-position`
39
+ const originalPosition = getHtmlNodeAttribute(
40
+ node,
41
+ originalPositionAttributeName,
42
+ )
43
+ if (originalPosition !== undefined) {
44
+ return true
45
+ }
46
+ const { startLine, startCol, endLine, endCol } = attributeLocation
47
+ setHtmlNodeAttributes(node, {
48
+ [originalPositionAttributeName]: `${startLine}:${startCol};${endLine}:${endCol}`,
49
+ })
50
+ return true
51
+ }
52
+
53
+ export const getHtmlNodePosition = (node, { preferOriginal = false } = {}) => {
54
+ const position = {}
55
+ const { sourceCodeLocation } = node
56
+ if (sourceCodeLocation) {
57
+ const { startLine, startCol, endLine, endCol } = sourceCodeLocation
58
+ Object.assign(position, {
59
+ line: startLine,
60
+ lineEnd: endLine,
61
+ column: startCol,
62
+ columnEnd: endCol,
63
+ })
64
+ }
65
+ const originalPosition = getHtmlNodeAttribute(node, "original-position")
66
+ if (originalPosition === undefined) {
67
+ return position
68
+ }
69
+ const [start, end] = originalPosition.split(";")
70
+ const [originalLine, originalColumn] = start.split(":")
71
+ const [originalLineEnd, originalColumnEnd] = end.split(":")
72
+ Object.assign(position, {
73
+ originalLine: parseInt(originalLine),
74
+ originalColumn: parseInt(originalColumn),
75
+ originalLineEnd: parseInt(originalLineEnd),
76
+ originalColumnEnd: parseInt(originalColumnEnd),
77
+ })
78
+ if (preferOriginal) {
79
+ position.line = position.originalLine
80
+ position.column = position.originalColumn
81
+ position.lineEnd = position.originalLineEnd
82
+ position.columnEnd = position.originalColumnEnd
83
+ position.isOriginal = true
84
+ }
85
+ return position
86
+ }
87
+ export const getHtmlNodeAttributePosition = (node, attributeName) => {
88
+ const position = {}
89
+ const { sourceCodeLocation } = node
90
+ if (sourceCodeLocation) {
91
+ const attributeLocation = sourceCodeLocation.attrs[attributeName]
92
+ if (attributeLocation) {
93
+ Object.assign(position, {
94
+ line: attributeLocation.startLine,
95
+ column: attributeLocation.startCol,
96
+ })
97
+ }
98
+ }
99
+ const originalPositionAttributeName =
100
+ attributeName === "generated-from-src"
101
+ ? "original-src-position"
102
+ : attributeName === "generated-from-href"
103
+ ? "original-href-position"
104
+ : `original-${attributeName}-position`
105
+ const originalPosition = getHtmlNodeAttribute(
106
+ node,
107
+ originalPositionAttributeName,
108
+ )
109
+ if (originalPosition === undefined) {
110
+ return position
111
+ }
112
+ const [start, end] = originalPosition.split(";")
113
+ const [originalLine, originalColumn] = start.split(":")
114
+ const [originalLineEnd, originalColumnEnd] = end.split(":")
115
+ Object.assign(position, {
116
+ originalLine: parseInt(originalLine),
117
+ originalColumn: parseInt(originalColumn),
118
+ originalLineEnd: parseInt(originalLineEnd),
119
+ originalColumnEnd: parseInt(originalColumnEnd),
120
+ })
121
+ return position
122
+ }
@@ -0,0 +1,32 @@
1
+ export const getHtmlNodeText = (htmlNode) => {
2
+ const textNode = getTextNode(htmlNode)
3
+ return textNode ? textNode.value : undefined
4
+ }
5
+
6
+ const getTextNode = (htmlNode) => {
7
+ const firstChild = htmlNode.childNodes[0]
8
+ const textNode =
9
+ firstChild && firstChild.nodeName === "#text" ? firstChild : null
10
+ return textNode
11
+ }
12
+
13
+ export const removeHtmlNodeText = (htmlNode) => {
14
+ const textNode = getTextNode(htmlNode)
15
+ if (textNode) {
16
+ htmlNode.childNodes = []
17
+ }
18
+ }
19
+
20
+ export const setHtmlNodeText = (htmlNode, textContent) => {
21
+ const textNode = getTextNode(htmlNode)
22
+ if (textNode) {
23
+ textNode.value = textContent
24
+ } else {
25
+ const newTextNode = {
26
+ nodeName: "#text",
27
+ value: textContent,
28
+ parentNode: htmlNode,
29
+ }
30
+ htmlNode.childNodes.splice(0, 0, newTextNode)
31
+ }
32
+ }
@@ -0,0 +1,89 @@
1
+ import { parse, serialize, parseFragment } from "parse5"
2
+
3
+ import {
4
+ getHtmlNodeAttribute,
5
+ setHtmlNodeAttributes,
6
+ } from "./html_node_attributes.js"
7
+ import {
8
+ storeHtmlNodePosition,
9
+ storeHtmlNodeAttributePosition,
10
+ } from "./html_node_position.js"
11
+ import { findHtmlChildNode, visitHtmlNodes } from "./html_search.js"
12
+ import { getHtmlNodeText } from "./html_node_text.js"
13
+
14
+ export const parseHtmlString = (
15
+ htmlString,
16
+ { storeOriginalPositions = true } = {},
17
+ ) => {
18
+ const htmlAst = parse(htmlString, { sourceCodeLocationInfo: true })
19
+ if (storeOriginalPositions) {
20
+ const htmlNode = findHtmlChildNode(
21
+ htmlAst,
22
+ (node) => node.nodeName === "html",
23
+ )
24
+ const stored = getHtmlNodeAttribute(htmlNode, "original-position-stored")
25
+ if (stored === undefined) {
26
+ visitHtmlNodes(htmlAst, {
27
+ "*": (node) => {
28
+ if (node.nodeName === "script" || node.nodeName === "style") {
29
+ const htmlNodeText = getHtmlNodeText(node)
30
+ if (htmlNodeText !== undefined) {
31
+ storeHtmlNodePosition(node)
32
+ }
33
+ }
34
+ storeHtmlNodeAttributePosition(node, "src")
35
+ storeHtmlNodeAttributePosition(node, "href")
36
+ },
37
+ })
38
+ setHtmlNodeAttributes(htmlNode, {
39
+ "original-position-stored": "",
40
+ })
41
+ }
42
+ }
43
+ return htmlAst
44
+ }
45
+
46
+ export const stringifyHtmlAst = (
47
+ htmlAst,
48
+ { removeOriginalPositionAttributes = false } = {},
49
+ ) => {
50
+ if (removeOriginalPositionAttributes) {
51
+ const htmlNode = findHtmlChildNode(
52
+ htmlAst,
53
+ (node) => node.nodeName === "html",
54
+ )
55
+ const storedAttribute = getHtmlNodeAttribute(
56
+ htmlNode,
57
+ "original-position-stored",
58
+ )
59
+ if (storedAttribute !== undefined) {
60
+ setHtmlNodeAttributes(htmlNode, {
61
+ "original-position-stored": undefined,
62
+ })
63
+ visitHtmlNodes(htmlAst, {
64
+ "*": (node) => {
65
+ setHtmlNodeAttributes(node, {
66
+ "original-position": undefined,
67
+ "original-src-position": undefined,
68
+ "original-href-position": undefined,
69
+ "injected-by": undefined,
70
+ "generated-by": undefined,
71
+ "generated-from-src": undefined,
72
+ "generated-from-href": undefined,
73
+ })
74
+ },
75
+ })
76
+ }
77
+ }
78
+ const htmlString = serialize(htmlAst)
79
+
80
+ return htmlString
81
+ }
82
+
83
+ export const parseSvgString = (svgString) => {
84
+ const svgAst = parseFragment(svgString, {
85
+ sourceCodeLocationInfo: true,
86
+ })
87
+ return svgAst
88
+ }
89
+ export const stringifySvgAst = stringifyHtmlAst
@@ -0,0 +1,64 @@
1
+ import { getHtmlNodeAttribute } from "./html_node_attributes.js"
2
+
3
+ export const visitHtmlNodes = (htmlAst, visitors) => {
4
+ const visitNode = (node) => {
5
+ const visitor = visitors[node.nodeName] || visitors["*"]
6
+ if (visitor) {
7
+ const callbackReturnValue = visitor(node)
8
+ if (callbackReturnValue === "stop") {
9
+ return
10
+ }
11
+ }
12
+ const { childNodes } = node
13
+ if (childNodes) {
14
+ let i = 0
15
+ while (i < childNodes.length) {
16
+ visitNode(childNodes[i++])
17
+ }
18
+ }
19
+ }
20
+ visitNode(htmlAst)
21
+ }
22
+
23
+ export const findHtmlNode = (htmlAst, predicate) => {
24
+ let nodeMatching = null
25
+ visitHtmlNodes(htmlAst, {
26
+ "*": (node) => {
27
+ if (predicate(node)) {
28
+ nodeMatching = node
29
+ return "stop"
30
+ }
31
+ return null
32
+ },
33
+ })
34
+ return nodeMatching
35
+ }
36
+
37
+ export const findHtmlChildNode = (htmlNode, predicate) => {
38
+ const { childNodes = [] } = htmlNode
39
+ return childNodes.find(predicate)
40
+ }
41
+
42
+ export const findHtmlNodes = (htmlAst, predicate) => {
43
+ const nodes = []
44
+ visitHtmlNodes(htmlAst, {
45
+ "*": (node) => {
46
+ if (predicate(node)) {
47
+ nodes.push(node)
48
+ }
49
+ return null
50
+ },
51
+ })
52
+ return nodes
53
+ }
54
+
55
+ export const findHtmlNodeByTagName = (htmlAst, tagName) => {
56
+ return findHtmlNode(htmlAst, (node) => node.nodeName === tagName)
57
+ }
58
+
59
+ export const findHtmlNodeById = (htmlAst, id) => {
60
+ return findHtmlNode(htmlAst, (node) => {
61
+ const idCandidate = getHtmlNodeAttribute(node, "id")
62
+ return idCandidate === id
63
+ })
64
+ }
@@ -0,0 +1,18 @@
1
+ export const parseSrcSet = (srcset) => {
2
+ const srcCandidates = []
3
+ srcset.split(",").forEach((set) => {
4
+ const [specifier, descriptor] = set.trim().split(" ")
5
+ srcCandidates.push({
6
+ specifier,
7
+ descriptor,
8
+ })
9
+ })
10
+ return srcCandidates
11
+ }
12
+
13
+ export const stringifySrcSet = (srcCandidates) => {
14
+ const srcset = srcCandidates
15
+ .map(({ specifier, descriptor }) => `${specifier} ${descriptor}`)
16
+ .join(", ")
17
+ return srcset
18
+ }
@@ -0,0 +1,100 @@
1
+ /*
2
+ * Useful when writin a babel plugin:
3
+ * - https://astexplorer.net/
4
+ * - https://bvaughn.github.io/babel-repl
5
+ */
6
+ import { fileURLToPath } from "node:url"
7
+
8
+ import { createJsParseError } from "./js_parse_error.js"
9
+
10
+ export const applyBabelPlugins = async ({
11
+ babelPlugins,
12
+ urlInfo,
13
+ ast,
14
+ options = {},
15
+ }) => {
16
+ const sourceType = {
17
+ js_module: "module",
18
+ js_classic: "classic",
19
+ [urlInfo.type]: undefined,
20
+ }[urlInfo.type]
21
+ const url = urlInfo.originalUrl
22
+ const generatedUrl = urlInfo.generatedUrl
23
+ const content = urlInfo.content
24
+
25
+ if (babelPlugins.length === 0) {
26
+ return { code: content }
27
+ }
28
+ const { transformAsync, transformFromAstAsync } = await import("@babel/core")
29
+ const sourceFileName = url.startsWith("file:")
30
+ ? fileURLToPath(url)
31
+ : undefined
32
+ options = {
33
+ ast: false,
34
+ // https://babeljs.io/docs/en/options#source-map-options
35
+ sourceMaps: true,
36
+ sourceFileName,
37
+ filename: generatedUrl
38
+ ? generatedUrl.startsWith("file:")
39
+ ? fileURLToPath(url)
40
+ : undefined
41
+ : sourceFileName,
42
+ configFile: false,
43
+ babelrc: false,
44
+ highlightCode: false,
45
+ // consider using startColumn and startLine for inline scripts?
46
+ // see https://github.com/babel/babel/blob/3ee9db7afe741f4d2f7933c519d8e7672fccb08d/packages/babel-parser/src/options.js#L36-L39
47
+ parserOpts: {
48
+ sourceType,
49
+ // allowAwaitOutsideFunction: true,
50
+ plugins: [
51
+ // "importMeta",
52
+ // "topLevelAwait",
53
+ "dynamicImport",
54
+ "importAssertions",
55
+ "jsx",
56
+ "classProperties",
57
+ "classPrivateProperties",
58
+ "classPrivateMethods",
59
+ ...(useTypeScriptExtension(url) ? ["typescript"] : []),
60
+ ...(options.parserPlugins || []),
61
+ ].filter(Boolean),
62
+ },
63
+ generatorOpts: {
64
+ compact: false,
65
+ },
66
+ plugins: babelPlugins,
67
+ ...options,
68
+ }
69
+ try {
70
+ if (ast) {
71
+ const result = await transformFromAstAsync(ast, content, options)
72
+ return result
73
+ }
74
+ const result = await transformAsync(content, options)
75
+ return result
76
+ } catch (error) {
77
+ if (error && error.code === "BABEL_PARSE_ERROR") {
78
+ throw createJsParseError({
79
+ message: error.message,
80
+ reasonCode: error.reasonCode,
81
+ content,
82
+ url,
83
+ line: error.loc.line,
84
+ column: error.loc.column,
85
+ })
86
+ }
87
+ throw error
88
+ }
89
+ }
90
+
91
+ const useTypeScriptExtension = (url) => {
92
+ const { pathname } = new URL(url)
93
+ return pathname.endsWith(".ts") || pathname.endsWith(".tsx")
94
+ }
95
+
96
+ // const pattern = [
97
+ // "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
98
+ // "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))",
99
+ // ].join("|")
100
+ // const ansiRegex = new RegExp(pattern, "g")
@@ -0,0 +1,64 @@
1
+ import { require } from "../require.js"
2
+
3
+ // https://github.com/babel/babel/tree/master/packages/babel-helper-module-imports
4
+ export const injectJsImport = ({
5
+ programPath,
6
+ namespace,
7
+ name,
8
+ from,
9
+ nameHint,
10
+ sideEffect,
11
+ }) => {
12
+ const {
13
+ addNamespace,
14
+ addDefault,
15
+ addNamed,
16
+ addSideEffect,
17
+ } = require("@babel/helper-module-imports")
18
+
19
+ if (namespace) {
20
+ return addNamespace(programPath, from, {
21
+ nameHint,
22
+ })
23
+ }
24
+ if (name) {
25
+ return addNamed(programPath, name, from)
26
+ }
27
+ if (sideEffect) {
28
+ return addSideEffect(programPath, from)
29
+ }
30
+ return addDefault(programPath, from, {
31
+ nameHint,
32
+ })
33
+ }
34
+
35
+ const generateExpressionAst = (expression, options) => {
36
+ const { parseExpression } = require("@babel/parser")
37
+
38
+ const ast = parseExpression(expression, options)
39
+ return ast
40
+ }
41
+
42
+ export const generateValueAst = (value) => {
43
+ const valueAst = generateExpressionAst(
44
+ value === undefined
45
+ ? "undefined"
46
+ : value === null
47
+ ? "null"
48
+ : JSON.stringify(value),
49
+ )
50
+ return valueAst
51
+ }
52
+
53
+ export const injectAstAfterImport = (programPath, ast) => {
54
+ const bodyNodePaths = programPath.get("body")
55
+ const notAnImportIndex = bodyNodePaths.findIndex(
56
+ (bodyNodePath) => bodyNodePath.node.type !== "ImportDeclaration",
57
+ )
58
+ const notAnImportNodePath = bodyNodePaths[notAnImportIndex]
59
+ if (notAnImportNodePath) {
60
+ notAnImportNodePath.insertBefore(ast)
61
+ } else {
62
+ bodyNodePaths[0].insertBefore(ast)
63
+ }
64
+ }
@@ -0,0 +1,15 @@
1
+ export const createJsParseError = ({
2
+ message,
3
+ reasonCode,
4
+ url,
5
+ line,
6
+ column,
7
+ }) => {
8
+ const parseError = new Error(message)
9
+ parseError.reasonCode = reasonCode
10
+ parseError.code = "PARSE_ERROR"
11
+ parseError.url = url
12
+ parseError.line = line
13
+ parseError.column = column
14
+ return parseError
15
+ }
@@ -0,0 +1,17 @@
1
+ export const getTypePropertyNode = (node) => {
2
+ if (node.type !== "ObjectExpression") {
3
+ return null
4
+ }
5
+ const { properties } = node
6
+ return properties.find((property) => {
7
+ return (
8
+ property.type === "Property" &&
9
+ property.key.type === "Identifier" &&
10
+ property.key.name === "type"
11
+ )
12
+ })
13
+ }
14
+
15
+ export const isStringLiteralNode = (node) => {
16
+ return node.type === "Literal" && typeof node.value === "string"
17
+ }
@@ -0,0 +1,135 @@
1
+ import { isStringLiteralNode } from "./helpers.js"
2
+
3
+ export const analyzeImportDeclaration = (node, { onUrl }) => {
4
+ const specifierNode = node.source
5
+ const assertionInfo = extractImportAssertionsInfo(node)
6
+ onUrl({
7
+ type: "js_import_export",
8
+ subtype: "import_static",
9
+ specifier: specifierNode.value,
10
+ specifierStart: specifierNode.start,
11
+ specifierEnd: specifierNode.end,
12
+ specifierLine: specifierNode.loc.start.line,
13
+ specifierColumn: specifierNode.loc.start.column,
14
+ expectedType: assertionInfo ? assertionInfo.assert.type : "js_module",
15
+ ...assertionInfo,
16
+ })
17
+ }
18
+ export const analyzeImportExpression = (node, { onUrl }) => {
19
+ const specifierNode = node.source
20
+ if (!isStringLiteralNode(specifierNode)) {
21
+ return
22
+ }
23
+ const assertionInfo = extractImportAssertionsInfo(node)
24
+
25
+ onUrl({
26
+ type: "js_import_export",
27
+ subtype: "import_dynamic",
28
+ specifier: specifierNode.value,
29
+ specifierStart: specifierNode.start,
30
+ specifierEnd: specifierNode.end,
31
+ specifierLine: specifierNode.loc.start.line,
32
+ specifierColumn: specifierNode.loc.start.column,
33
+ expectedType: assertionInfo ? assertionInfo.assert.type : "js_module",
34
+ ...assertionInfo,
35
+ })
36
+ }
37
+ export const analyzeExportNamedDeclaration = (node, { onUrl }) => {
38
+ const specifierNode = node.source
39
+ if (!specifierNode) {
40
+ // This export has no "source", so it's probably
41
+ // a local variable or function, e.g.
42
+ // export { varName }
43
+ // export const constName = ...
44
+ // export function funcName() {}
45
+ return
46
+ }
47
+ onUrl({
48
+ type: "js_import_export",
49
+ subtype: "export_named",
50
+ specifier: specifierNode.value,
51
+ specifierStart: specifierNode.start,
52
+ specifierEnd: specifierNode.end,
53
+ specifierLine: specifierNode.loc.start.line,
54
+ specifierColumn: specifierNode.loc.start.column,
55
+ })
56
+ }
57
+ export const analyzeExportAllDeclaration = (node, { onUrl }) => {
58
+ const specifierNode = node.source
59
+ onUrl({
60
+ type: "js_import_export",
61
+ subtype: "export_all",
62
+ specifier: specifierNode.value,
63
+ specifierStart: specifierNode.start,
64
+ specifierEnd: specifierNode.end,
65
+ specifierLine: specifierNode.loc.start.line,
66
+ specifierColumn: specifierNode.loc.start.column,
67
+ })
68
+ }
69
+
70
+ const extractImportAssertionsInfo = (node) => {
71
+ if (node.type === "ImportDeclaration") {
72
+ // static import
73
+ const { assertions } = node
74
+ if (!assertions) {
75
+ return null
76
+ }
77
+ if (assertions.length === 0) {
78
+ return null
79
+ }
80
+ const typeAssertionNode = assertions.find(
81
+ (assertion) => assertion.key.name === "type",
82
+ )
83
+ if (!typeAssertionNode) {
84
+ return null
85
+ }
86
+ const typeNode = typeAssertionNode.value
87
+ if (!isStringLiteralNode(typeNode)) {
88
+ return null
89
+ }
90
+ return {
91
+ assertNode: typeAssertionNode,
92
+ assert: {
93
+ type: typeNode.value,
94
+ },
95
+ }
96
+ }
97
+ // dynamic import
98
+ const args = node.arguments
99
+ if (!args) {
100
+ // acorn keeps node.arguments undefined for dynamic import without a second argument
101
+ return null
102
+ }
103
+ const firstArgNode = args[0]
104
+ if (!firstArgNode) {
105
+ return null
106
+ }
107
+ const { properties } = firstArgNode
108
+ const assertProperty = properties.find((property) => {
109
+ return property.key.name === "assert"
110
+ })
111
+ if (!assertProperty) {
112
+ return null
113
+ }
114
+ const assertValueNode = assertProperty.value
115
+ if (assertValueNode.type !== "ObjectExpression") {
116
+ return null
117
+ }
118
+ const assertValueProperties = assertValueNode.properties
119
+ const typePropertyNode = assertValueProperties.find((property) => {
120
+ return property.key.name === "type"
121
+ })
122
+ if (!typePropertyNode) {
123
+ return null
124
+ }
125
+ const typePropertyValue = typePropertyNode.value
126
+ if (!isStringLiteralNode(typePropertyValue)) {
127
+ return null
128
+ }
129
+ return {
130
+ assertNode: firstArgNode,
131
+ assert: {
132
+ type: typePropertyValue.value,
133
+ },
134
+ }
135
+ }