@jskit-ai/architecture-inspector 0.1.1 → 0.1.2

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,222 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+
4
+ // Syntax helpers over Babel trees: static values, bindings, scopes and the
5
+ // source evidence an observation cites.
6
+
7
+ const FUNCTION_NODE_PATTERN = /Function/u;
8
+
9
+ function hashBytes(source) {
10
+ return `sha256:${createHash("sha256").update(source).digest("hex")}`;
11
+ }
12
+
13
+ function isCanonicalPath(value) {
14
+ if (typeof value !== "string" || value.length === 0 || value === ".") {
15
+ return false;
16
+ }
17
+ if (/[\\\0]/u.test(value) || path.posix.isAbsolute(value)) {
18
+ return false;
19
+ }
20
+ return path.posix.normalize(value) === value && !value.split("/").includes("..");
21
+ }
22
+
23
+ function propertyKey(entry) {
24
+ return entry.key.name || entry.key.value;
25
+ }
26
+
27
+ function isStaticObjectProperty(entry) {
28
+ return entry.type === "ObjectProperty" && !entry.computed;
29
+ }
30
+
31
+ function objectProperty(node, key) {
32
+ if (node?.type !== "ObjectExpression") {
33
+ return undefined;
34
+ }
35
+ return node.properties.find((entry) => isStaticObjectProperty(entry) && propertyKey(entry) === key)?.value;
36
+ }
37
+
38
+ function hasOnlyStaticProperties(node) {
39
+ return node.properties.every(isStaticObjectProperty);
40
+ }
41
+
42
+ function isFunctionNode(node) {
43
+ return FUNCTION_NODE_PATTERN.test(node.type);
44
+ }
45
+
46
+ function directoryPrefixes(filePath) {
47
+ const parts = filePath.split("/");
48
+ return parts.map((_, index) => parts.slice(0, index + 1).join("/"));
49
+ }
50
+
51
+ // Every AST node with its ancestor chain, depth first.
52
+ // Node records link to their parent record. `ancestors` (outermost first) is
53
+ // built on demand, so a module's records stay linear in its size.
54
+ const NODE_RECORD = Object.freeze({
55
+ get ancestors() {
56
+ const chain = [];
57
+ for (let current = this.parent; current; current = current.parent) {
58
+ chain.push(current.node);
59
+ }
60
+ return chain.reverse();
61
+ }
62
+ });
63
+
64
+ function nodeRecord(node, parent) {
65
+ return Object.assign(Object.create(NODE_RECORD), { node, parent });
66
+ }
67
+
68
+ function collectNodes(root) {
69
+ const result = [];
70
+ const pending = [nodeRecord(root, null)];
71
+ while (pending.length) {
72
+ const current = pending.pop();
73
+ const { node } = current;
74
+ if (!node || typeof node.type !== "string") {
75
+ continue;
76
+ }
77
+ result.push(current);
78
+ for (const value of Object.values(node)) {
79
+ const children = Array.isArray(value) ? value : value?.type ? [value] : [];
80
+ for (const child of children) {
81
+ if (child?.type) {
82
+ pending.push(nodeRecord(child, current));
83
+ }
84
+ }
85
+ }
86
+ }
87
+ return result;
88
+ }
89
+
90
+ // The literal value of an expression, following local bindings; undefined when
91
+ // any part is dynamic.
92
+ function staticValue(node, bindings, seen = new Set(), environment = null) {
93
+ if (!node) {
94
+ return undefined;
95
+ }
96
+ if (node.type === "Identifier" && environment?.has(node.name)) {
97
+ return environment.get(node.name);
98
+ }
99
+ if (["StringLiteral", "BooleanLiteral", "NumericLiteral"].includes(node.type)) {
100
+ return node.value;
101
+ }
102
+ if (node.type === "NullLiteral") {
103
+ return null;
104
+ }
105
+ if (node.type === "Identifier" && !seen.has(node.name)) {
106
+ return staticValue(bindings.get(node.name), bindings, new Set([...seen, node.name]));
107
+ }
108
+ if (node.type === "ArrayExpression") {
109
+ const values = node.elements.map((entry) => staticValue(entry, bindings, seen, environment));
110
+ return values.every((value) => value !== undefined) ? values : undefined;
111
+ }
112
+ if (node.type === "ObjectExpression") {
113
+ return staticObjectValue(node, bindings, seen, environment);
114
+ }
115
+ if (node.type === "TemplateLiteral") {
116
+ const values = node.expressions.map((entry) => staticValue(entry, bindings, seen, environment));
117
+ if (values.some((value) => typeof value !== "string")) {
118
+ return undefined;
119
+ }
120
+ return node.quasis.map((entry, index) => entry.value.cooked + (values[index] || "")).join("");
121
+ }
122
+ if (node.type === "BinaryExpression" && node.operator === "+") {
123
+ const left = staticValue(node.left, bindings, seen, environment);
124
+ const right = staticValue(node.right, bindings, seen, environment);
125
+ return typeof left === "string" && typeof right === "string" ? left + right : undefined;
126
+ }
127
+ return undefined;
128
+ }
129
+
130
+ function staticObjectValue(node, bindings, seen, environment) {
131
+ const result = {};
132
+ for (const entry of node.properties) {
133
+ if (!isStaticObjectProperty(entry)) {
134
+ return undefined;
135
+ }
136
+ const value = staticValue(entry.value, bindings, seen, environment);
137
+ if (value === undefined) {
138
+ return undefined;
139
+ }
140
+ result[propertyKey(entry)] = value;
141
+ }
142
+ return result;
143
+ }
144
+
145
+ // The node an identifier is bound to locally, or the node itself.
146
+ function bindingNode(node, bindings, seen = new Set()) {
147
+ if (node?.type !== "Identifier" || seen.has(node.name) || !bindings.has(node.name)) {
148
+ return node;
149
+ }
150
+ return bindingNode(bindings.get(node.name), bindings, new Set([...seen, node.name]));
151
+ }
152
+
153
+ function isShadowed({ ancestors }, name) {
154
+ return ancestors.some((node) =>
155
+ node.params?.some((parameter) =>
156
+ collectNodes(parameter).some(({ node: binding }) => binding.type === "Identifier" && binding.name === name)
157
+ )
158
+ );
159
+ }
160
+
161
+ function isLocalCall(record, names) {
162
+ const { node } = record;
163
+ return node.type === "CallExpression" &&
164
+ node.callee.type === "Identifier" &&
165
+ names.has(node.callee.name) &&
166
+ !isShadowed(record, node.callee.name);
167
+ }
168
+
169
+ function importedNames(module, source, imported) {
170
+ const names = [...module.imports]
171
+ .filter(([, entry]) => entry.source === source && entry.imported === imported)
172
+ .map(([name]) => name);
173
+ return new Set(names);
174
+ }
175
+
176
+ function findRecord(module, node) {
177
+ return module.nodes.find((entry) => entry.node === node);
178
+ }
179
+
180
+ function kebabCase(name) {
181
+ return name.replace(/[A-Z]/gu, (letter, index) => `${index ? "-" : ""}${letter.toLowerCase()}`);
182
+ }
183
+
184
+ // The imported local name a template tag refers to, by exact or kebab-case name.
185
+ function templateTagImport(module, tag) {
186
+ return [...module.imports.keys()].find((name) => name === tag || kebabCase(name) === tag);
187
+ }
188
+
189
+ function sourcePoint(module, index) {
190
+ const lines = module.source.slice(0, index).split("\n");
191
+ return { line: lines.length, column: lines.at(-1).length + 1 };
192
+ }
193
+
194
+ function sourceEvidence(module, node, offset = 0) {
195
+ const sourceEnd = Math.max(1, module.source.length - (module.source.endsWith("\n") ? 1 : 0));
196
+ return [{
197
+ path: module.path,
198
+ hash: module.hash,
199
+ start: sourcePoint(module, offset + (node?.start || 0)),
200
+ end: sourcePoint(module, offset + (node?.end || sourceEnd))
201
+ }];
202
+ }
203
+
204
+ export {
205
+ bindingNode,
206
+ collectNodes,
207
+ directoryPrefixes,
208
+ findRecord,
209
+ hasOnlyStaticProperties,
210
+ hashBytes,
211
+ importedNames,
212
+ isCanonicalPath,
213
+ isFunctionNode,
214
+ isLocalCall,
215
+ isShadowed,
216
+ isStaticObjectProperty,
217
+ objectProperty,
218
+ propertyKey,
219
+ sourceEvidence,
220
+ staticValue,
221
+ templateTagImport
222
+ };