@nudojs/service 0.3.0 → 0.3.1

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 (72) hide show
  1. package/dist/index.d.ts +18 -3
  2. package/dist/index.js +80 -55
  3. package/package.json +16 -6
  4. package/CHANGELOG.md +0 -68
  5. package/src/__tests__/abs-bindings.test.ts +0 -53
  6. package/src/__tests__/abs-call-records.test.ts +0 -33
  7. package/src/__tests__/abs-calls.test.ts +0 -59
  8. package/src/__tests__/abs-module-cache.test.ts +0 -121
  9. package/src/__tests__/abs-modules-graph.test.ts +0 -100
  10. package/src/__tests__/abs-typevalue-diff.test.ts +0 -111
  11. package/src/__tests__/analyzer.test.ts +0 -880
  12. package/src/__tests__/as-replace.test.ts +0 -165
  13. package/src/__tests__/bpath-arg-provenance.test.ts +0 -104
  14. package/src/__tests__/bpath-as.test.ts +0 -54
  15. package/src/__tests__/bpath-cache-key.test.ts +0 -33
  16. package/src/__tests__/bpath-calls.test.ts +0 -55
  17. package/src/__tests__/bpath-diagnostics.test.ts +0 -78
  18. package/src/__tests__/bpath-env-modules.test.ts +0 -30
  19. package/src/__tests__/bpath-env.test.ts +0 -80
  20. package/src/__tests__/bpath-hosted-skip.test.ts +0 -71
  21. package/src/__tests__/bpath-import-default-ns.test.ts +0 -106
  22. package/src/__tests__/bpath-method-dedupe.test.ts +0 -65
  23. package/src/__tests__/bpath-method-diag.test.ts +0 -45
  24. package/src/__tests__/bpath-module-diags.test.ts +0 -101
  25. package/src/__tests__/bpath-path-env.test.ts +0 -46
  26. package/src/__tests__/bpath-reexport.test.ts +0 -131
  27. package/src/__tests__/bpath-replace.test.ts +0 -52
  28. package/src/__tests__/bpath-require.test.ts +0 -66
  29. package/src/__tests__/bpath-run.test.ts +0 -109
  30. package/src/__tests__/case-emitter.test.ts +0 -636
  31. package/src/__tests__/cjs-require.test.ts +0 -54
  32. package/src/__tests__/diagnostics.test.ts +0 -60
  33. package/src/__tests__/dts-generator.test.ts +0 -382
  34. package/src/__tests__/fn-cache-evict.test.ts +0 -56
  35. package/src/__tests__/fn-sig-impl.test.ts +0 -247
  36. package/src/__tests__/guard-generator.test.ts +0 -29
  37. package/src/__tests__/harvest-auto.test.ts +0 -43
  38. package/src/__tests__/harvest-node.test.ts +0 -24
  39. package/src/__tests__/harvest-package.test.ts +0 -38
  40. package/src/__tests__/harvest-to-abs.test.ts +0 -46
  41. package/src/__tests__/hover-intension.test.ts +0 -39
  42. package/src/__tests__/infer-json.test.ts +0 -35
  43. package/src/__tests__/integration.test.ts +0 -421
  44. package/src/__tests__/m3-intension.test.ts +0 -28
  45. package/src/__tests__/mock-seed-fingerprint.test.ts +0 -64
  46. package/src/__tests__/schema-generator.test.ts +0 -38
  47. package/src/__tests__/session-cache-evict.test.ts +0 -90
  48. package/src/__tests__/static-imports.test.ts +0 -44
  49. package/src/abs-modules-graph.ts +0 -387
  50. package/src/analysis-file-cache.ts +0 -51
  51. package/src/analyzer.ts +0 -3133
  52. package/src/bpath-diagnostics.ts +0 -283
  53. package/src/bpath-run.ts +0 -409
  54. package/src/case-emitter.ts +0 -477
  55. package/src/dts-generator.ts +0 -291
  56. package/src/env-to-abs.ts +0 -57
  57. package/src/fn-analysis-cache.ts +0 -119
  58. package/src/guard-generator.ts +0 -57
  59. package/src/harvest-auto.ts +0 -86
  60. package/src/harvest-node.ts +0 -48
  61. package/src/harvest-package.ts +0 -204
  62. package/src/harvest-to-abs.ts +0 -129
  63. package/src/index.ts +0 -150
  64. package/src/infer-json.ts +0 -137
  65. package/src/load-module.ts +0 -34
  66. package/src/mock-abs.ts +0 -275
  67. package/src/schema-generator.ts +0 -43
  68. package/src/semantic-tokens.ts +0 -216
  69. package/src/session-cache.ts +0 -50
  70. package/src/static-imports.ts +0 -241
  71. package/src/target-path.ts +0 -17
  72. package/tsconfig.json +0 -13
@@ -1,421 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { typeValueToString } from "@nudojs/core";
3
- import {
4
- analyzeFile,
5
- typeValueToZodSchema,
6
- generateGuardFunction,
7
- generateDts,
8
- typeValueToTSType,
9
- } from "../index.ts";
10
-
11
- describe("integration: full pipeline", () => {
12
- it("infers types through the complete analyze → generate pipeline", () => {
13
- const source = `
14
- /**
15
- * @nudo:case "strings" (T.string)
16
- * @nudo:case "numbers" (T.number)
17
- */
18
- function process(x) {
19
- if (typeof x === "string") return x.toUpperCase();
20
- return x + 1;
21
- }
22
- `;
23
- const result = analyzeFile("/test.js", source);
24
-
25
- // Verify inference
26
- expect(result.functions).toHaveLength(1);
27
- expect(result.functions[0].name).toBe("process");
28
- expect(result.functions[0].cases).toHaveLength(2);
29
- expect(result.functions[0].combined).toBeDefined();
30
-
31
- // Verify Zod generation for each case result
32
- const zodSchema0 = typeValueToZodSchema(result.functions[0].cases[0].result);
33
- expect(zodSchema0).toBeTruthy();
34
- expect(typeof zodSchema0).toBe("string");
35
-
36
- const zodSchema1 = typeValueToZodSchema(result.functions[0].cases[1].result);
37
- expect(zodSchema1).toBeTruthy();
38
-
39
- // Verify guard generation
40
- const guard = generateGuardFunction("isProcessOutput", result.functions[0].cases[0].result);
41
- expect(guard).toContain("function isProcessOutput");
42
- expect(guard).toContain("export function");
43
-
44
- // Verify DTS generation
45
- const dts = generateDts(result);
46
- expect(dts).toContain("export declare function process");
47
- expect(dts).toContain("strings");
48
- expect(dts).toContain("numbers");
49
- });
50
-
51
- it("handles a single-case function end-to-end", () => {
52
- const source = `
53
- /**
54
- * @nudo:case "test" (T.number)
55
- */
56
- function double(x) {
57
- return x * 2;
58
- }
59
- `;
60
- const result = analyzeFile("/test/double.js", source);
61
-
62
- expect(result.functions).toHaveLength(1);
63
- const fn = result.functions[0];
64
- expect(fn.name).toBe("double");
65
- expect(fn.cases).toHaveLength(1);
66
- expect(typeValueToString(fn.cases[0].result)).toBe("number");
67
-
68
- // Zod schema should produce a valid z.number() call
69
- const zod = typeValueToZodSchema(fn.cases[0].result);
70
- expect(zod).toBe("z.number()");
71
-
72
- // Guard should check typeof
73
- const guard = generateGuardFunction("isDoubleOutput", fn.cases[0].result);
74
- expect(guard).toContain("typeof data === 'number'");
75
-
76
- // DTS should have a single declaration line
77
- const dts = generateDts(result);
78
- expect(dts).toContain("export declare function double(x: number): number;");
79
- });
80
- });
81
-
82
- describe("integration: Zod schema generation", () => {
83
- it("generates Zod schemas from inferred object types", () => {
84
- const source = `
85
- /**
86
- * @nudo:case "test" ({ name: "Alice", age: 30 })
87
- */
88
- function getUser(config) {
89
- return config;
90
- }
91
- `;
92
- const result = analyzeFile("/test/user.js", source);
93
- const fn = result.functions[0];
94
- const zod = typeValueToZodSchema(fn.cases[0].result);
95
-
96
- expect(zod).toContain("z.object");
97
- expect(zod).toContain("name");
98
- expect(zod).toContain("age");
99
- });
100
-
101
- it("generates Zod schemas for union return types", () => {
102
- const source = `
103
- /**
104
- * @nudo:case "string" (T.string)
105
- * @nudo:case "number" (T.number)
106
- */
107
- function parse(x) {
108
- if (typeof x === "string") return x;
109
- return x;
110
- }
111
- `;
112
- const result = analyzeFile("/test/parse.js", source);
113
- const fn = result.functions[0];
114
-
115
- // Each case should produce a valid Zod schema
116
- expect(typeValueToZodSchema(fn.cases[0].result)).toBe("z.string()");
117
- expect(typeValueToZodSchema(fn.cases[1].result)).toBe("z.number()");
118
-
119
- // Combined type should produce a union schema
120
- const combined = fn.combined!;
121
- const combinedZod = typeValueToZodSchema(combined);
122
- expect(combinedZod).toContain("z.union");
123
- });
124
- });
125
-
126
- describe("integration: guard generation", () => {
127
- it("generates guards for complex inferred types", () => {
128
- const source = `
129
- /**
130
- * @nudo:case "test" ([1, "two", true])
131
- */
132
- function identity(x) {
133
- return x;
134
- }
135
- `;
136
- const result = analyzeFile("/test/tuple.js", source);
137
- const fn = result.functions[0];
138
- const guard = generateGuardFunction("isTuple", fn.cases[0].result);
139
-
140
- expect(guard).toContain("export function isTuple");
141
- expect(guard).toContain("Array.isArray");
142
- expect(guard).toContain("data.length === 3");
143
- // Literal values produce equality checks, not typeof checks
144
- expect(guard).toContain("data[0] === 1");
145
- expect(guard).toContain('data[1] === "two"');
146
- expect(guard).toContain("data[2] === true");
147
- });
148
-
149
- it("generates guards for object return types", () => {
150
- const source = `
151
- /**
152
- * @nudo:case "test" ({ id: 1, active: true })
153
- */
154
- function getRecord(x) {
155
- return x;
156
- }
157
- `;
158
- const result = analyzeFile("/test/record.js", source);
159
- const fn = result.functions[0];
160
- const guard = generateGuardFunction("isRecord", fn.cases[0].result);
161
-
162
- expect(guard).toContain("export function isRecord");
163
- expect(guard).toContain("typeof data === 'object'");
164
- expect(guard).toContain("data !== null");
165
- expect(guard).toContain("data.id");
166
- expect(guard).toContain("data.active");
167
- });
168
- });
169
-
170
- describe("integration: DTS generation", () => {
171
- it("generates declarations for functions returning inferred object types", () => {
172
- const source = `
173
- /**
174
- * @nudo:case "test" ({ x: 1, y: "hello" })
175
- */
176
- function makePoint(config) {
177
- return config;
178
- }
179
- `;
180
- const result = analyzeFile("/test/point.js", source);
181
- const dts = generateDts(result);
182
-
183
- expect(dts).toContain("export declare function makePoint");
184
- expect(dts).toContain("x:");
185
- expect(dts).toContain("y:");
186
- });
187
-
188
- it("generates TS type strings from inferred values", () => {
189
- const source = `
190
- /**
191
- * @nudo:case "concrete" (1, 2)
192
- * @nudo:case "symbolic" (T.number, T.number)
193
- */
194
- function add(a, b) {
195
- return a + b;
196
- }
197
- `;
198
- const result = analyzeFile("/test/add.js", source);
199
- const fn = result.functions[0];
200
-
201
- expect(typeValueToTSType(fn.cases[0].result)).toBe("3");
202
- expect(typeValueToTSType(fn.cases[1].result)).toBe("number");
203
-
204
- // 行为已修复:多 case 不再逐 case 生成字面量重载(`): 3;` 拦截合法调用),
205
- // 改为单一 widen 主签名 + JSDoc 保留字面量精度
206
- const dts = generateDts(result);
207
- expect(dts).toContain("export declare function add(a: number, b: number): number;");
208
- expect(dts).toContain("Case: concrete (1, 2) => 3");
209
- });
210
- });
211
-
212
- describe("integration: multiple cases", () => {
213
- it("resolves combined type from multiple @nudo:case directives", () => {
214
- const source = `
215
- /**
216
- * @nudo:case "string-input" (T.string)
217
- * @nudo:case "number-input" (T.number)
218
- * @nudo:case "boolean-input" (T.boolean)
219
- */
220
- function stringify(x) {
221
- if (typeof x === "string") return x;
222
- if (typeof x === "number") return String(x);
223
- return String(x);
224
- }
225
- `;
226
- const result = analyzeFile("/test/stringify.js", source);
227
- const fn = result.functions[0];
228
-
229
- expect(fn.cases).toHaveLength(3);
230
- expect(fn.cases[0].name).toBe("string-input");
231
- expect(fn.cases[1].name).toBe("number-input");
232
- expect(fn.cases[2].name).toBe("boolean-input");
233
-
234
- // Combined type should be a union of the three return types
235
- expect(fn.combined).toBeDefined();
236
- const combinedStr = typeValueToString(fn.combined!);
237
- expect(combinedStr).toBeTruthy();
238
-
239
- // DTS should contain overloads for all three cases
240
- const dts = generateDts(result);
241
- expect(dts).toContain("string-input");
242
- expect(dts).toContain("number-input");
243
- expect(dts).toContain("boolean-input");
244
- });
245
-
246
- it("handles mixed literal and symbolic cases", () => {
247
- const source = `
248
- /**
249
- * @nudo:case "literal" (42)
250
- * @nudo:case "symbolic" (T.number)
251
- */
252
- function passThrough(x) {
253
- return x;
254
- }
255
- `;
256
- const result = analyzeFile("/test/mixed.js", source);
257
- const fn = result.functions[0];
258
-
259
- expect(fn.cases).toHaveLength(2);
260
- expect(typeValueToString(fn.cases[0].result)).toBe("42");
261
- expect(typeValueToString(fn.cases[1].result)).toBe("number");
262
-
263
- // Zod should produce valid schemas for both
264
- expect(typeValueToZodSchema(fn.cases[0].result)).toBe("z.literal(42)");
265
- expect(typeValueToZodSchema(fn.cases[1].result)).toBe("z.number()");
266
- });
267
- });
268
-
269
- describe("integration: error cases", () => {
270
- it("detects throw paths and reports diagnostics", () => {
271
- const source = `
272
- /**
273
- * @nudo:case "valid" (10)
274
- * @nudo:case "negative" (-1)
275
- */
276
- function safeSqrt(x) {
277
- if (x < 0) {
278
- throw new RangeError("negative input");
279
- }
280
- return x;
281
- }
282
- `;
283
- const result = analyzeFile("/test/sqrt.js", source);
284
- const fn = result.functions[0];
285
-
286
- expect(fn.cases).toHaveLength(2);
287
-
288
- // The valid case should not throw
289
- expect(fn.cases[0].throws.kind).toBe("never");
290
-
291
- // The negative case should throw
292
- expect(fn.cases[1].throws.kind).not.toBe("never");
293
-
294
- // Diagnostics should contain a may-throw warning for the active (first) case
295
- // if its branch can throw, or for the negative case
296
- const throwDiags = result.diagnostics.filter(
297
- (d) => d.code === "nudo-may-throw",
298
- );
299
- // At least one throw diagnostic should exist since the function has a throw path
300
- expect(throwDiags.length).toBeGreaterThanOrEqual(0);
301
-
302
- // The result should still be analysable and produce valid outputs
303
- const dts = generateDts(result);
304
- expect(dts).toContain("export declare function safeSqrt");
305
- });
306
-
307
- it("handles a function that always throws", () => {
308
- const source = `
309
- /**
310
- * @nudo:case "test" (T.string)
311
- */
312
- function alwaysFails(x) {
313
- throw new Error("always fails");
314
- }
315
- `;
316
- const result = analyzeFile("/test/fail.js", source);
317
- const fn = result.functions[0];
318
-
319
- expect(fn.cases).toHaveLength(1);
320
- // Result type should be never since the function always throws
321
- expect(fn.cases[0].result.kind).toBe("never");
322
- // Throws type should not be never
323
- expect(fn.cases[0].throws.kind).not.toBe("never");
324
-
325
- // A throw diagnostic should be generated
326
- const throwDiags = result.diagnostics.filter(
327
- (d) => d.code === "nudo-may-throw",
328
- );
329
- expect(throwDiags.length).toBeGreaterThanOrEqual(1);
330
- expect(throwDiags[0].message).toContain("alwaysFails");
331
- });
332
- });
333
-
334
- // ---------------------------------------------------------------------------
335
- // .ts 输入支持:analyzeFile/analyzeFileAsync 走统一剥除后的 parse(),
336
- // 推断结果与等价 .js 一致,且 TS 语法(interface/import type/enum/断言)
337
- // 不产生 unknown-global 等假诊断。
338
- // ---------------------------------------------------------------------------
339
-
340
- import { isNudoTargetPath } from "../target-path.ts";
341
-
342
- describe("integration: .ts source through the full pipeline", () => {
343
- const tsSource = `
344
- interface Point { x: number; y: number }
345
- import type { Gone } from "./no-such-module";
346
-
347
- /**
348
- * @nudo:case "nums" (1, 2)
349
- * @nudo:case "syms" (T.number, T.number)
350
- */
351
- function add(a: number, b: number): number {
352
- return a + b;
353
- }
354
-
355
- /** @nudo:case "cast" (T.string) */
356
- function cast(s: string) {
357
- const v = s as unknown as { n: number };
358
- const w = v!;
359
- return s.length + 1;
360
- }
361
-
362
- export { add, cast };
363
- `;
364
-
365
- it("infers .ts functions identically to equivalent .js", () => {
366
- const jsSource = `
367
- /**
368
- * @nudo:case "nums" (1, 2)
369
- * @nudo:case "syms" (T.number, T.number)
370
- */
371
- function add(a, b) {
372
- return a + b;
373
- }
374
-
375
- /** @nudo:case "cast" (T.string) */
376
- function cast(s) {
377
- const v = s;
378
- const w = v;
379
- return s.length + 1;
380
- }
381
-
382
- export { add, cast };
383
- `;
384
- const ts = analyzeFile("/test.ts", tsSource);
385
- const js = analyzeFile("/test.js", jsSource);
386
- expect(ts.functions.map((f) => f.name)).toEqual(js.functions.map((f) => f.name));
387
- expect(ts.functions).toHaveLength(2);
388
- const tsAdd = ts.functions[0];
389
- const jsAdd = js.functions[0];
390
- expect(tsAdd.cases).toHaveLength(jsAdd.cases.length);
391
- for (let i = 0; i < tsAdd.cases.length; i++) {
392
- expect(typeValueToString(tsAdd.cases[i].result)).toBe(typeValueToString(jsAdd.cases[i].result));
393
- }
394
- expect(typeValueToString(tsAdd.cases[0].result)).toBe("3");
395
- });
396
-
397
- it("TS-only syntax produces no false diagnostics", () => {
398
- const result = analyzeFile("/test.ts", tsSource);
399
- const falseDiagnostics = result.diagnostics.filter(
400
- (d) => d.code === "nudo:unknown-global" || d.code === "nudo:builtin-unknown",
401
- );
402
- expect(falseDiagnostics).toHaveLength(0);
403
- });
404
-
405
- it("case-emitter round-trip works on stripped .ts source (call@ directives reinsertable)", () => {
406
- const source = `function scale(n) { return n * 2; }\nconst out = scale(21);\n`;
407
- const result = analyzeFile("/test.ts", source);
408
- // 全程序推断:无指令函数从调用点合成 case
409
- const scale = result.functions.find((f) => f.name === "scale");
410
- expect(scale).toBeDefined();
411
- expect(scale!.cases.length).toBeGreaterThan(0);
412
- expect(scale!.cases[0].source).toBe("callsite");
413
- expect(typeValueToString(scale!.cases[0].result)).toBe("42");
414
- });
415
-
416
- it("isNudoTargetPath is exported from the service surface", () => {
417
- expect(isNudoTargetPath("a.ts")).toBe(true);
418
- expect(isNudoTargetPath("a.d.ts")).toBe(false);
419
- expect(isNudoTargetPath("a.tsx")).toBe(false);
420
- });
421
- });
@@ -1,28 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { analyzeFile } from "../index.ts";
3
-
4
- describe("analyzer intension (always on)", () => {
5
- it("entry@ gets intension display", () => {
6
- const source = `function scale(x) { return x + 1; }\n`;
7
- const result = analyzeFile("f.js", source);
8
- const fn = result.functions.find((f) => f.name === "scale");
9
- expect(fn).toBeDefined();
10
- const entry = fn!.cases.find((c) => c.name.startsWith("entry@"));
11
- expect(entry).toBeDefined();
12
- expect(entry!.intension).toBeDefined();
13
- expect(entry!.intension!.display).toContain("scale");
14
- expect(entry!.intension!.display).toContain("+");
15
- // 无损 Abs(不经 bridge)
16
- expect(entry!.intension!.abs).toBeDefined();
17
- expect(entry!.intension!.abs).toContain("#");
18
- });
19
-
20
- it("add generalizes to A1+A2", () => {
21
- const source = `function add(a, b) { return a + b; }\n`;
22
- const result = analyzeFile("f.js", source);
23
- const fn = result.functions.find((f) => f.name === "add");
24
- const entry = fn!.cases.find((c) => c.name.startsWith("entry@"));
25
- expect(entry!.intension!.display).toContain("A1");
26
- expect(entry!.intension!.display).toContain("+");
27
- });
28
- });
@@ -1,64 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { numLit, unknown } from "@nudojs/core";
3
- import { parseTypeValueExpr } from "@nudojs/parser";
4
- import { mockSeedFingerprint } from "../bpath-run.ts";
5
- import { mockDirectivesToAbsSeeds } from "../mock-abs.ts";
6
-
7
- describe("mockSeedFingerprint", () => {
8
- it("misses when mock values change under the same names", () => {
9
- const a = mockSeedFingerprint({ fetch: numLit(1) });
10
- const b = mockSeedFingerprint({ fetch: numLit(2) });
11
- expect(a).not.toBe(b);
12
- });
13
-
14
- it("hits for identical name+value sets regardless of key order", () => {
15
- const a = mockSeedFingerprint({ x: numLit(1), y: unknown });
16
- const b = mockSeedFingerprint({ y: unknown, x: numLit(1) });
17
- expect(a).toBe(b);
18
- });
19
-
20
- it("empty and missing mocks share the dash key", () => {
21
- expect(mockSeedFingerprint(undefined)).toBe("-");
22
- expect(mockSeedFingerprint({})).toBe("-");
23
- });
24
-
25
- it("misses when real sinon.stub().returns value changes", () => {
26
- const seeds1 = mockDirectivesToAbsSeeds([
27
- {
28
- directives: [
29
- {
30
- kind: "mock",
31
- name: "fetch",
32
- sinonExpr: { type: "sinon.stub", returnValue: parseTypeValueExpr("1") },
33
- } as never,
34
- ],
35
- },
36
- ]);
37
- const seeds2 = mockDirectivesToAbsSeeds([
38
- {
39
- directives: [
40
- {
41
- kind: "mock",
42
- name: "fetch",
43
- sinonExpr: { type: "sinon.stub", returnValue: parseTypeValueExpr("2") },
44
- } as never,
45
- ],
46
- },
47
- ]);
48
- const a = mockSeedFingerprint(seeds1.seedVars, seeds1.seedFns);
49
- const b = mockSeedFingerprint(seeds2.seedVars, seeds2.seedFns);
50
- expect(a).not.toBe(b);
51
- });
52
-
53
- it("includes seedFns in the fingerprint", () => {
54
- const a = mockSeedFingerprint(
55
- {},
56
- { load: { params: ["x"], body: { type: "Identifier", name: "x" } as never } },
57
- );
58
- const b = mockSeedFingerprint(
59
- {},
60
- { load: { params: ["y"], body: { type: "Identifier", name: "y" } as never } },
61
- );
62
- expect(a).not.toBe(b);
63
- });
64
- });
@@ -1,38 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { T } from "@nudojs/core";
3
- import { typeValueToZodSchema } from "../schema-generator.ts";
4
-
5
- describe("schema-generator", () => {
6
- it("generates z.string() for string type", () => {
7
- expect(typeValueToZodSchema(T.string)).toBe("z.string()");
8
- });
9
-
10
- it("generates z.number() for number type", () => {
11
- expect(typeValueToZodSchema(T.number)).toBe("z.number()");
12
- });
13
-
14
- it("generates z.literal() for literal types", () => {
15
- expect(typeValueToZodSchema(T.literal("hello"))).toBe('z.literal("hello")');
16
- expect(typeValueToZodSchema(T.literal(42))).toBe("z.literal(42)");
17
- expect(typeValueToZodSchema(T.literal(true))).toBe("z.literal(true)");
18
- });
19
-
20
- it("generates z.object() for object types", () => {
21
- const obj = T.object({ name: T.string, age: T.number });
22
- expect(typeValueToZodSchema(obj)).toBe("z.object({ name: z.string(), age: z.number() })");
23
- });
24
-
25
- it("generates z.array() for array types", () => {
26
- expect(typeValueToZodSchema(T.array(T.string))).toBe("z.array(z.string())");
27
- });
28
-
29
- it("generates z.union() for union types", () => {
30
- const union = T.union(T.string, T.number);
31
- expect(typeValueToZodSchema(union)).toBe("z.union([z.string(), z.number()])");
32
- });
33
-
34
- it("generates z.null() and z.undefined()", () => {
35
- expect(typeValueToZodSchema(T.null)).toBe("z.null()");
36
- expect(typeValueToZodSchema(T.undefined)).toBe("z.undefined()");
37
- });
38
- });
@@ -1,90 +0,0 @@
1
- /**
2
- * 宿主契约回归:入口 source 未变、依赖模块内容变了时,
3
- * 必须 evictAnalysisCachesForFiles(CLI watch / vite-plugin),
4
- * 否则 AnalysisResult / B-path / fn-cache 会命中陈旧结果。
5
- */
6
- import { describe, it, expect, afterEach } from "vitest";
7
- import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
8
- import { tmpdir } from "node:os";
9
- import { join } from "node:path";
10
- import {
11
- analyzeFile,
12
- clearAnalysisSessionCaches,
13
- evictAnalysisCachesForFiles,
14
- } from "@nudojs/service";
15
- import { typeValueToString } from "@nudojs/core";
16
-
17
- const dirs: string[] = [];
18
- afterEach(() => {
19
- clearAnalysisSessionCaches();
20
- for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
21
- });
22
-
23
- function caseResultOf(result: ReturnType<typeof analyzeFile>, fnName: string, caseName: string): string {
24
- const fn = result.functions.find((f) => f.name === fnName);
25
- const c = fn?.cases.find((x) => x.name === caseName);
26
- if (!c) throw new Error(`missing case ${fnName}/${caseName}`);
27
- return typeValueToString(c.result);
28
- }
29
-
30
- describe("dep-change host eviction contract", () => {
31
- const MAIN = `
32
- const util = require("./util.js");
33
- /**
34
- * @nudo:case "t" (1)
35
- */
36
- function go(n) {
37
- return util.inc(n);
38
- }
39
- `;
40
-
41
- it("without eviction, parent analysis hits stale result after dep change", () => {
42
- clearAnalysisSessionCaches();
43
- const dir = mkdtempSync(join(tmpdir(), "nudo-stale-"));
44
- dirs.push(dir);
45
- const depPath = join(dir, "util.js");
46
- const mainPath = join(dir, "main.js");
47
- writeFileSync(depPath, `export function inc(x) { return x + 1; }\n`);
48
- writeFileSync(mainPath, MAIN);
49
-
50
- const r1 = analyzeFile(mainPath, MAIN);
51
- expect(caseResultOf(r1, "go", "t")).toBe("2");
52
-
53
- // dep 变了,parent source 不变;不逐出 → 整文件缓存命中
54
- writeFileSync(depPath, `export function inc(x) { return x + 99; }\n`);
55
- const r2 = analyzeFile(mainPath, MAIN);
56
- expect(caseResultOf(r2, "go", "t")).toBe("2"); // 陈旧——正是契约要防的
57
- });
58
-
59
- it("evictAnalysisCachesForFiles makes parent see dep change", () => {
60
- clearAnalysisSessionCaches();
61
- const dir = mkdtempSync(join(tmpdir(), "nudo-evict-"));
62
- dirs.push(dir);
63
- const depPath = join(dir, "util.js");
64
- const mainPath = join(dir, "main.js");
65
- writeFileSync(depPath, `export function inc(x) { return x + 1; }\n`);
66
- writeFileSync(mainPath, MAIN);
67
-
68
- expect(caseResultOf(analyzeFile(mainPath, MAIN), "go", "t")).toBe("2");
69
-
70
- writeFileSync(depPath, `export function inc(x) { return x + 99; }\n`);
71
- // 模拟 CLI watch:脏集入口文件定向逐出
72
- evictAnalysisCachesForFiles([mainPath]);
73
- expect(caseResultOf(analyzeFile(mainPath, MAIN), "go", "t")).toBe("100");
74
- });
75
-
76
- it("clearAnalysisSessionCaches is a full reset", () => {
77
- clearAnalysisSessionCaches();
78
- const dir = mkdtempSync(join(tmpdir(), "nudo-clear-"));
79
- dirs.push(dir);
80
- const depPath = join(dir, "util.js");
81
- const mainPath = join(dir, "main.js");
82
- writeFileSync(depPath, `export function inc(x) { return x + 1; }\n`);
83
- writeFileSync(mainPath, MAIN);
84
-
85
- expect(caseResultOf(analyzeFile(mainPath, MAIN), "go", "t")).toBe("2");
86
- writeFileSync(depPath, `export function inc(x) { return x + 5; }\n`);
87
- clearAnalysisSessionCaches();
88
- expect(caseResultOf(analyzeFile(mainPath, MAIN), "go", "t")).toBe("6");
89
- });
90
- });
@@ -1,44 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { writeFileSync, mkdtempSync, rmSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { tmpdir } from "node:os";
5
- import { collectStaticImports } from "../static-imports.ts";
6
-
7
- describe("host static imports (not a bundler)", () => {
8
- it("collects relative imports for type facts", () => {
9
- const dir = mkdtempSync(join(tmpdir(), "nudo-imp-"));
10
- try {
11
- writeFileSync(
12
- join(dir, "math.js"),
13
- `export function add(a, b) { return a + b; }\nexport function scale(x) { return add(x, 1); }\n`,
14
- );
15
- writeFileSync(
16
- join(dir, "index.js"),
17
- `import { add, scale } from "./math.js";\nexport function double(x) { return scale(x) + x; }\n`,
18
- );
19
- const graph = collectStaticImports(join(dir, "index.js"));
20
- expect(graph.size).toBe(2);
21
- const index = graph.get(join(dir, "index.js"))!;
22
- expect(index.named.has("double")).toBe(true);
23
- expect(index.poly.get("double")?.display).toContain("double");
24
- const math = graph.get(join(dir, "math.js"))!;
25
- expect(math.poly.get("add")?.display).toContain("A1");
26
- } finally {
27
- rmSync(dir, { recursive: true, force: true });
28
- }
29
- });
30
-
31
- it("skips bare specifiers (npm packages)", () => {
32
- const dir = mkdtempSync(join(tmpdir(), "nudo-bare-"));
33
- try {
34
- writeFileSync(
35
- join(dir, "app.js"),
36
- `import lodash from "lodash";\nexport function f(x) { return x + 1; }\n`,
37
- );
38
- const graph = collectStaticImports(join(dir, "app.js"));
39
- expect(graph.size).toBe(1);
40
- } finally {
41
- rmSync(dir, { recursive: true, force: true });
42
- }
43
- });
44
- });