@nudojs/service 0.2.1 → 0.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/index.d.ts +502 -4
  3. package/dist/index.js +3017 -212
  4. package/package.json +5 -4
  5. package/src/__tests__/abs-bindings.test.ts +53 -0
  6. package/src/__tests__/abs-call-records.test.ts +33 -0
  7. package/src/__tests__/abs-calls.test.ts +59 -0
  8. package/src/__tests__/abs-module-cache.test.ts +121 -0
  9. package/src/__tests__/abs-modules-graph.test.ts +100 -0
  10. package/src/__tests__/abs-typevalue-diff.test.ts +111 -0
  11. package/src/__tests__/analyzer.test.ts +71 -3
  12. package/src/__tests__/bpath-arg-provenance.test.ts +104 -0
  13. package/src/__tests__/bpath-as.test.ts +54 -0
  14. package/src/__tests__/bpath-cache-key.test.ts +33 -0
  15. package/src/__tests__/bpath-calls.test.ts +55 -0
  16. package/src/__tests__/bpath-diagnostics.test.ts +78 -0
  17. package/src/__tests__/bpath-env-modules.test.ts +30 -0
  18. package/src/__tests__/bpath-env.test.ts +80 -0
  19. package/src/__tests__/bpath-hosted-skip.test.ts +71 -0
  20. package/src/__tests__/bpath-import-default-ns.test.ts +106 -0
  21. package/src/__tests__/bpath-method-dedupe.test.ts +65 -0
  22. package/src/__tests__/bpath-method-diag.test.ts +45 -0
  23. package/src/__tests__/bpath-module-diags.test.ts +101 -0
  24. package/src/__tests__/bpath-path-env.test.ts +46 -0
  25. package/src/__tests__/bpath-reexport.test.ts +131 -0
  26. package/src/__tests__/bpath-replace.test.ts +52 -0
  27. package/src/__tests__/bpath-require.test.ts +66 -0
  28. package/src/__tests__/bpath-run.test.ts +109 -0
  29. package/src/__tests__/cjs-require.test.ts +54 -0
  30. package/src/__tests__/diagnostics.test.ts +0 -30
  31. package/src/__tests__/fn-cache-evict.test.ts +56 -0
  32. package/src/__tests__/harvest-auto.test.ts +43 -0
  33. package/src/__tests__/harvest-node.test.ts +24 -0
  34. package/src/__tests__/harvest-package.test.ts +38 -0
  35. package/src/__tests__/harvest-to-abs.test.ts +46 -0
  36. package/src/__tests__/hover-intension.test.ts +39 -0
  37. package/src/__tests__/infer-json.test.ts +35 -0
  38. package/src/__tests__/integration.test.ts +89 -0
  39. package/src/__tests__/m3-intension.test.ts +28 -0
  40. package/src/__tests__/mock-seed-fingerprint.test.ts +64 -0
  41. package/src/__tests__/session-cache-evict.test.ts +90 -0
  42. package/src/__tests__/static-imports.test.ts +44 -0
  43. package/src/abs-modules-graph.ts +387 -0
  44. package/src/analysis-file-cache.ts +51 -0
  45. package/src/analyzer.ts +1416 -124
  46. package/src/bpath-diagnostics.ts +283 -0
  47. package/src/bpath-run.ts +409 -0
  48. package/src/case-emitter.ts +2 -0
  49. package/src/dts-generator.ts +3 -3
  50. package/src/env-to-abs.ts +57 -0
  51. package/src/fn-analysis-cache.ts +119 -0
  52. package/src/guard-generator.ts +8 -1
  53. package/src/harvest-auto.ts +86 -0
  54. package/src/harvest-node.ts +48 -0
  55. package/src/harvest-package.ts +204 -0
  56. package/src/harvest-to-abs.ts +129 -0
  57. package/src/index.ts +104 -1
  58. package/src/infer-json.ts +137 -0
  59. package/src/load-module.ts +34 -0
  60. package/src/mock-abs.ts +275 -0
  61. package/src/semantic-tokens.ts +216 -0
  62. package/src/session-cache.ts +50 -0
  63. package/src/static-imports.ts +241 -0
  64. package/src/target-path.ts +17 -0
@@ -0,0 +1,216 @@
1
+ import { dirname } from "node:path";
2
+ import type { Node } from "@babel/types";
3
+ import { createEnvironment, type Environment } from "@nudojs/core";
4
+ import { parse } from "@nudojs/parser";
5
+ import {
6
+ evaluateProgram,
7
+ setModuleResolver,
8
+ setCurrentFileDir,
9
+ resetMemo,
10
+ } from "@nudojs/cli/evaluator";
11
+ import { resolveModule } from "./analyzer.ts";
12
+
13
+ /**
14
+ * Semantic tokens 图例(tokenTypes 下标即 LSP 编码里的 tokenType 值)。
15
+ * 与 LSP server capabilities 里声明的 legend 必须逐字对齐——server.ts 直接
16
+ * 导入本常量注册,保证「提取端索引」与「客户端图例」单一来源。
17
+ * 顺序沿用 lsp 包原 legend(function/variable/parameter/property 在前),
18
+ * 末尾追加 method(对象字面量方法键),只追加不重排,客户端索引稳定。
19
+ */
20
+ export const SEMANTIC_TOKEN_TYPES = [
21
+ "function",
22
+ "variable",
23
+ "parameter",
24
+ "property",
25
+ "type",
26
+ "keyword",
27
+ "string",
28
+ "number",
29
+ "comment",
30
+ "decorator",
31
+ "method",
32
+ ] as const;
33
+
34
+ export const SEMANTIC_TOKEN_MODIFIERS = [
35
+ "declaration",
36
+ "readonly",
37
+ "deprecated",
38
+ "unreachable",
39
+ ] as const;
40
+
41
+ export type SemanticToken = {
42
+ line: number;
43
+ char: number;
44
+ length: number;
45
+ typeIndex: number;
46
+ modifierBitmask: number;
47
+ };
48
+
49
+ /** LSP 标准相对五元组编码:deltaLine/deltaStartChar/length/tokenType/tokenModifiers。 */
50
+ export function encodeSemanticTokens(tokens: SemanticToken[]): number[] {
51
+ const result: number[] = [];
52
+ let prevLine = 0;
53
+ let prevChar = 0;
54
+
55
+ for (const token of tokens) {
56
+ const deltaLine = token.line - prevLine;
57
+ const deltaChar = deltaLine === 0 ? token.char - prevChar : token.char;
58
+
59
+ result.push(deltaLine, deltaChar, token.length, token.typeIndex, token.modifierBitmask);
60
+
61
+ prevLine = token.line;
62
+ prevChar = token.char;
63
+ }
64
+
65
+ return result;
66
+ }
67
+
68
+ const TYPE_FUNCTION = SEMANTIC_TOKEN_TYPES.indexOf("function");
69
+ const TYPE_VARIABLE = SEMANTIC_TOKEN_TYPES.indexOf("variable");
70
+ const TYPE_PARAMETER = SEMANTIC_TOKEN_TYPES.indexOf("parameter");
71
+ const TYPE_PROPERTY = SEMANTIC_TOKEN_TYPES.indexOf("property");
72
+ const TYPE_METHOD = SEMANTIC_TOKEN_TYPES.indexOf("method");
73
+ const MOD_DECLARATION = 1 << SEMANTIC_TOKEN_MODIFIERS.indexOf("declaration");
74
+
75
+ /**
76
+ * 从源码提取 semantic tokens 并按 LSP 相对编码返回扁平 number[]。
77
+ *
78
+ * 上色范围(声明位优先,未解析的 token 一律不上色):
79
+ * - 顶层 const/let/var 声明的绑定名:推断为函数绑定 → function,否则 variable
80
+ * (函数体内部的声明不做 env 反查——外层同名绑定会串味,统一 variable);
81
+ * - 函数声明/函数表达式的名字 → function;所有函数的参数 → parameter;
82
+ * - 对象字面量的键:值为函数 → method,否则 property。
83
+ *
84
+ * 推断复用 evaluateProgram 的绑定分析(与补全/诊断同一求值链),不另建
85
+ * 符号体系;解析失败返回 [],求值中断则按已绑定的部分结果继续上色。
86
+ */
87
+ export function buildSemanticTokens(filePath: string, source: string): number[] {
88
+ let ast: Node;
89
+ try {
90
+ ast = parse(source);
91
+ } catch {
92
+ return [];
93
+ }
94
+
95
+ const env = createEnvironment();
96
+ resetMemo();
97
+ setModuleResolver(resolveModule);
98
+ setCurrentFileDir(dirname(filePath));
99
+ try {
100
+ evaluateProgram(ast, env);
101
+ } catch {
102
+ // 求值中断(如未支持语法):尽力而为,用已落进 env 的绑定继续上色
103
+ } finally {
104
+ setModuleResolver(null);
105
+ }
106
+
107
+ const program = (ast as { program?: Node }).program ?? ast;
108
+ const ownBindings = env.getOwnBindings();
109
+ // 顶层声明器节点集合:仅这些绑定名做 env 反查区分 function/variable
110
+ const topLevelDeclarators = new Set<unknown>();
111
+ for (const stmt of ((program as { body?: Node[] }).body ?? []) as Node[]) {
112
+ const decl =
113
+ stmt.type === "ExportNamedDeclaration" || stmt.type === "ExportDefaultDeclaration"
114
+ ? ((stmt as { declaration?: Node }).declaration ?? null)
115
+ : stmt;
116
+ if (decl && decl.type === "VariableDeclaration") {
117
+ for (const d of (decl as unknown as { declarations: Node[] }).declarations) {
118
+ topLevelDeclarators.add(d);
119
+ }
120
+ }
121
+ }
122
+
123
+ const tokens: SemanticToken[] = [];
124
+ const pushIdentifier = (id: Node, typeIndex: number): void => {
125
+ const loc = (id as { loc?: { start: { line: number; column: number } } }).loc;
126
+ const name = (id as { name?: string }).name;
127
+ if (!loc || typeof name !== "string") return;
128
+ tokens.push({
129
+ line: loc.start.line - 1,
130
+ char: loc.start.column,
131
+ length: name.length,
132
+ typeIndex,
133
+ modifierBitmask: MOD_DECLARATION,
134
+ });
135
+ };
136
+
137
+ const collectParams = (params: Node[]): void => {
138
+ for (const p of params) {
139
+ if (!p) continue;
140
+ if (p.type === "Identifier") pushIdentifier(p, TYPE_PARAMETER);
141
+ else if (p.type === "AssignmentPattern" && (p as { left?: Node }).left?.type === "Identifier") {
142
+ pushIdentifier((p as { left: Node }).left, TYPE_PARAMETER);
143
+ } else if (p.type === "RestElement" && (p as { argument?: Node }).argument?.type === "Identifier") {
144
+ pushIdentifier((p as { argument: Node }).argument, TYPE_PARAMETER);
145
+ }
146
+ // 解构模式(ObjectPattern/ArrayPattern)内部不上色:子绑定的
147
+ // function/variable 区分没有可靠推断依据,宁缺毋滥
148
+ }
149
+ };
150
+
151
+ const isFunctionValue = (name: string): boolean =>
152
+ ownBindings[name]?.kind === "function";
153
+
154
+ const visit = (node: unknown): void => {
155
+ if (!node || typeof node !== "object") return;
156
+ const n = node as Record<string, unknown>;
157
+
158
+ switch (n.type) {
159
+ case "VariableDeclarator": {
160
+ const id = n.id as Node | undefined;
161
+ if (id?.type === "Identifier") {
162
+ const isTopLevel = topLevelDeclarators.has(node);
163
+ const typeIndex =
164
+ isTopLevel && isFunctionValue((id as { name: string }).name) ? TYPE_FUNCTION : TYPE_VARIABLE;
165
+ pushIdentifier(id, typeIndex);
166
+ }
167
+ break;
168
+ }
169
+ case "FunctionDeclaration":
170
+ case "FunctionExpression": {
171
+ const id = n.id as Node | undefined;
172
+ if (id?.type === "Identifier") pushIdentifier(id, TYPE_FUNCTION);
173
+ collectParams((n.params as Node[] | undefined) ?? []);
174
+ break;
175
+ }
176
+ case "ArrowFunctionExpression":
177
+ collectParams((n.params as Node[] | undefined) ?? []);
178
+ break;
179
+ case "ObjectMethod": {
180
+ const key = n.key as Node | undefined;
181
+ if (key?.type === "Identifier" && !n.computed) pushIdentifier(key, TYPE_METHOD);
182
+ collectParams((n.params as Node[] | undefined) ?? []);
183
+ break;
184
+ }
185
+ case "ObjectProperty": {
186
+ const key = n.key as Node | undefined;
187
+ const value = n.value as Node | undefined;
188
+ if (key?.type === "Identifier" && !n.computed) {
189
+ const isFn =
190
+ value?.type === "FunctionExpression" || value?.type === "ArrowFunctionExpression";
191
+ pushIdentifier(key, isFn ? TYPE_METHOD : TYPE_PROPERTY);
192
+ }
193
+ break;
194
+ }
195
+ }
196
+
197
+ for (const key of Object.keys(n)) {
198
+ if (key === "loc" || key === "start" || key === "end" || key === "leadingComments" || key === "trailingComments" || key === "innerComments") continue;
199
+ const child = n[key];
200
+ if (Array.isArray(child)) {
201
+ for (const item of child) {
202
+ if (item && typeof item === "object" && typeof (item as { type?: unknown }).type === "string") {
203
+ visit(item);
204
+ }
205
+ }
206
+ } else if (child && typeof child === "object" && typeof (child as { type?: unknown }).type === "string") {
207
+ visit(child);
208
+ }
209
+ }
210
+ };
211
+
212
+ visit(program);
213
+
214
+ tokens.sort((a, b) => a.line - b.line || a.char - b.char);
215
+ return encodeSemanticTokens(tokens);
216
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * 会话缓存失效入口(宿主契约的唯一接线点)。
3
+ *
4
+ * analyzeFile / tryRunBPath / generalize L0 的键都不含「依赖模块内容」——
5
+ * 入口 source 未变但 dep 变了时,必须由宿主主动逐出,否则会命中陈旧结果。
6
+ * LSP 走定向逐出;CLI watch / vite-plugin 走这里。
7
+ */
8
+ import {
9
+ resetGeneralizeMemo,
10
+ resetCheckSourceMemo,
11
+ resetNudoModuleExecCache,
12
+ resetParseSourceCache,
13
+ } from "@nudojs/core";
14
+ import { clearAbsModuleCache, evictAbsModuleCacheFiles } from "./abs-modules-graph.ts";
15
+ import { clearBPathCache, evictBPathCacheForFiles } from "./bpath-run.ts";
16
+ import { clearAnalysisFileCache, evictAnalysisFileCacheForFiles } from "./analysis-file-cache.ts";
17
+ import { clearFnAnalysisCache, evictFnAnalysisCacheForFiles } from "./fn-analysis-cache.ts";
18
+
19
+ /**
20
+ * 依赖内容变更后:按入口文件定向逐出 service 层缓存。
21
+ * 调用方应传「以这些文件为入口」的路径(脏集里的 dependents),
22
+ * 而不是变更的 dep 文件本身——dep 自己 source 变了会自然 miss。
23
+ */
24
+ export function evictAnalysisCachesForFiles(files: string[]): void {
25
+ if (files.length === 0) return;
26
+ evictBPathCacheForFiles(files);
27
+ evictAnalysisFileCacheForFiles(files);
28
+ evictFnAnalysisCacheForFiles(files);
29
+ evictAbsModuleCacheFiles(files);
30
+ }
31
+
32
+ /**
33
+ * 清空全部会话级分析缓存(service + core)。
34
+ * 适用于:CLI watch 增量批前、vite buildStart / watchChange、测试隔离。
35
+ * 比定向逐出重,但保证无陈旧命中;单次 analyze 内部的 per-fn / generalize
36
+ * memo 不受影响(它们在同一轮里先写后读)。
37
+ */
38
+ export function clearAnalysisSessionCaches(): void {
39
+ clearBPathCache(); // cascades analysis-file + fn-analysis
40
+ clearAbsModuleCache();
41
+ resetGeneralizeMemo();
42
+ resetCheckSourceMemo();
43
+ resetNudoModuleExecCache();
44
+ }
45
+
46
+ /** 比 clearAnalysisSessionCaches 更彻底:再丢 AST LRU(测试 / 进程复用场景) */
47
+ export function resetAllAnalysisCaches(): void {
48
+ clearAnalysisSessionCaches();
49
+ resetParseSourceCache();
50
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Host 侧多文件解析(非代数层)。
3
+ *
4
+ * 分层:
5
+ * algebra = 纯抽象解释:吃 AST / source string,不碰 fs/path
6
+ * host = 读文件、解析相对 import/require、把 source 喂给代数
7
+ * Node/打包器 = 真正的模块加载与执行
8
+ *
9
+ * 引擎不做 bundler;这里只为「跨文件类型事实」做最薄的静态扫描。
10
+ */
11
+
12
+ import { parse } from "@nudojs/parser";
13
+ import type { File, Node } from "@babel/types";
14
+ import { readFileSync, existsSync } from "node:fs";
15
+ import { resolve, dirname, join } from "node:path";
16
+ import { generalizeFromAst, type PolyFn } from "@nudojs/core";
17
+
18
+ export type ModuleExports = {
19
+ path: string;
20
+ named: Map<string, string>;
21
+ defaultExport?: string;
22
+ source: string;
23
+ poly: Map<string, PolyFn>;
24
+ };
25
+
26
+ function resolveRelative(fromFile: string, spec: string): string | undefined {
27
+ if (!spec.startsWith(".") && !spec.startsWith("/")) return undefined;
28
+ const base = resolve(dirname(fromFile), spec);
29
+ for (const cand of [
30
+ base,
31
+ base + ".js",
32
+ base + ".cjs",
33
+ base + ".mjs",
34
+ base + ".ts",
35
+ base + ".mts",
36
+ base + ".cts",
37
+ join(base, "index.js"),
38
+ join(base, "index.cjs"),
39
+ join(base, "index.mjs"),
40
+ join(base, "index.ts"),
41
+ ]) {
42
+ if (existsSync(cand)) return cand;
43
+ }
44
+ return undefined;
45
+ }
46
+
47
+ /** 从 AST 收集静态相对依赖(ESM import + CJS require) */
48
+ export function collectDependencySpecs(ast: File): string[] {
49
+ const specs: string[] = [];
50
+ for (const stmt of ast.program.body) {
51
+ if (stmt.type === "ImportDeclaration") {
52
+ specs.push(stmt.source.value);
53
+ }
54
+ if (stmt.type === "ExportNamedDeclaration" && stmt.source) {
55
+ specs.push(stmt.source.value);
56
+ }
57
+ if (stmt.type === "ExportAllDeclaration" && stmt.source) {
58
+ specs.push(stmt.source.value);
59
+ }
60
+ // require("...") / require.resolve
61
+ collectRequires(stmt, specs);
62
+ }
63
+ return specs;
64
+ }
65
+
66
+ function collectRequires(node: Node, out: string[]): void {
67
+ const visit = (n: unknown): void => {
68
+ if (!n || typeof n !== "object") return;
69
+ const obj = n as {
70
+ type?: string;
71
+ callee?: { type?: string; name?: string };
72
+ arguments?: Array<{ type?: string; value?: unknown }>;
73
+ [k: string]: unknown;
74
+ };
75
+ if (
76
+ obj.type === "CallExpression" &&
77
+ obj.callee?.type === "Identifier" &&
78
+ obj.callee.name === "require" &&
79
+ obj.arguments?.[0]?.type === "StringLiteral"
80
+ ) {
81
+ const spec = obj.arguments[0].value;
82
+ if (typeof spec === "string") out.push(spec);
83
+ }
84
+ for (const key of Object.keys(obj)) {
85
+ if (key === "loc" || key === "start" || key === "end") continue;
86
+ const v = obj[key];
87
+ if (Array.isArray(v)) v.forEach(visit);
88
+ else if (v && typeof v === "object") visit(v);
89
+ }
90
+ };
91
+ visit(node);
92
+ }
93
+
94
+ /** 分析单文件导出 + 内涵签名(source 由 host 提供) */
95
+ export function analyzeExportsFromSource(
96
+ filePath: string,
97
+ source: string,
98
+ ): ModuleExports {
99
+ const file = parse(source);
100
+ const named = new Map<string, string>();
101
+ let defaultExport: string | undefined;
102
+ const poly = new Map<string, PolyFn>();
103
+
104
+ for (const stmt of file.program.body) {
105
+ let decl: Node = stmt;
106
+ if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) {
107
+ decl = stmt.declaration;
108
+ }
109
+ if (decl.type === "FunctionDeclaration" && decl.id) {
110
+ named.set(decl.id.name, decl.id.name);
111
+ }
112
+ if (decl.type === "VariableDeclaration") {
113
+ for (const d of decl.declarations) {
114
+ if (d.id.type === "Identifier") named.set(d.id.name, d.id.name);
115
+ }
116
+ }
117
+ if (decl.type === "ClassDeclaration" && decl.id) {
118
+ named.set(decl.id.name, decl.id.name);
119
+ }
120
+ if (stmt.type === "ExportDefaultDeclaration") {
121
+ const d = stmt.declaration;
122
+ if (d.type === "FunctionDeclaration" && d.id) defaultExport = d.id.name;
123
+ if (d.type === "Identifier") defaultExport = d.name;
124
+ }
125
+ // CJS: exports.foo = fn / module.exports = fn / module.exports = { a, b }
126
+ collectCjsExports(stmt, named, (n) => {
127
+ defaultExport = n;
128
+ });
129
+ }
130
+
131
+ for (const name of named.values()) {
132
+ const g = generalizeFromAst(name, source);
133
+ if (g) poly.set(name, g);
134
+ }
135
+ if (defaultExport) {
136
+ const g = generalizeFromAst(defaultExport, source);
137
+ if (g) poly.set("default", g);
138
+ }
139
+
140
+ return { path: filePath, named, defaultExport, source, poly };
141
+ }
142
+
143
+ function collectCjsExports(
144
+ stmt: Node,
145
+ named: Map<string, string>,
146
+ setDefault: (name: string) => void,
147
+ ): void {
148
+ const visit = (n: unknown): void => {
149
+ if (!n || typeof n !== "object") return;
150
+ const obj = n as Record<string, unknown> & { type?: string };
151
+ if (obj.type !== "AssignmentExpression") {
152
+ for (const key of Object.keys(obj)) {
153
+ if (key === "loc" || key === "start" || key === "end") continue;
154
+ const v = obj[key];
155
+ if (Array.isArray(v)) v.forEach(visit);
156
+ else if (v && typeof v === "object") visit(v);
157
+ }
158
+ return;
159
+ }
160
+ const left = obj.left as {
161
+ type?: string;
162
+ object?: { type?: string; name?: string };
163
+ property?: { type?: string; name?: string; value?: string };
164
+ };
165
+ const right = obj.right as Record<string, unknown> & { type?: string };
166
+
167
+ // exports.foo = ...
168
+ if (
169
+ left?.type === "MemberExpression" &&
170
+ left.object?.type === "Identifier" &&
171
+ left.object.name === "exports" &&
172
+ left.property?.type === "Identifier" &&
173
+ left.property.name
174
+ ) {
175
+ named.set(left.property.name, left.property.name);
176
+ }
177
+ // module.exports = fnName
178
+ if (
179
+ left?.type === "MemberExpression" &&
180
+ left.object?.type === "Identifier" &&
181
+ left.object.name === "module" &&
182
+ left.property?.type === "Identifier" &&
183
+ left.property.name === "exports"
184
+ ) {
185
+ if (right?.type === "Identifier" && typeof right.name === "string") {
186
+ named.set(right.name, right.name);
187
+ setDefault(right.name);
188
+ }
189
+ // module.exports = { foo, bar: baz }
190
+ if (right?.type === "ObjectExpression") {
191
+ const props = right.properties as Array<Record<string, unknown>>;
192
+ for (const p of props) {
193
+ if (p.type !== "ObjectProperty") continue;
194
+ const key = p.key as { type?: string; name?: string; value?: string };
195
+ const val = p.value as { type?: string; name?: string };
196
+ const exportName =
197
+ key?.type === "Identifier" ? key.name : key?.type === "StringLiteral" ? key.value : undefined;
198
+ if (exportName) named.set(exportName, exportName);
199
+ if (val?.type === "Identifier" && val.name && exportName) {
200
+ // exported binding may differ from local name — generalize uses local
201
+ // keep exportName as key; generalizeFromAst(exportName) may fail if local differs
202
+ }
203
+ }
204
+ }
205
+ }
206
+ };
207
+ visit(stmt);
208
+ }
209
+
210
+ /**
211
+ * 从入口文件沿静态相对 import/require 收集(仅类型事实,不是运行时加载器)。
212
+ * bare specifier(npm 包)跳过——交给 harvest / env。
213
+ */
214
+ export function collectStaticImports(
215
+ entryFile: string,
216
+ maxDepth = 8,
217
+ ): Map<string, ModuleExports> {
218
+ const graph = new Map<string, ModuleExports>();
219
+ const queue: Array<{ file: string; depth: number }> = [
220
+ { file: resolve(entryFile), depth: 0 },
221
+ ];
222
+ const seen = new Set<string>();
223
+
224
+ while (queue.length > 0) {
225
+ const { file, depth } = queue.shift()!;
226
+ if (seen.has(file) || depth > maxDepth) continue;
227
+ seen.add(file);
228
+ if (!existsSync(file)) continue;
229
+
230
+ const source = readFileSync(file, "utf8");
231
+ const mod = analyzeExportsFromSource(file, source);
232
+ graph.set(file, mod);
233
+
234
+ const ast: File = parse(source);
235
+ for (const spec of collectDependencySpecs(ast)) {
236
+ const resolved = resolveRelative(file, spec);
237
+ if (resolved) queue.push({ file: resolved, depth: depth + 1 });
238
+ }
239
+ }
240
+ return graph;
241
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * nudo 推断目标文件判定(纯扩展名规则,路径无需存在)。
3
+ *
4
+ * 规则:.js / .mjs / .ts 可作为推断目标;其余扩展(.cjs/.tsx/.jsx/.mts/.d.ts…)
5
+ * 一律 false。两类显式排除:
6
+ * - .d.ts —— 类型声明文件(harvester 的输入),不是可求值的实现源码;
7
+ * - .tsx / .jsx —— JSX 构造的求值超出 nudo 推断器范围。
8
+ *
9
+ * 消费方:CLI 的 collectNudoFiles/watch 过滤/doctor 目录展开(.ts 放开后统一
10
+ * 走本函数,避免各处手写 endsWith 漂移),以及 LSP 的 isNudoFile(接线由
11
+ * LSP 侧负责)。
12
+ */
13
+ export function isNudoTargetPath(path: string): boolean {
14
+ const lower = path.toLowerCase();
15
+ if (lower.endsWith(".d.ts")) return false; // 类型声明,非推断目标
16
+ return lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".ts");
17
+ }