universal-ast-mapper 1.1.0 → 1.3.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/README.md +4 -0
- package/dist/extractors/typescript.js +37 -2
- package/dist/graph.js +5 -3
- package/dist/resolver.js +8 -3
- package/dist/workspace.js +123 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -326,6 +326,8 @@ Scan a file or directory for **named functions/methods with parameters that are
|
|
|
326
326
|
|
|
327
327
|
---
|
|
328
328
|
|
|
329
|
+
> **Monorepo note:** once a workspace is detected, `resolve_imports` and `build_symbol_graph` resolve cross-package imports (`@org/pkg`) to real source files and draw cross-package edges.
|
|
330
|
+
|
|
329
331
|
### `analyze_workspace`
|
|
330
332
|
**Monorepo support.** Discover the packages in a JS/TS monorepo (npm/yarn `workspaces`, `pnpm-workspace.yaml`, or `lerna.json`) and the dependency edges between them. Returns each package's name, directory, and workspace-internal dependencies, plus any circular dependencies between packages.
|
|
331
333
|
|
|
@@ -603,6 +605,8 @@ Not part of the public API: the internal `src/` module layout and the generated
|
|
|
603
605
|
|
|
604
606
|
| Version | What changed |
|
|
605
607
|
|---------|--------------|
|
|
608
|
+
| **1.3.0** | **TS/JS decorators** — class and method symbols now carry a `decorators` field (`@Component({...})`, `@Injectable()`, `@Get("/x")`), in skeletons and `get_call_graph`. Extends the Python decorator support (v0.8.7) to TypeScript/JavaScript — traces Angular/NestJS-style framework wiring to its class/handler. |
|
|
609
|
+
| **1.2.0** | **File-level cross-package resolution** — in a monorepo, bare imports of a workspace package (`@org/utils`, `@org/utils/sub`) now resolve to the actual source file (preferring `src/` over built `dist/`), so `resolve_imports` marks them in-project and `build_symbol_graph` draws cross-package edges. Builds on the v1.1.0 workspace discovery. |
|
|
606
610
|
| **1.1.0** | **Monorepo support** — new `analyze_workspace` MCP tool + `ast-map workspace` (alias `ws`) CLI: discovers packages from npm/yarn `workspaces`, `pnpm-workspace.yaml`, or `lerna.json`, maps internal package dependencies, and flags circular package deps. **19 MCP tools**. |
|
|
607
611
|
| **1.0.0** | **Stable release.** Locks the public API (MCP tool names + schemas, CLI surface) for the 1.x line. Adds a **GitHub Action** (`action.yml`) to run `ast-map validate` as a CI architecture gate, plus a project CI workflow. Caps a 12-language engine with 18 MCP tools / 17 CLI commands spanning skeletons, dependency graphs, and deep analysis (dead code · cycles · impact · complexity · duplicates · unused params · decorators · type flow). |
|
|
608
612
|
| **0.9.0** | **Scoped type-flow tracing** — new `trace_type` MCP tool + `ast-map trace-type` (alias `flow`) CLI: follow a named type through function params, return types, typed variables, and class fields across a directory. Completes the deeper-analysis suite (dead code · cycles · impact · complexity · duplicates · unused params · type flow). **18 MCP tools**. |
|
|
@@ -47,7 +47,7 @@ function handle(node, exported, typeIndex) {
|
|
|
47
47
|
const name = nameOf(node) ?? "(anonymous class)";
|
|
48
48
|
const body = node.childForFieldName("body");
|
|
49
49
|
const children = body ? collect(namedChildren(body), false, typeIndex) : [];
|
|
50
|
-
|
|
50
|
+
const clsSym = makeSymbol({
|
|
51
51
|
name,
|
|
52
52
|
kind: "class",
|
|
53
53
|
node,
|
|
@@ -56,6 +56,8 @@ function handle(node, exported, typeIndex) {
|
|
|
56
56
|
doc: leadingComment(node),
|
|
57
57
|
children,
|
|
58
58
|
});
|
|
59
|
+
attachDecorators(clsSym, node);
|
|
60
|
+
return clsSym;
|
|
59
61
|
}
|
|
60
62
|
case "interface_declaration": {
|
|
61
63
|
const name = nameOf(node) ?? "(anonymous interface)";
|
|
@@ -114,7 +116,7 @@ function handle(node, exported, typeIndex) {
|
|
|
114
116
|
case "abstract_method_signature": {
|
|
115
117
|
const name = nameOf(node) ?? "(method)";
|
|
116
118
|
const body = node.childForFieldName("body");
|
|
117
|
-
|
|
119
|
+
const mSym = makeSymbol({
|
|
118
120
|
name,
|
|
119
121
|
kind: "method",
|
|
120
122
|
node,
|
|
@@ -123,6 +125,8 @@ function handle(node, exported, typeIndex) {
|
|
|
123
125
|
visibility: memberVisibility(node),
|
|
124
126
|
doc: leadingComment(node),
|
|
125
127
|
});
|
|
128
|
+
attachDecorators(mSym, node);
|
|
129
|
+
return mSym;
|
|
126
130
|
}
|
|
127
131
|
case "public_field_definition":
|
|
128
132
|
case "field_definition": {
|
|
@@ -454,3 +458,34 @@ function attachComponentInfo(sym, funcNode, declNode, name, idx) {
|
|
|
454
458
|
if (resolved)
|
|
455
459
|
sym.props = resolved;
|
|
456
460
|
}
|
|
461
|
+
// ─── TS/JS decorators ─────────────────────────────────────────────────────────
|
|
462
|
+
/** Strip the leading `@` and collapse whitespace from a decorator node. */
|
|
463
|
+
function decoratorText(node) {
|
|
464
|
+
return node.text.replace(/^@\s*/, "").replace(/\s+/g, " ").trim();
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Attach decorators to a class/method symbol. TS decorators appear either as
|
|
468
|
+
* preceding sibling `decorator` nodes (classes, methods) or as leading child
|
|
469
|
+
* decorators (some grammars) — collect both.
|
|
470
|
+
*/
|
|
471
|
+
function attachDecorators(sym, node) {
|
|
472
|
+
const decs = [];
|
|
473
|
+
// leading child decorators
|
|
474
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
475
|
+
const c = node.namedChild(i);
|
|
476
|
+
if (c && c.type === "decorator")
|
|
477
|
+
decs.push(decoratorText(c));
|
|
478
|
+
else if (c && c.type !== "decorator")
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
// preceding sibling decorators (most common for classes/methods)
|
|
482
|
+
let prev = node.previousNamedSibling;
|
|
483
|
+
const lead = [];
|
|
484
|
+
while (prev && prev.type === "decorator") {
|
|
485
|
+
lead.unshift(decoratorText(prev));
|
|
486
|
+
prev = prev.previousNamedSibling;
|
|
487
|
+
}
|
|
488
|
+
const all = [...lead, ...decs].filter((t) => t.length > 0);
|
|
489
|
+
if (all.length > 0)
|
|
490
|
+
sym.decorators = all;
|
|
491
|
+
}
|
package/dist/graph.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { resolveImportPath } from "./resolver.js";
|
|
3
|
+
import { resolveWorkspaceImportCached } from "./workspace.js";
|
|
3
4
|
import { buildCrossLangIndex, resolveCrossLangTarget, } from "./crosslang.js";
|
|
4
5
|
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
5
6
|
function collectSymbolNodes(symbols, parentId, file, nodes, edges) {
|
|
@@ -30,11 +31,12 @@ function isPathBasedLanguage(language) {
|
|
|
30
31
|
}
|
|
31
32
|
// Wire one TS/JS/Python-style relative import.
|
|
32
33
|
function wirePathImport(skel, imp, fromFileAbs, root, exportedSymbolMap, edges) {
|
|
33
|
-
if (!imp.from.startsWith("."))
|
|
34
|
-
return;
|
|
35
34
|
if (imp.isSideEffect)
|
|
36
35
|
return;
|
|
37
|
-
|
|
36
|
+
// Relative import → path resolve; bare specifier → monorepo workspace package.
|
|
37
|
+
const resolvedAbs = imp.from.startsWith(".")
|
|
38
|
+
? resolveImportPath(imp.from, fromFileAbs)
|
|
39
|
+
: resolveWorkspaceImportCached(imp.from, root);
|
|
38
40
|
if (!resolvedAbs)
|
|
39
41
|
return;
|
|
40
42
|
const resolvedRel = path.relative(root, resolvedAbs).split(path.sep).join("/");
|
package/dist/resolver.js
CHANGED
|
@@ -4,6 +4,7 @@ import { buildSkeleton, collectSourceFiles } from "./skeleton.js";
|
|
|
4
4
|
import { resolveOptions } from "./config.js";
|
|
5
5
|
import { findSymbol } from "./analysis.js";
|
|
6
6
|
import { buildCrossLangIndex, resolveCrossLangTarget, } from "./crosslang.js";
|
|
7
|
+
import { resolveWorkspaceImportCached } from "./workspace.js";
|
|
7
8
|
const SRC_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"];
|
|
8
9
|
function extractParams(sig) {
|
|
9
10
|
const start = sig.indexOf("(");
|
|
@@ -120,8 +121,12 @@ async function lookupSymbolInTarget(targetAbs, targetRel, symbol) {
|
|
|
120
121
|
return { found: false };
|
|
121
122
|
}
|
|
122
123
|
async function enrichRelativeImport(imp, fromAbs, root) {
|
|
123
|
-
const
|
|
124
|
-
|
|
124
|
+
const isBare = !imp.from.startsWith(".");
|
|
125
|
+
// Relative import → path resolve; bare specifier → try monorepo workspace.
|
|
126
|
+
let resolvedAbs = isBare ? null : resolveImportPath(imp.from, fromAbs);
|
|
127
|
+
if (!resolvedAbs && isBare)
|
|
128
|
+
resolvedAbs = resolveWorkspaceImportCached(imp.from, root);
|
|
129
|
+
const treatedExternal = isBare && !resolvedAbs;
|
|
125
130
|
const resolvedRel = resolvedAbs
|
|
126
131
|
? path.relative(root, resolvedAbs).split(path.sep).join("/")
|
|
127
132
|
: null;
|
|
@@ -132,7 +137,7 @@ async function enrichRelativeImport(imp, fromAbs, root) {
|
|
|
132
137
|
else if (resolvedAbs) {
|
|
133
138
|
enrichment = { found: true };
|
|
134
139
|
}
|
|
135
|
-
return assembleResolved(imp, resolvedAbs, resolvedRel,
|
|
140
|
+
return assembleResolved(imp, resolvedAbs, resolvedRel, treatedExternal, enrichment);
|
|
136
141
|
}
|
|
137
142
|
async function enrichCrossLangImport(imp, skel, fromAbs, root, index) {
|
|
138
143
|
const target = resolveCrossLangTarget(imp, skel, fromAbs, root, index);
|
package/dist/workspace.js
CHANGED
|
@@ -205,3 +205,126 @@ export function findPackageCycles(info) {
|
|
|
205
205
|
dfs(k);
|
|
206
206
|
return cycles;
|
|
207
207
|
}
|
|
208
|
+
/* ─── File-level cross-package import resolution ───────────────────────────── */
|
|
209
|
+
const SRC_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
210
|
+
const JS_TO_SRC = {
|
|
211
|
+
".js": [".ts", ".tsx", ".js"],
|
|
212
|
+
".jsx": [".tsx", ".jsx"],
|
|
213
|
+
".mjs": [".mts", ".mjs"],
|
|
214
|
+
".cjs": [".cts", ".cjs"],
|
|
215
|
+
};
|
|
216
|
+
function existsFileSync(p) {
|
|
217
|
+
try {
|
|
218
|
+
return fs.statSync(p).isFile();
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Resolve a candidate path (file, extensionless, or directory) to a real source
|
|
226
|
+
* file, preferring TS sources over the declared `.js` build output.
|
|
227
|
+
*/
|
|
228
|
+
function resolveSourceFile(candidate) {
|
|
229
|
+
const ext = path.extname(candidate).toLowerCase();
|
|
230
|
+
if (ext && JS_TO_SRC[ext]) {
|
|
231
|
+
const base = candidate.slice(0, candidate.length - ext.length);
|
|
232
|
+
for (const e of JS_TO_SRC[ext]) {
|
|
233
|
+
if (existsFileSync(base + e))
|
|
234
|
+
return base + e;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (existsFileSync(candidate))
|
|
238
|
+
return candidate;
|
|
239
|
+
for (const e of SRC_EXTS)
|
|
240
|
+
if (existsFileSync(candidate + e))
|
|
241
|
+
return candidate + e;
|
|
242
|
+
for (const e of SRC_EXTS) {
|
|
243
|
+
const idx = path.join(candidate, `index${e}`);
|
|
244
|
+
if (existsFileSync(idx))
|
|
245
|
+
return idx;
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
/** Pick the source entry file of a package, preferring real source over dist. */
|
|
250
|
+
function packageEntry(pkgDir, pkg) {
|
|
251
|
+
const candidates = [];
|
|
252
|
+
// explicit source-ish fields first
|
|
253
|
+
for (const f of [pkg?.source, pkg?.module, pkg?.types, pkg?.typings, pkg?.main]) {
|
|
254
|
+
if (typeof f === "string")
|
|
255
|
+
candidates.push(f);
|
|
256
|
+
}
|
|
257
|
+
// exports["."] as a string or { source/import/default/types }
|
|
258
|
+
const dot = pkg?.exports?.["."] ?? pkg?.exports;
|
|
259
|
+
if (typeof dot === "string")
|
|
260
|
+
candidates.push(dot);
|
|
261
|
+
else if (dot && typeof dot === "object") {
|
|
262
|
+
for (const k of ["source", "import", "default", "types"]) {
|
|
263
|
+
if (typeof dot[k] === "string")
|
|
264
|
+
candidates.push(dot[k]);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// conventional source roots
|
|
268
|
+
candidates.push("src/index.ts", "src/index.tsx", "src/index.js", "index.ts", "index.tsx", "index.js");
|
|
269
|
+
for (const c of candidates) {
|
|
270
|
+
const resolved = resolveSourceFile(path.resolve(pkgDir, c));
|
|
271
|
+
if (resolved)
|
|
272
|
+
return resolved;
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Resolve a bare import specifier to an in-monorepo source file when it targets
|
|
278
|
+
* a workspace package. Handles exact package imports (`@org/utils`) and subpaths
|
|
279
|
+
* (`@org/utils/helpers`). Returns null for non-workspace (external) specifiers.
|
|
280
|
+
*/
|
|
281
|
+
export function resolveWorkspaceImport(importFrom, rootAbs, info) {
|
|
282
|
+
if (importFrom.startsWith("."))
|
|
283
|
+
return null; // relative, not a package import
|
|
284
|
+
let match = null;
|
|
285
|
+
let subpath = "";
|
|
286
|
+
for (const p of info.packages) {
|
|
287
|
+
if (importFrom === p.name) {
|
|
288
|
+
match = p;
|
|
289
|
+
subpath = "";
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
if (importFrom.startsWith(p.name + "/")) {
|
|
293
|
+
match = p;
|
|
294
|
+
subpath = importFrom.slice(p.name.length + 1);
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (!match)
|
|
299
|
+
return null;
|
|
300
|
+
const pkgDir = path.resolve(rootAbs, match.dir);
|
|
301
|
+
const pj = readJson(path.join(pkgDir, "package.json"));
|
|
302
|
+
const entry = packageEntry(pkgDir, pj);
|
|
303
|
+
if (!subpath)
|
|
304
|
+
return entry;
|
|
305
|
+
// Subpath import (`@org/utils/helpers`): resolve against the source root
|
|
306
|
+
// (the entry file's directory, typically `src/`) first, then the package dir.
|
|
307
|
+
const srcRoot = entry ? path.dirname(entry) : pkgDir;
|
|
308
|
+
return (resolveSourceFile(path.resolve(srcRoot, subpath)) ??
|
|
309
|
+
resolveSourceFile(path.resolve(pkgDir, subpath)));
|
|
310
|
+
}
|
|
311
|
+
/* ─── Cached per-root workspace index (for resolvers) ──────────────────────── */
|
|
312
|
+
const workspaceCache = new Map();
|
|
313
|
+
/** Discover (and cache) the workspace for a root, then resolve a package import. */
|
|
314
|
+
export function resolveWorkspaceImportCached(importFrom, rootAbs) {
|
|
315
|
+
if (importFrom.startsWith("."))
|
|
316
|
+
return null;
|
|
317
|
+
const key = path.resolve(rootAbs);
|
|
318
|
+
let info = workspaceCache.get(key);
|
|
319
|
+
if (!info) {
|
|
320
|
+
info = discoverWorkspace(key);
|
|
321
|
+
workspaceCache.set(key, info);
|
|
322
|
+
}
|
|
323
|
+
if (info.packages.length === 0)
|
|
324
|
+
return null;
|
|
325
|
+
return resolveWorkspaceImport(importFrom, key, info);
|
|
326
|
+
}
|
|
327
|
+
/** Test/debug hook: drop the cached workspace index. */
|
|
328
|
+
export function clearWorkspaceCache() {
|
|
329
|
+
workspaceCache.clear();
|
|
330
|
+
}
|
package/package.json
CHANGED