@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.
- package/CHANGELOG.md +30 -0
- package/dist/index.d.ts +502 -4
- package/dist/index.js +3017 -212
- package/package.json +5 -4
- package/src/__tests__/abs-bindings.test.ts +53 -0
- package/src/__tests__/abs-call-records.test.ts +33 -0
- package/src/__tests__/abs-calls.test.ts +59 -0
- package/src/__tests__/abs-module-cache.test.ts +121 -0
- package/src/__tests__/abs-modules-graph.test.ts +100 -0
- package/src/__tests__/abs-typevalue-diff.test.ts +111 -0
- package/src/__tests__/analyzer.test.ts +71 -3
- package/src/__tests__/bpath-arg-provenance.test.ts +104 -0
- package/src/__tests__/bpath-as.test.ts +54 -0
- package/src/__tests__/bpath-cache-key.test.ts +33 -0
- package/src/__tests__/bpath-calls.test.ts +55 -0
- package/src/__tests__/bpath-diagnostics.test.ts +78 -0
- package/src/__tests__/bpath-env-modules.test.ts +30 -0
- package/src/__tests__/bpath-env.test.ts +80 -0
- package/src/__tests__/bpath-hosted-skip.test.ts +71 -0
- package/src/__tests__/bpath-import-default-ns.test.ts +106 -0
- package/src/__tests__/bpath-method-dedupe.test.ts +65 -0
- package/src/__tests__/bpath-method-diag.test.ts +45 -0
- package/src/__tests__/bpath-module-diags.test.ts +101 -0
- package/src/__tests__/bpath-path-env.test.ts +46 -0
- package/src/__tests__/bpath-reexport.test.ts +131 -0
- package/src/__tests__/bpath-replace.test.ts +52 -0
- package/src/__tests__/bpath-require.test.ts +66 -0
- package/src/__tests__/bpath-run.test.ts +109 -0
- package/src/__tests__/cjs-require.test.ts +54 -0
- package/src/__tests__/diagnostics.test.ts +0 -30
- package/src/__tests__/fn-cache-evict.test.ts +56 -0
- package/src/__tests__/harvest-auto.test.ts +43 -0
- package/src/__tests__/harvest-node.test.ts +24 -0
- package/src/__tests__/harvest-package.test.ts +38 -0
- package/src/__tests__/harvest-to-abs.test.ts +46 -0
- package/src/__tests__/hover-intension.test.ts +39 -0
- package/src/__tests__/infer-json.test.ts +35 -0
- package/src/__tests__/integration.test.ts +89 -0
- package/src/__tests__/m3-intension.test.ts +28 -0
- package/src/__tests__/mock-seed-fingerprint.test.ts +64 -0
- package/src/__tests__/session-cache-evict.test.ts +90 -0
- package/src/__tests__/static-imports.test.ts +44 -0
- package/src/abs-modules-graph.ts +387 -0
- package/src/analysis-file-cache.ts +51 -0
- package/src/analyzer.ts +1416 -124
- package/src/bpath-diagnostics.ts +283 -0
- package/src/bpath-run.ts +409 -0
- package/src/case-emitter.ts +2 -0
- package/src/dts-generator.ts +3 -3
- package/src/env-to-abs.ts +57 -0
- package/src/fn-analysis-cache.ts +119 -0
- package/src/guard-generator.ts +8 -1
- package/src/harvest-auto.ts +86 -0
- package/src/harvest-node.ts +48 -0
- package/src/harvest-package.ts +204 -0
- package/src/harvest-to-abs.ts +129 -0
- package/src/index.ts +104 -1
- package/src/infer-json.ts +137 -0
- package/src/load-module.ts +34 -0
- package/src/mock-abs.ts +275 -0
- package/src/semantic-tokens.ts +216 -0
- package/src/session-cache.ts +50 -0
- package/src/static-imports.ts +241 -0
- package/src/target-path.ts +17 -0
package/src/dts-generator.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TypeValue } from "@nudojs/core";
|
|
2
|
-
import { T, isTemplate, getTemplateParts, simplifyUnion, typeValueEquals } from "@nudojs/core";
|
|
2
|
+
import { T, isTemplate, getTemplateParts, simplifyUnion, typeValueEquals, getFnSig } from "@nudojs/core";
|
|
3
3
|
import type { AnalysisResult, FunctionAnalysis } from "./analyzer.ts";
|
|
4
4
|
|
|
5
5
|
export function typeValueToTSType(tv: TypeValue): string {
|
|
@@ -42,8 +42,8 @@ export function typeValueToTSType(tv: TypeValue): string {
|
|
|
42
42
|
const params = tv.params
|
|
43
43
|
.map((p) => `${p}: unknown`)
|
|
44
44
|
.join(", ");
|
|
45
|
-
const
|
|
46
|
-
const retStr = returnType ? typeValueToTSType(returnType) : "unknown";
|
|
45
|
+
const sig = getFnSig(tv);
|
|
46
|
+
const retStr = sig?.returnType ? typeValueToTSType(sig.returnType) : "unknown";
|
|
47
47
|
return `(${params}) => ${retStr}`;
|
|
48
48
|
}
|
|
49
49
|
case "promise":
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* env TypeValue → Abs:保留 fnSig impl,使 Promise.resolve / URL 等
|
|
3
|
+
* 在 B 路径也能产出精确字面量。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type TypeValue,
|
|
8
|
+
type Abs,
|
|
9
|
+
typeValueToAbs,
|
|
10
|
+
absToTypeValue,
|
|
11
|
+
getFnSig,
|
|
12
|
+
absFunction,
|
|
13
|
+
objOf,
|
|
14
|
+
} from "@nudojs/core";
|
|
15
|
+
|
|
16
|
+
export function envValueToAbs(tv: TypeValue): Abs {
|
|
17
|
+
if (!tv) return typeValueToAbs({ kind: "unknown" });
|
|
18
|
+
|
|
19
|
+
if (tv.kind === "function") {
|
|
20
|
+
const sig = getFnSig(tv);
|
|
21
|
+
if (sig?.impl) {
|
|
22
|
+
const params =
|
|
23
|
+
tv.kind === "function" && tv.params?.length
|
|
24
|
+
? tv.params
|
|
25
|
+
: sig.paramTypes.map((_, i) => `_arg${i}`);
|
|
26
|
+
const dummyBody = {
|
|
27
|
+
type: "BlockStatement",
|
|
28
|
+
body: [],
|
|
29
|
+
directives: [],
|
|
30
|
+
} as never;
|
|
31
|
+
const impl = sig.impl;
|
|
32
|
+
return absFunction(params, {
|
|
33
|
+
body: dummyBody,
|
|
34
|
+
apply: (args: Abs[]): Abs => {
|
|
35
|
+
const tvArgs = args.map((a) => absToTypeValue(a));
|
|
36
|
+
try {
|
|
37
|
+
const r = impl(tvArgs);
|
|
38
|
+
if (!r) return typeValueToAbs(sig.returnType);
|
|
39
|
+
return envValueToAbs(r);
|
|
40
|
+
} catch {
|
|
41
|
+
return typeValueToAbs(sig.returnType);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (tv.kind === "object") {
|
|
49
|
+
const slots: Record<string, { value: Abs }> = {};
|
|
50
|
+
for (const [k, v] of Object.entries(tv.properties)) {
|
|
51
|
+
slots[k] = { value: envValueToAbs(v) };
|
|
52
|
+
}
|
|
53
|
+
return objOf(slots);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return typeValueToAbs(tv);
|
|
57
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-function FunctionAnalysis cache (body-edit: recompute only dirty fns).
|
|
3
|
+
* Cleared together with B-path / whole-file analysis caches.
|
|
4
|
+
*/
|
|
5
|
+
import type { TypeValue } from "@nudojs/core";
|
|
6
|
+
|
|
7
|
+
// Structural types — avoid importing analyzer (cycle).
|
|
8
|
+
export type CachedSourceLocation = {
|
|
9
|
+
start: { line: number; column: number };
|
|
10
|
+
end: { line: number; column: number };
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type CachedDiagnostic = {
|
|
14
|
+
range: CachedSourceLocation;
|
|
15
|
+
severity: "error" | "warning" | "info";
|
|
16
|
+
message: string;
|
|
17
|
+
tags?: string[];
|
|
18
|
+
code?: string;
|
|
19
|
+
suggestions?: string[];
|
|
20
|
+
data?: unknown;
|
|
21
|
+
origin?: { line: number; column: number };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type CachedCaseHint = {
|
|
25
|
+
line: number;
|
|
26
|
+
label: string;
|
|
27
|
+
ok: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type CachedCallRecord = {
|
|
31
|
+
// opaque; cloned shallowly. Position fields that need lineDelta shift
|
|
32
|
+
// (currently only callLoc) are listed on CallRecord in evaluator.ts —
|
|
33
|
+
// keep shiftCallRecordLines in sync when adding any.
|
|
34
|
+
[k: string]: unknown;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type CachedFnAnalysis = {
|
|
38
|
+
analysis: {
|
|
39
|
+
name: string;
|
|
40
|
+
loc: CachedSourceLocation;
|
|
41
|
+
paramNames: string[];
|
|
42
|
+
cases: Array<{
|
|
43
|
+
name: string;
|
|
44
|
+
args: TypeValue[];
|
|
45
|
+
result: TypeValue;
|
|
46
|
+
throws: TypeValue;
|
|
47
|
+
throwLoc?: CachedSourceLocation;
|
|
48
|
+
source?: string;
|
|
49
|
+
expected?: TypeValue;
|
|
50
|
+
aggregatedFrom?: number;
|
|
51
|
+
intension?: Record<string, unknown>;
|
|
52
|
+
}>;
|
|
53
|
+
combined?: TypeValue;
|
|
54
|
+
entryOnly?: boolean;
|
|
55
|
+
skipped?: boolean;
|
|
56
|
+
noDeclaration?: boolean;
|
|
57
|
+
fromModule?: string;
|
|
58
|
+
};
|
|
59
|
+
diagnostics: CachedDiagnostic[];
|
|
60
|
+
caseHints: CachedCaseHint[];
|
|
61
|
+
callRecords: CachedCallRecord[];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const fnAnalysisCache = new Map<string, CachedFnAnalysis>();
|
|
65
|
+
const MAX_FN_ANALYSIS_CACHE = 1024;
|
|
66
|
+
|
|
67
|
+
export function clearFnAnalysisCache(): void {
|
|
68
|
+
fnAnalysisCache.clear();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function fnAnalysisCacheGet(key: string): CachedFnAnalysis | undefined {
|
|
72
|
+
const hit = fnAnalysisCache.get(key);
|
|
73
|
+
if (hit !== undefined) {
|
|
74
|
+
fnAnalysisCache.delete(key);
|
|
75
|
+
fnAnalysisCache.set(key, hit);
|
|
76
|
+
}
|
|
77
|
+
return hit;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function fnAnalysisCacheSet(key: string, value: CachedFnAnalysis): void {
|
|
81
|
+
// Only evict when inserting a new key; overwrite of an existing key keeps LRU size
|
|
82
|
+
if (fnAnalysisCache.size >= MAX_FN_ANALYSIS_CACHE && !fnAnalysisCache.has(key)) {
|
|
83
|
+
const oldest = fnAnalysisCache.keys().next().value;
|
|
84
|
+
if (oldest !== undefined) fnAnalysisCache.delete(oldest);
|
|
85
|
+
}
|
|
86
|
+
fnAnalysisCache.set(key, value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Dependency content changed: drop every per-fn entry for these entry files.
|
|
91
|
+
* Keys are `filePath\0...`, so a prefix scan is sound and cheap at LRU size.
|
|
92
|
+
*/
|
|
93
|
+
export function evictFnAnalysisCacheForFiles(files: string[]): number {
|
|
94
|
+
if (files.length === 0 || fnAnalysisCache.size === 0) return 0;
|
|
95
|
+
const prefixes = files.map((f) => `${f}\0`);
|
|
96
|
+
let n = 0;
|
|
97
|
+
for (const key of [...fnAnalysisCache.keys()]) {
|
|
98
|
+
if (prefixes.some((p) => key.startsWith(p))) {
|
|
99
|
+
fnAnalysisCache.delete(key);
|
|
100
|
+
n++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return n;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function caseDirectiveKey(
|
|
107
|
+
directives: Array<{ kind: string; name?: string; args?: TypeValue[] }>,
|
|
108
|
+
typeValueToString: (t: TypeValue) => string,
|
|
109
|
+
): string {
|
|
110
|
+
return directives
|
|
111
|
+
.map((d) => {
|
|
112
|
+
if (d.kind === "case") {
|
|
113
|
+
const args = (d.args ?? []).map((a) => typeValueToString(a)).join(",");
|
|
114
|
+
return `c:${d.name ?? ""}:${args}`;
|
|
115
|
+
}
|
|
116
|
+
return d.kind;
|
|
117
|
+
})
|
|
118
|
+
.join("|");
|
|
119
|
+
}
|
package/src/guard-generator.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import type { TypeValue } from "@nudojs/core";
|
|
1
|
+
import type { TypeValue, Abs } from "@nudojs/core";
|
|
2
|
+
import { denoteGuard } from "@nudojs/core";
|
|
2
3
|
|
|
3
4
|
export function generateGuardFunction(name: string, tv: TypeValue): string {
|
|
4
5
|
const body = generateGuardBody(tv, "data");
|
|
5
6
|
return `export function ${name}(data) {\n return ${body};\n}`;
|
|
6
7
|
}
|
|
7
8
|
|
|
9
|
+
/** Abs 指称守卫(设计 §2.7):比 TypeValue 投影多保留 pred */
|
|
10
|
+
export function generateGuardFunctionFromAbs(name: string, abs: Abs): string {
|
|
11
|
+
const body = denoteGuard(abs, "data");
|
|
12
|
+
return `export function ${name}(data) {\n return ${body};\n}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
function generateGuardBody(tv: TypeValue, varName: string): string {
|
|
9
16
|
switch (tv.kind) {
|
|
10
17
|
case "literal": {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* harvest 自动化:源码裸 import → @types/package → env modules。
|
|
3
|
+
* 手动 `nudo harvest` 仍保留;这里是分析路径上的按需注入。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { parse } from "@nudojs/parser";
|
|
7
|
+
import type { TypeValue } from "@nudojs/core";
|
|
8
|
+
import { collectDependencySpecs } from "./static-imports.ts";
|
|
9
|
+
import { harvestPackage, type PackageHarvest } from "./harvest-package.ts";
|
|
10
|
+
|
|
11
|
+
/** 裸说明符 → 包名(含 scope);相对/绝对/node: 内建返回 undefined */
|
|
12
|
+
export function barePackageName(spec: string): string | undefined {
|
|
13
|
+
if (!spec) return undefined;
|
|
14
|
+
if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("node:")) return undefined;
|
|
15
|
+
const parts = spec.split("/");
|
|
16
|
+
if (spec.startsWith("@")) {
|
|
17
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : undefined;
|
|
18
|
+
}
|
|
19
|
+
return parts[0];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function collectBarePackages(source: string): string[] {
|
|
23
|
+
try {
|
|
24
|
+
const ast = parse(source);
|
|
25
|
+
const names = new Set<string>();
|
|
26
|
+
for (const spec of collectDependencySpecs(ast)) {
|
|
27
|
+
const pkg = barePackageName(spec);
|
|
28
|
+
if (pkg) names.add(pkg);
|
|
29
|
+
}
|
|
30
|
+
return [...names];
|
|
31
|
+
} catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 进程内 harvest 缓存:同包只 walk 一次 dts */
|
|
37
|
+
const harvestCache = new Map<string, PackageHarvest | null>();
|
|
38
|
+
|
|
39
|
+
export function harvestPackageCached(pkg: string, fromDir: string): PackageHarvest | null {
|
|
40
|
+
const key = `${fromDir}::${pkg}`;
|
|
41
|
+
if (harvestCache.has(key)) return harvestCache.get(key)!;
|
|
42
|
+
let result: PackageHarvest | null = null;
|
|
43
|
+
try {
|
|
44
|
+
const h = harvestPackage(pkg, fromDir);
|
|
45
|
+
if (!("error" in h)) result = h;
|
|
46
|
+
} catch {
|
|
47
|
+
result = null;
|
|
48
|
+
}
|
|
49
|
+
harvestCache.set(key, result);
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function clearHarvestCache(): void {
|
|
54
|
+
harvestCache.clear();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 自动 harvest 源码中全部裸包(有 @types 或包内 dts 时)。
|
|
59
|
+
* 返回可注入 setEnvModules 的 modules 表;无包可 harvest 时为空对象。
|
|
60
|
+
*/
|
|
61
|
+
export function autoHarvestModules(
|
|
62
|
+
source: string,
|
|
63
|
+
fromDir: string,
|
|
64
|
+
): Record<string, Record<string, TypeValue>> {
|
|
65
|
+
const packages = collectBarePackages(source);
|
|
66
|
+
if (packages.length === 0) return {};
|
|
67
|
+
const modules: Record<string, Record<string, TypeValue>> = {};
|
|
68
|
+
for (const pkg of packages) {
|
|
69
|
+
const h = harvestPackageCached(pkg, fromDir);
|
|
70
|
+
if (!h) continue;
|
|
71
|
+
for (const [mod, exports] of Object.entries(h.env.modules)) {
|
|
72
|
+
// 模块键可能是相对路径形态;同时登记裸包名与原始键
|
|
73
|
+
modules[mod] = exports;
|
|
74
|
+
if (!modules[pkg]) modules[pkg] = exports;
|
|
75
|
+
// `path` / `node:path` 等别名:basename 匹配
|
|
76
|
+
const base = mod.replace(/^@types\//, "").replace(/\\/g, "/");
|
|
77
|
+
if (base && !modules[base]) modules[base] = exports;
|
|
78
|
+
}
|
|
79
|
+
for (const [k, v] of Object.entries(h.env.globals)) {
|
|
80
|
+
// 全局 API 也挂一份到包名模块,供 default/namespace 消费
|
|
81
|
+
if (!modules[pkg]) modules[pkg] = {};
|
|
82
|
+
if (modules[pkg]![k] === undefined) modules[pkg]![k] = v;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return modules;
|
|
86
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 预置 Node API env:从 @types/node 的 .d.ts harvest。
|
|
3
|
+
* 与 packages/env 的手写 env 并行;此路径自动、可刷新。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { harvestDts, type HarvestedEnv } from "@nudojs/harvester";
|
|
9
|
+
import { collectDtsFiles, resolvePackageRoot } from "./harvest-package.ts";
|
|
10
|
+
|
|
11
|
+
export type NodeEnvResult =
|
|
12
|
+
| { ok: true; env: HarvestedEnv; root: string; files: number }
|
|
13
|
+
| { ok: false; error: string };
|
|
14
|
+
|
|
15
|
+
/** harvest @types/node(限制文件数 + 时间预算,避免拖垮启动) */
|
|
16
|
+
export function harvestNodeTypes(
|
|
17
|
+
fromDir?: string,
|
|
18
|
+
maxFiles = 12,
|
|
19
|
+
maxMs = 2500,
|
|
20
|
+
): NodeEnvResult {
|
|
21
|
+
// @types/node 在 node_modules/@types/node
|
|
22
|
+
const root =
|
|
23
|
+
resolvePackageRoot("@types/node", fromDir) ??
|
|
24
|
+
resolvePackageRoot("node", fromDir);
|
|
25
|
+
if (!root || !existsSync(root)) {
|
|
26
|
+
return { ok: false, error: "@types/node not found" };
|
|
27
|
+
}
|
|
28
|
+
// 优先 index.d.ts 等入口
|
|
29
|
+
const dts = collectDtsFiles(root, maxFiles);
|
|
30
|
+
if (dts.length === 0) {
|
|
31
|
+
return { ok: false, error: `no .d.ts under ${root}` };
|
|
32
|
+
}
|
|
33
|
+
const env = harvestDts(dts, { maxMs });
|
|
34
|
+
return { ok: true, env, root, files: env.stats.files };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 把 harvest 结果压成「模块名 → 导出名列表」摘要,便于日志/测试 */
|
|
38
|
+
export function summarizeNodeEnv(env: HarvestedEnv): {
|
|
39
|
+
modules: string[];
|
|
40
|
+
globals: string[];
|
|
41
|
+
symbolCount: number;
|
|
42
|
+
} {
|
|
43
|
+
return {
|
|
44
|
+
modules: Object.keys(env.modules).sort(),
|
|
45
|
+
globals: Object.keys(env.globals).sort().slice(0, 50),
|
|
46
|
+
symbolCount: env.stats.symbols,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host:从 npm 包 / @types 收集 .d.ts → harvestDts。
|
|
3
|
+
* 代数不碰 fs;这里只做「找到 dts 路径」这一层宿主职责。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readdirSync, statSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join, resolve, dirname, basename } from "node:path";
|
|
8
|
+
import { harvestDts, type HarvestedEnv } from "@nudojs/harvester";
|
|
9
|
+
import { typeValueToString, type TypeValue } from "@nudojs/core";
|
|
10
|
+
|
|
11
|
+
/** 在 node_modules 中解析包根(含 @types/*) */
|
|
12
|
+
export function resolvePackageRoot(
|
|
13
|
+
pkg: string,
|
|
14
|
+
fromDir: string = process.cwd(),
|
|
15
|
+
): string | undefined {
|
|
16
|
+
let dir = resolve(fromDir);
|
|
17
|
+
const bare = pkg.replace(/^@/, "").replace(/\//g, "__");
|
|
18
|
+
// 向上找 node_modules;每层重算候选(@types 优先:lodash 等包本体无 index.d.ts)
|
|
19
|
+
for (let i = 0; i < 8; i++) {
|
|
20
|
+
const typesPath = join(dir, "node_modules", "@types", bare);
|
|
21
|
+
if (existsSync(typesPath)) return typesPath;
|
|
22
|
+
const pkgPath = join(dir, "node_modules", pkg);
|
|
23
|
+
if (existsSync(pkgPath)) return pkgPath;
|
|
24
|
+
const parent = resolve(dir, "..");
|
|
25
|
+
if (parent === dir) break;
|
|
26
|
+
dir = parent;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const SKIP_DIRS = new Set(["node_modules", "test", "tests", "docs", "doc", "examples", "__tests__"]);
|
|
32
|
+
/** 单文件上限:巨型 lib.d.ts 级别拖垮 harvest */
|
|
33
|
+
const MAX_DTS_BYTES = 1_500_000;
|
|
34
|
+
|
|
35
|
+
/** 优先入口:index.d.ts / pkg 名 / lib 入口 */
|
|
36
|
+
function dtsPriority(p: string, root: string): number {
|
|
37
|
+
const base = basename(p);
|
|
38
|
+
const rel = p.slice(root.length);
|
|
39
|
+
if (base === "index.d.ts") return 0;
|
|
40
|
+
if (base === "index.d.mts") return 1;
|
|
41
|
+
if (rel.includes("/types/") && base === "index.d.ts") return 2;
|
|
42
|
+
if (base.endsWith(".d.ts") && !base.includes(".")) return 3; // foo.d.ts
|
|
43
|
+
return 10;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 递归收集包内 .d.ts(限制数量 + 跳过超大文件 + 入口优先)。
|
|
48
|
+
*/
|
|
49
|
+
export function collectDtsFiles(root: string, maxFiles = 8): string[] {
|
|
50
|
+
const out: string[] = [];
|
|
51
|
+
const walk = (dir: string): void => {
|
|
52
|
+
if (out.length >= maxFiles * 3) return; // 多收一点再排序截断
|
|
53
|
+
let entries: string[];
|
|
54
|
+
try {
|
|
55
|
+
entries = readdirSync(dir);
|
|
56
|
+
} catch {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
for (const name of entries) {
|
|
60
|
+
if (out.length >= maxFiles * 3) return;
|
|
61
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
62
|
+
const p = join(dir, name);
|
|
63
|
+
let st;
|
|
64
|
+
try {
|
|
65
|
+
st = statSync(p);
|
|
66
|
+
} catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (st.isDirectory()) walk(p);
|
|
70
|
+
else if (name.endsWith(".d.ts") && !name.endsWith(".d.ts.map")) {
|
|
71
|
+
if (st.size > MAX_DTS_BYTES) continue;
|
|
72
|
+
out.push(p);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
walk(root);
|
|
77
|
+
out.sort((a, b) => dtsPriority(a, root) - dtsPriority(b, root) || a.length - b.length);
|
|
78
|
+
return out.slice(0, maxFiles);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** package.json types/typings 入口(若存在且合理大小) */
|
|
82
|
+
function entryDtsFromPackageJson(root: string): string | undefined {
|
|
83
|
+
const pj = join(root, "package.json");
|
|
84
|
+
if (!existsSync(pj)) return undefined;
|
|
85
|
+
try {
|
|
86
|
+
const raw = JSON.parse(readFileSync(pj, "utf8")) as {
|
|
87
|
+
types?: string;
|
|
88
|
+
typings?: string;
|
|
89
|
+
exports?: Record<string, unknown>;
|
|
90
|
+
};
|
|
91
|
+
const entry = raw.types ?? raw.typings;
|
|
92
|
+
if (typeof entry === "string" && entry.endsWith(".d.ts")) {
|
|
93
|
+
const p = resolve(root, entry);
|
|
94
|
+
if (existsSync(p)) {
|
|
95
|
+
const st = statSync(p);
|
|
96
|
+
if (st.size <= MAX_DTS_BYTES) return p;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// ignore
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type PackageHarvest = {
|
|
106
|
+
pkg: string;
|
|
107
|
+
root: string;
|
|
108
|
+
dtsFiles: string[];
|
|
109
|
+
env: HarvestedEnv;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 从入口 .d.ts 沿 reference path / 相对 import 做 BFS 收集。
|
|
114
|
+
* 适合 @types 包(三斜线引用图);比整树 walk 更贴声明入口。
|
|
115
|
+
*/
|
|
116
|
+
export function collectDtsFromEntry(entry: string, maxFiles = 200): string[] {
|
|
117
|
+
const files: string[] = [];
|
|
118
|
+
const seen = new Set<string>();
|
|
119
|
+
const queue = [entry];
|
|
120
|
+
const REFERENCE_PATH_REGEX = /<reference\s+path=["']([^"']+)["']\s*\/>/g;
|
|
121
|
+
const RELATIVE_FROM_REGEX = /\bfrom\s+["'](\.[^"']+)["']/g;
|
|
122
|
+
|
|
123
|
+
while (queue.length > 0 && files.length < maxFiles) {
|
|
124
|
+
const current = queue.shift()!;
|
|
125
|
+
if (seen.has(current) || !existsSync(current)) continue;
|
|
126
|
+
seen.add(current);
|
|
127
|
+
if (!current.endsWith(".d.ts")) continue;
|
|
128
|
+
files.push(current);
|
|
129
|
+
|
|
130
|
+
let text: string;
|
|
131
|
+
try {
|
|
132
|
+
text = readFileSync(current, "utf-8");
|
|
133
|
+
} catch {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const dir = dirname(current);
|
|
138
|
+
for (const match of text.matchAll(REFERENCE_PATH_REGEX)) {
|
|
139
|
+
queue.push(resolve(dir, match[1]!));
|
|
140
|
+
}
|
|
141
|
+
for (const match of text.matchAll(RELATIVE_FROM_REGEX)) {
|
|
142
|
+
const base = resolve(dir, match[1]!);
|
|
143
|
+
for (const candidate of [`${base}.d.ts`, join(base, "index.d.ts")]) {
|
|
144
|
+
if (existsSync(candidate)) {
|
|
145
|
+
queue.push(candidate);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return files;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** harvest 一个 npm 包(或 @types 包) */
|
|
156
|
+
export function harvestPackage(
|
|
157
|
+
pkg: string,
|
|
158
|
+
fromDir?: string,
|
|
159
|
+
maxFiles = 8,
|
|
160
|
+
): PackageHarvest | { error: string } {
|
|
161
|
+
const root = resolvePackageRoot(pkg, fromDir);
|
|
162
|
+
if (!root) return { error: `package not found: ${pkg}` };
|
|
163
|
+
const entry = entryDtsFromPackageJson(root) ?? join(root, "index.d.ts");
|
|
164
|
+
// 入口 BFS 优先(@types 引用图);退化为整树 walk
|
|
165
|
+
let dtsFiles = existsSync(entry) ? collectDtsFromEntry(entry, maxFiles) : [];
|
|
166
|
+
if (dtsFiles.length === 0) {
|
|
167
|
+
const collected = collectDtsFiles(root, maxFiles);
|
|
168
|
+
dtsFiles =
|
|
169
|
+
entry && existsSync(entry) && !collected.includes(entry)
|
|
170
|
+
? [entry, ...collected.filter((f) => f !== entry)].slice(0, maxFiles)
|
|
171
|
+
: collected;
|
|
172
|
+
}
|
|
173
|
+
if (dtsFiles.length === 0) {
|
|
174
|
+
return { error: `no .d.ts under ${root}` };
|
|
175
|
+
}
|
|
176
|
+
const env = harvestDts(dtsFiles);
|
|
177
|
+
return { pkg, root, dtsFiles, env };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** harvest 结果 → 人类可读导出表 */
|
|
181
|
+
export function formatHarvestSummary(h: PackageHarvest): string {
|
|
182
|
+
const lines: string[] = [];
|
|
183
|
+
lines.push(`${h.pkg}: ${h.dtsFiles.length} d.ts, ${h.env.stats.symbols} symbols`);
|
|
184
|
+
const mods = Object.keys(h.env.modules);
|
|
185
|
+
for (const m of mods.slice(0, 5)) {
|
|
186
|
+
const exports = h.env.modules[m]!;
|
|
187
|
+
const names = Object.keys(exports).slice(0, 12);
|
|
188
|
+
lines.push(` module "${m}": ${names.join(", ")}${Object.keys(exports).length > 12 ? "…" : ""}`);
|
|
189
|
+
}
|
|
190
|
+
if (mods.length > 5) lines.push(` … ${mods.length - 5} more modules`);
|
|
191
|
+
return lines.join("\n");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** 取某个导出的 TypeValue 字符串 */
|
|
195
|
+
export function lookupHarvested(
|
|
196
|
+
h: PackageHarvest,
|
|
197
|
+
moduleName: string,
|
|
198
|
+
exportName: string,
|
|
199
|
+
): string | undefined {
|
|
200
|
+
const mod = h.env.modules[moduleName];
|
|
201
|
+
const tv = mod?.[exportName] ?? h.env.globals[exportName];
|
|
202
|
+
if (!tv) return undefined;
|
|
203
|
+
return typeValueToString(tv as TypeValue);
|
|
204
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harvest TypeValue 导出 → AbsModuleExports(供 Abs 模块图注入)。
|
|
3
|
+
* fnSig → absFunction + apply 直接返回声明返回类型(conf=mock)。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TypeValue } from "@nudojs/core";
|
|
7
|
+
import {
|
|
8
|
+
T,
|
|
9
|
+
type Abs,
|
|
10
|
+
typeValueToAbs,
|
|
11
|
+
confJoin,
|
|
12
|
+
getFnSig,
|
|
13
|
+
absFunction,
|
|
14
|
+
} from "@nudojs/core";
|
|
15
|
+
import type { AbsModuleExports } from "@nudojs/core";
|
|
16
|
+
import { dirname } from "node:path";
|
|
17
|
+
import type { PackageHarvest } from "./harvest-package.ts";
|
|
18
|
+
import { harvestPackageCached } from "./harvest-auto.ts";
|
|
19
|
+
|
|
20
|
+
function markMockConf(a: Abs): Abs {
|
|
21
|
+
a.conf = confJoin(a.conf, "mock");
|
|
22
|
+
return a;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 单个 TypeValue 导出 → Abs(可调用) */
|
|
26
|
+
export function harvestedValueToAbs(tv: TypeValue): Abs {
|
|
27
|
+
if (!tv) return markMockConf(typeValueToAbs(T.unknown));
|
|
28
|
+
const sig = getFnSig(tv);
|
|
29
|
+
if (sig) {
|
|
30
|
+
const ret = markMockConf(typeValueToAbs(sig.returnType));
|
|
31
|
+
const params =
|
|
32
|
+
tv.kind === "function" && tv.params?.length
|
|
33
|
+
? tv.params
|
|
34
|
+
: sig.paramTypes.map((_, i) => `_arg${i}`);
|
|
35
|
+
const dummyBody = {
|
|
36
|
+
type: "BlockStatement",
|
|
37
|
+
body: [],
|
|
38
|
+
directives: [],
|
|
39
|
+
} as never;
|
|
40
|
+
return markMockConf(
|
|
41
|
+
absFunction(params, {
|
|
42
|
+
body: dummyBody,
|
|
43
|
+
apply: () => ret,
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return markMockConf(typeValueToAbs(tv));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 把 harvest 的 modules/globals 合成 AbsModuleExports。
|
|
52
|
+
* 键同时登记:原始模块路径、包名、basename(path / @types/path)。
|
|
53
|
+
*/
|
|
54
|
+
export function harvestToAbsModules(
|
|
55
|
+
pkg: string,
|
|
56
|
+
fromDir: string,
|
|
57
|
+
): Record<string, AbsModuleExports> {
|
|
58
|
+
const h = harvestPackageCached(pkg, fromDir);
|
|
59
|
+
if (!h) return {};
|
|
60
|
+
return packageHarvestToAbsModules(pkg, h);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function packageHarvestToAbsModules(
|
|
64
|
+
pkg: string,
|
|
65
|
+
h: PackageHarvest,
|
|
66
|
+
): Record<string, AbsModuleExports> {
|
|
67
|
+
const out: Record<string, AbsModuleExports> = {};
|
|
68
|
+
|
|
69
|
+
const convertRecord = (
|
|
70
|
+
rec: Record<string, TypeValue>,
|
|
71
|
+
): Record<string, Abs> => {
|
|
72
|
+
const named: Record<string, Abs> = {};
|
|
73
|
+
for (const [k, v] of Object.entries(rec)) {
|
|
74
|
+
named[k] = harvestedValueToAbs(v);
|
|
75
|
+
}
|
|
76
|
+
return named;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const register = (key: string, exports: AbsModuleExports) => {
|
|
80
|
+
if (!out[key]) out[key] = exports;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
for (const [mod, rec] of Object.entries(h.env.modules)) {
|
|
84
|
+
const named = convertRecord(rec);
|
|
85
|
+
const exports: AbsModuleExports = { named };
|
|
86
|
+
const def = rec["default"];
|
|
87
|
+
if (def) exports.default = harvestedValueToAbs(def);
|
|
88
|
+
register(mod, exports);
|
|
89
|
+
// 包名 / 去掉 @types/ 前缀 / basename
|
|
90
|
+
register(pkg, exports);
|
|
91
|
+
const stripped = mod.replace(/^@types\//, "").replace(/\\/g, "/");
|
|
92
|
+
if (stripped) register(stripped, exports);
|
|
93
|
+
const base = stripped.split("/").filter(Boolean).pop();
|
|
94
|
+
if (base && base !== pkg) register(base, exports);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// globals 也挂到包名(default / namespace 消费)
|
|
98
|
+
const globalNamed = convertRecord(h.env.globals);
|
|
99
|
+
if (Object.keys(globalNamed).length > 0) {
|
|
100
|
+
const existing = out[pkg];
|
|
101
|
+
if (existing) {
|
|
102
|
+
out[pkg] = { named: { ...globalNamed, ...existing.named }, default: existing.default };
|
|
103
|
+
} else {
|
|
104
|
+
out[pkg] = { named: globalNamed };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 裸说明符 → Abs 导出表(走 harvest 缓存) */
|
|
112
|
+
export function bareSpecToAbsModules(
|
|
113
|
+
spec: string,
|
|
114
|
+
fromFile: string,
|
|
115
|
+
): AbsModuleExports | undefined {
|
|
116
|
+
// node: 内建交给 env,不 harvest
|
|
117
|
+
if (!spec || spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("node:")) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const parts = spec.split("/");
|
|
121
|
+
const pkg = spec.startsWith("@")
|
|
122
|
+
? parts.length >= 2
|
|
123
|
+
? `${parts[0]}/${parts[1]}`
|
|
124
|
+
: undefined
|
|
125
|
+
: parts[0];
|
|
126
|
+
if (!pkg) return undefined;
|
|
127
|
+
const table = harvestToAbsModules(pkg, dirname(fromFile));
|
|
128
|
+
return table[spec] ?? table[pkg];
|
|
129
|
+
}
|