@nudojs/service 3.0.0 → 5.0.0-beta.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,199 @@
1
+ import {
2
+ collectDtsFiles,
3
+ depsCacheRoot,
4
+ dtsClosureHash,
5
+ harvestCacheKey,
6
+ materializeHarvestJson,
7
+ readHarvestDisk,
8
+ resolvePackageRoot,
9
+ serializeHarvestJson,
10
+ writeHarvestDisk
11
+ } from "./chunk-U7JYTRIB.js";
12
+ import {
13
+ BoundedLruMap,
14
+ loadEnvs
15
+ } from "./chunk-PVJIMQRI.js";
16
+
17
+ // src/harvest-node.ts
18
+ import { existsSync, statSync, readFileSync } from "fs";
19
+ import { join } from "path";
20
+ import { harvestDts } from "@nudojs/harvester";
21
+ import { createEnvironment } from "@nudojs/core";
22
+ var HARVEST_NODE_DEFAULT_MAX_FILES = 12;
23
+ var HARVEST_NODE_DEFAULT_MAX_MS = 2500;
24
+ var NODE_HARVEST_CACHE_MAX = 32;
25
+ var nodeHarvestCache = new BoundedLruMap(NODE_HARVEST_CACHE_MAX);
26
+ function notFoundCacheKey(fromDir) {
27
+ return `not-found|@types/node|${fromDir ?? ""}`;
28
+ }
29
+ function nodeHarvestCacheKey(root, maxFiles, maxMs) {
30
+ let sig = "nostat";
31
+ try {
32
+ const st = statSync(join(root, "package.json"));
33
+ sig = `${st.size}:${Math.floor(st.mtimeMs)}`;
34
+ } catch {
35
+ }
36
+ return `${root}|${maxFiles}|${maxMs}|${sig}`;
37
+ }
38
+ function clearNodeHarvestCache() {
39
+ nodeHarvestCache.clear();
40
+ }
41
+ function getNodeHarvestCacheSize() {
42
+ return nodeHarvestCache.size;
43
+ }
44
+ function isHarvestNodeDisabled(env = process.env) {
45
+ return env.NUDO_HARVEST_NODE === "off";
46
+ }
47
+ function readPkgVersion(root) {
48
+ try {
49
+ const raw = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
50
+ return raw.version;
51
+ } catch {
52
+ return void 0;
53
+ }
54
+ }
55
+ function handwrittenNodeEnv() {
56
+ try {
57
+ const loaded = loadEnvs(["node"], createEnvironment());
58
+ return {
59
+ modules: loaded.modules,
60
+ globals: loaded.globals,
61
+ stats: {
62
+ files: 0,
63
+ symbols: Object.keys(loaded.globals).length,
64
+ skipped: 0
65
+ }
66
+ };
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+ function degradeToHandwritten(error, reason, cached = false) {
72
+ const env = handwrittenNodeEnv();
73
+ if (env) {
74
+ return {
75
+ ok: true,
76
+ env,
77
+ root: "@nudojs/env",
78
+ files: 0,
79
+ stats: env.stats,
80
+ cached,
81
+ degraded: true
82
+ };
83
+ }
84
+ return { ok: false, error, reason, ...cached ? { cached: true } : {} };
85
+ }
86
+ function harvestNodeTypes(fromDir, maxFiles = HARVEST_NODE_DEFAULT_MAX_FILES, maxMs = HARVEST_NODE_DEFAULT_MAX_MS) {
87
+ if (isHarvestNodeDisabled()) {
88
+ return {
89
+ ok: false,
90
+ error: "harvest disabled via NUDO_HARVEST_NODE=off",
91
+ reason: "disabled"
92
+ };
93
+ }
94
+ const root = resolvePackageRoot("@types/node", fromDir) ?? resolvePackageRoot("node", fromDir);
95
+ if (!root || !existsSync(root)) {
96
+ const missKey = notFoundCacheKey(fromDir);
97
+ const miss = nodeHarvestCache.get(missKey);
98
+ if (miss) {
99
+ return miss.ok ? { ...miss, cached: true } : { ...miss, cached: true };
100
+ }
101
+ const result = degradeToHandwritten("@types/node not found", "not-found");
102
+ nodeHarvestCache.set(missKey, result);
103
+ return result;
104
+ }
105
+ const key = nodeHarvestCacheKey(root, maxFiles, maxMs);
106
+ const hit = nodeHarvestCache.get(key);
107
+ if (hit) {
108
+ return hit.ok ? { ...hit, cached: true } : { ...hit, cached: true };
109
+ }
110
+ const dts = collectDtsFiles(root, maxFiles);
111
+ if (dts.length === 0) {
112
+ const result = degradeToHandwritten(`no .d.ts under ${root}`, "no-dts");
113
+ nodeHarvestCache.set(key, result);
114
+ return result;
115
+ }
116
+ const diskRoot = depsCacheRoot();
117
+ const dtsHash = dtsClosureHash(dts);
118
+ const pkgVersion = readPkgVersion(root);
119
+ const diskKey = harvestCacheKey("@types/node", {
120
+ dtsHash,
121
+ maxFiles,
122
+ ...pkgVersion !== void 0 ? { pkgVersion } : {}
123
+ });
124
+ if (diskRoot) {
125
+ const diskHit = readHarvestDisk(diskRoot, diskKey);
126
+ const env = diskHit ? materializeHarvestJson(diskHit) : null;
127
+ if (env) {
128
+ const stats = {
129
+ files: env.stats.files,
130
+ symbols: env.stats.symbols,
131
+ skipped: env.stats.skipped
132
+ };
133
+ const result = {
134
+ ok: true,
135
+ env,
136
+ root,
137
+ files: stats.files,
138
+ stats,
139
+ cached: true
140
+ };
141
+ nodeHarvestCache.set(key, result);
142
+ return result;
143
+ }
144
+ }
145
+ try {
146
+ const env = harvestDts(dts, { maxMs });
147
+ const stats = {
148
+ files: env.stats.files,
149
+ symbols: env.stats.symbols,
150
+ skipped: env.stats.skipped
151
+ };
152
+ if (diskRoot) {
153
+ writeHarvestDisk(
154
+ diskRoot,
155
+ diskKey,
156
+ serializeHarvestJson("@types/node", env, {
157
+ dtsHash,
158
+ maxFiles,
159
+ ...pkgVersion !== void 0 ? { pkgVersion } : {}
160
+ })
161
+ );
162
+ }
163
+ const result = {
164
+ ok: true,
165
+ env,
166
+ root,
167
+ files: stats.files,
168
+ stats,
169
+ cached: false
170
+ };
171
+ nodeHarvestCache.set(key, result);
172
+ return result;
173
+ } catch (e) {
174
+ const result = degradeToHandwritten(
175
+ `harvest failed: ${e.message}`,
176
+ "failed"
177
+ );
178
+ nodeHarvestCache.set(key, result);
179
+ return result;
180
+ }
181
+ }
182
+ function summarizeNodeEnv(env) {
183
+ return {
184
+ modules: Object.keys(env.modules).sort(),
185
+ globals: Object.keys(env.globals).sort().slice(0, 50),
186
+ symbolCount: env.stats.symbols
187
+ };
188
+ }
189
+
190
+ export {
191
+ HARVEST_NODE_DEFAULT_MAX_FILES,
192
+ HARVEST_NODE_DEFAULT_MAX_MS,
193
+ clearNodeHarvestCache,
194
+ getNodeHarvestCacheSize,
195
+ isHarvestNodeDisabled,
196
+ handwrittenNodeEnv,
197
+ harvestNodeTypes,
198
+ summarizeNodeEnv
199
+ };
@@ -0,0 +1,89 @@
1
+ import {
2
+ analysisConfig,
3
+ findProjectConfig,
4
+ matchesEmitAllowlist
5
+ } from "./chunk-YLALZXTZ.js";
6
+
7
+ // src/target-path.ts
8
+ function isNudoTargetPath(path) {
9
+ const lower = path.toLowerCase();
10
+ if (lower.endsWith(".d.ts")) return false;
11
+ if (lower.endsWith(".nudo.js") || lower.endsWith(".nudo.mjs") || lower.endsWith(".nudo.ts") || lower.endsWith(".nudo.draft.js") || lower.endsWith(".nudo.draft.mjs") || lower.endsWith(".nudo.draft.ts")) {
12
+ return false;
13
+ }
14
+ return lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".ts");
15
+ }
16
+
17
+ // src/analysis-scope.ts
18
+ import { dirname } from "path";
19
+ import { existsSync } from "fs";
20
+ import { sidecarPathOf } from "@nudojs/core";
21
+ var NOISY_WARNING_CODES = /* @__PURE__ */ new Set([
22
+ "nudo:unknown-recv",
23
+ "nudo:builtin-unknown",
24
+ "nudo:no-signature"
25
+ ]);
26
+ function filterDiagnosticsByLevel(diags, level) {
27
+ if (level === "verbose") return diags;
28
+ if (level === "off") return [];
29
+ if (level === "errors") return diags.filter((d) => d.severity === "error");
30
+ return diags.filter((d) => {
31
+ if (d.severity === "error") return true;
32
+ if (d.severity === "info") return false;
33
+ if (d.severity === "warning" && d.code && NOISY_WARNING_CODES.has(d.code)) return false;
34
+ return d.severity === "warning";
35
+ });
36
+ }
37
+ function diagnosticsLevelForFile(filePath) {
38
+ return analysisConfig(findProjectConfig(dirname(filePath))?.config).diagnostics;
39
+ }
40
+ function hasNudoDirectives(source) {
41
+ return /@nudo:(case|mock|pure|skip|sample|contract|import|env|mock-module|as|replace)\b/.test(source);
42
+ }
43
+ function stripCommentsAndStrings(source) {
44
+ return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ").replace(/'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|`(?:\\.|[^`\\])*`/g, '""');
45
+ }
46
+ function hasExport(source) {
47
+ const s = stripCommentsAndStrings(source);
48
+ return /(^|[\s;}])export\b/.test(s) || /\bmodule\.exports\b/.test(s) || /\bexports\s*[.[]/.test(s) || /Object\.assign\s*\(\s*(module\.)?exports\b/.test(s);
49
+ }
50
+ function shouldAnalyzeFile(filePath, source, config) {
51
+ if (!isNudoTargetPath(filePath)) return false;
52
+ const proj = findProjectConfig(dirname(filePath));
53
+ const cfg = config ?? analysisConfig(proj?.config);
54
+ const projectDir = proj?.projectDir;
55
+ if (cfg.exclude.length > 0 && projectDir) {
56
+ if (matchesEmitAllowlist(filePath, projectDir, cfg.exclude)) return false;
57
+ } else if (cfg.exclude.length > 0 && !projectDir) {
58
+ const norm = filePath.replace(/\\/g, "/");
59
+ if (cfg.exclude.some((p) => simpleExcludeHit(p, norm))) return false;
60
+ }
61
+ if (cfg.include.length > 0 && projectDir) {
62
+ if (!matchesEmitAllowlist(filePath, projectDir, cfg.include)) return false;
63
+ }
64
+ if (cfg.mode === "all") return true;
65
+ const text = source;
66
+ if (text !== void 0 && hasNudoDirectives(text)) return true;
67
+ if (cfg.mode === "directives") return false;
68
+ if (text !== void 0 && hasExport(text)) return true;
69
+ try {
70
+ if (existsSync(sidecarPathOf(filePath))) return true;
71
+ } catch {
72
+ }
73
+ return false;
74
+ }
75
+ function simpleExcludeHit(pattern, path) {
76
+ const bare = pattern.replace(/^\*\*/, "").replace(/^\//, "").replace(/\/\*\*$/, "").replace(/^\*\//, "");
77
+ if (!bare || bare.includes("*")) {
78
+ return /\/(node_modules|dist|coverage)\//.test("/" + path + "/");
79
+ }
80
+ return path.includes(`/${bare}/`) || path.endsWith(`/${bare}`);
81
+ }
82
+
83
+ export {
84
+ isNudoTargetPath,
85
+ filterDiagnosticsByLevel,
86
+ diagnosticsLevelForFile,
87
+ hasNudoDirectives,
88
+ shouldAnalyzeFile
89
+ };
@@ -0,0 +1,246 @@
1
+ // src/evaluator/env-loader.ts
2
+ import { readFileSync, existsSync, statSync, mkdirSync, writeFileSync } from "fs";
3
+ import { resolve as resolvePath, join as joinPath } from "path";
4
+ import { pathToFileURL } from "url";
5
+ import { createRequire } from "module";
6
+ import { createHash } from "crypto";
7
+ import { tmpdir } from "os";
8
+ import { defineEnv as defineEsEnv } from "@nudojs/env/es";
9
+ import { defineEnv as defineWebEnv } from "@nudojs/env/web";
10
+ import { defineEnv as defineNodeEnv } from "@nudojs/env/node";
11
+
12
+ // src/lru-map.ts
13
+ var BoundedLruMap = class {
14
+ map = /* @__PURE__ */ new Map();
15
+ max;
16
+ constructor(max) {
17
+ this.max = max;
18
+ }
19
+ get size() {
20
+ return this.map.size;
21
+ }
22
+ /** 当前上限(可动态收紧;收紧时立刻按 LRU 压到新上限) */
23
+ getMax() {
24
+ return this.max;
25
+ }
26
+ setMax(max) {
27
+ this.max = max;
28
+ this.trim();
29
+ }
30
+ has(key) {
31
+ return this.map.has(key);
32
+ }
33
+ get(key) {
34
+ const hit = this.map.get(key);
35
+ if (hit === void 0) return void 0;
36
+ this.map.delete(key);
37
+ this.map.set(key, hit);
38
+ return hit;
39
+ }
40
+ /** 命中不改变 LRU 序(只读探测) */
41
+ peek(key) {
42
+ return this.map.get(key);
43
+ }
44
+ set(key, value) {
45
+ if (this.max <= 0) return;
46
+ while (this.map.size >= this.max && !this.map.has(key)) {
47
+ const oldest = this.map.keys().next().value;
48
+ if (oldest === void 0) break;
49
+ this.map.delete(oldest);
50
+ }
51
+ this.map.delete(key);
52
+ this.map.set(key, value);
53
+ }
54
+ delete(key) {
55
+ return this.map.delete(key);
56
+ }
57
+ clear() {
58
+ this.map.clear();
59
+ }
60
+ keys() {
61
+ return this.map.keys();
62
+ }
63
+ values() {
64
+ return this.map.values();
65
+ }
66
+ entries() {
67
+ return this.map.entries();
68
+ }
69
+ /** 压到当前 max(调低上限时收内存) */
70
+ trim() {
71
+ while (this.map.size > this.max) {
72
+ const oldest = this.map.keys().next().value;
73
+ if (oldest === void 0) break;
74
+ this.map.delete(oldest);
75
+ }
76
+ }
77
+ };
78
+
79
+ // src/evaluator/env-loader.ts
80
+ var envFactories = {
81
+ es: defineEsEnv,
82
+ web: defineWebEnv,
83
+ node: defineNodeEnv
84
+ };
85
+ var impliedDeps = {
86
+ web: ["es"],
87
+ node: ["es"]
88
+ };
89
+ function resolveEnvNames(names) {
90
+ const resolved = /* @__PURE__ */ new Set();
91
+ const visit = (name) => {
92
+ if (resolved.has(name)) return;
93
+ const deps = impliedDeps[name];
94
+ if (deps) deps.forEach(visit);
95
+ resolved.add(name);
96
+ };
97
+ names.forEach(visit);
98
+ return [...resolved];
99
+ }
100
+ var PATH_ENV_CACHE_MAX = 64;
101
+ var PATH_ENV_BY_PATH_MAX = 64;
102
+ var PATH_ENV_BASE_DIRS_MAX = 64;
103
+ var pathEnvCache = new BoundedLruMap(PATH_ENV_CACHE_MAX);
104
+ var pathEnvByPath = new BoundedLruMap(PATH_ENV_BY_PATH_MAX);
105
+ var pathEnvBaseDirs = /* @__PURE__ */ new Set();
106
+ function addPathEnvBaseDir(baseDir) {
107
+ pathEnvBaseDirs.delete(baseDir);
108
+ pathEnvBaseDirs.add(baseDir);
109
+ while (pathEnvBaseDirs.size > PATH_ENV_BASE_DIRS_MAX) {
110
+ const oldest = pathEnvBaseDirs.values().next().value;
111
+ if (oldest === void 0) break;
112
+ pathEnvBaseDirs.delete(oldest);
113
+ }
114
+ }
115
+ function getPathEnvCacheSizes() {
116
+ return {
117
+ byKey: pathEnvCache.size,
118
+ byPath: pathEnvByPath.size,
119
+ baseDirs: pathEnvBaseDirs.size
120
+ };
121
+ }
122
+ function isPathEnvName(name, baseDir) {
123
+ if (name in envFactories) return false;
124
+ if (name.includes("/") || name.startsWith("./") || name.startsWith("../")) return true;
125
+ const resolved = resolvePath(baseDir, name);
126
+ return resolved.endsWith(".ts") && existsSync(resolved);
127
+ }
128
+ function rewriteBareImports(text) {
129
+ const require2 = createRequire(import.meta.url);
130
+ let rewrote = false;
131
+ const out = text.replace(/(["'])(@nudojs\/[a-z0-9-]+)\1/g, (_m, quote, spec) => {
132
+ try {
133
+ const entry = require2.resolve(spec);
134
+ const url = pathToFileURL(entry).href;
135
+ rewrote = true;
136
+ return `${quote}${url}${quote}`;
137
+ } catch {
138
+ return `${quote}${spec}${quote}`;
139
+ }
140
+ });
141
+ return rewrote ? out : null;
142
+ }
143
+ async function importPathEnv(resolvedPath, mtimeMs) {
144
+ const cacheKey = `${resolvedPath}:${mtimeMs}`;
145
+ if (pathEnvCache.get(cacheKey)) return;
146
+ let mod = null;
147
+ try {
148
+ const url = pathToFileURL(resolvedPath).href + `?mtime=${mtimeMs}`;
149
+ mod = await import(url);
150
+ } catch {
151
+ mod = null;
152
+ }
153
+ if (!mod) {
154
+ try {
155
+ const text = readFileSync(resolvedPath, "utf-8");
156
+ const rewritten = rewriteBareImports(text);
157
+ if (rewritten === null) return;
158
+ const cacheDir = joinPath(tmpdir(), "nudo-env");
159
+ mkdirSync(cacheDir, { recursive: true });
160
+ const hash = createHash("md5").update(`${resolvedPath}:${mtimeMs}`).digest("hex").slice(0, 16);
161
+ const copyPath = joinPath(cacheDir, `${hash}.ts`);
162
+ writeFileSync(copyPath, rewritten, "utf-8");
163
+ mod = await import(pathToFileURL(copyPath).href);
164
+ } catch {
165
+ return;
166
+ }
167
+ }
168
+ if (mod && typeof mod.defineEnv === "function") {
169
+ pathEnvCache.set(cacheKey, mod.defineEnv);
170
+ pathEnvByPath.set(resolvedPath, {
171
+ factory: mod.defineEnv,
172
+ mtimeMs
173
+ });
174
+ }
175
+ }
176
+ function lookupPathEnv(name) {
177
+ if (pathEnvByPath.size === 0) return void 0;
178
+ const candidates = name.startsWith("/") ? [name] : [name, ...[...pathEnvBaseDirs].map((d) => resolvePath(d, name))];
179
+ for (const candidate of candidates) {
180
+ const hit = pathEnvByPath.get(candidate);
181
+ if (!hit) continue;
182
+ try {
183
+ const { mtimeMs } = statSync(candidate);
184
+ if (mtimeMs !== hit.mtimeMs) {
185
+ pathEnvByPath.delete(candidate);
186
+ return void 0;
187
+ }
188
+ } catch {
189
+ pathEnvByPath.delete(candidate);
190
+ return void 0;
191
+ }
192
+ return hit.factory;
193
+ }
194
+ return void 0;
195
+ }
196
+ function clearPathEnvCaches() {
197
+ pathEnvCache.clear();
198
+ pathEnvByPath.clear();
199
+ pathEnvBaseDirs.clear();
200
+ }
201
+ async function preloadPathEnvs(envNames, baseDir) {
202
+ addPathEnvBaseDir(baseDir);
203
+ for (const name of envNames) {
204
+ if (!isPathEnvName(name, baseDir)) continue;
205
+ const resolved = resolvePath(baseDir, name);
206
+ if (!existsSync(resolved)) continue;
207
+ try {
208
+ const { mtimeMs } = statSync(resolved);
209
+ await importPathEnv(resolved, mtimeMs);
210
+ } catch {
211
+ }
212
+ }
213
+ }
214
+ async function loadEnvsAsync(envNames, globalEnv, baseDir = process.cwd()) {
215
+ await preloadPathEnvs(envNames, baseDir);
216
+ return loadEnvs(envNames, globalEnv);
217
+ }
218
+ function loadEnvs(envNames, globalEnv) {
219
+ const allModules = {};
220
+ const allGlobals = {};
221
+ const resolved = resolveEnvNames(envNames);
222
+ for (const name of resolved) {
223
+ const factory = envFactories[name] ?? lookupPathEnv(name);
224
+ if (!factory) continue;
225
+ const def = factory();
226
+ for (const [key, value] of Object.entries(def.globals)) {
227
+ allGlobals[key] = value;
228
+ globalEnv.bind(key, value);
229
+ }
230
+ if (def.modules) {
231
+ for (const [modName, exports] of Object.entries(def.modules)) {
232
+ allModules[modName] = { ...allModules[modName], ...exports };
233
+ }
234
+ }
235
+ }
236
+ return { modules: allModules, globals: allGlobals };
237
+ }
238
+
239
+ export {
240
+ BoundedLruMap,
241
+ getPathEnvCacheSizes,
242
+ clearPathEnvCaches,
243
+ preloadPathEnvs,
244
+ loadEnvsAsync,
245
+ loadEnvs
246
+ };