@nudojs/cli 0.2.0 → 0.2.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 (60) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +34 -0
  3. package/dist/chunk-3HCPXPEW.js +5286 -0
  4. package/dist/evaluator-api.d.ts +94 -2
  5. package/dist/evaluator-api.js +187 -3
  6. package/dist/index.js +595 -130
  7. package/examples/real-world.js +66 -0
  8. package/examples/validate-real-world.ts +160 -0
  9. package/package.json +17 -2
  10. package/src/__tests__/builtin-advanced.test.ts +292 -0
  11. package/src/__tests__/builtin-api.test.ts +308 -0
  12. package/src/__tests__/builtin-functions.test.ts +313 -0
  13. package/src/__tests__/builtin-map.test.ts +129 -0
  14. package/src/__tests__/builtin-promise.test.ts +150 -0
  15. package/src/__tests__/builtin-regexp.test.ts +133 -0
  16. package/src/__tests__/builtin-set.test.ts +163 -0
  17. package/src/__tests__/builtin-url.test.ts +256 -0
  18. package/src/__tests__/builtin-web.test.ts +340 -0
  19. package/src/__tests__/combination-scenarios.test.ts +384 -0
  20. package/src/__tests__/edge-cases.test.ts +749 -0
  21. package/src/__tests__/evaluator-loops.test.ts +120 -0
  22. package/src/__tests__/evaluator-nullish-coalescing.test.ts +86 -0
  23. package/src/__tests__/evaluator-optional-chaining.test.ts +37 -0
  24. package/src/__tests__/evaluator-phase3.test.ts +353 -2
  25. package/src/__tests__/evaluator.test.ts +702 -2
  26. package/src/__tests__/mock-arrow-function.test.ts +169 -0
  27. package/src/__tests__/mock-comparison.test.ts +221 -0
  28. package/src/__tests__/mock-comprehensive.test.ts +250 -0
  29. package/src/__tests__/mock-final-integration.test.ts +263 -0
  30. package/src/__tests__/mock-helpers.test.ts +174 -0
  31. package/src/__tests__/mock-realworld.test.ts +191 -0
  32. package/src/__tests__/mock-rest-debug.test.ts +110 -0
  33. package/src/__tests__/mock-rest-edge.test.ts +176 -0
  34. package/src/__tests__/mock-sinon-runtime.test.ts +168 -0
  35. package/src/__tests__/narrowing-discriminated.test.ts +35 -0
  36. package/src/__tests__/narrowing-in-operator.test.ts +56 -0
  37. package/src/__tests__/narrowing-switch.test.ts +56 -0
  38. package/src/__tests__/narrowing-truthiness.test.ts +52 -0
  39. package/src/__tests__/nudo-mock-syntax.test.ts +245 -0
  40. package/src/__tests__/realworld-patterns.test.ts +430 -0
  41. package/src/__tests__/syntax-sugar.test.ts +345 -0
  42. package/src/builtins/builtin-intl.ts +17 -0
  43. package/src/builtins/builtin-map.ts +174 -0
  44. package/src/builtins/builtin-promise.ts +116 -0
  45. package/src/builtins/builtin-reflect.ts +30 -0
  46. package/src/builtins/builtin-regexp.ts +34 -0
  47. package/src/builtins/builtin-set.ts +131 -0
  48. package/src/builtins/builtin-symbol.ts +16 -0
  49. package/src/builtins/builtin-url.ts +58 -0
  50. package/src/builtins/builtin-weak.ts +22 -0
  51. package/src/builtins/builtin-web.ts +111 -0
  52. package/src/builtins/index.ts +61 -0
  53. package/src/config.ts +32 -0
  54. package/src/env-loader.ts +174 -0
  55. package/src/evaluator-api.ts +20 -0
  56. package/src/evaluator.ts +4018 -342
  57. package/src/index.ts +776 -137
  58. package/src/narrowing.ts +230 -15
  59. package/src/resolve-npm.ts +63 -0
  60. package/dist/chunk-LGWVGEMB.js +0 -1817
package/src/narrowing.ts CHANGED
@@ -19,6 +19,20 @@ export function narrow(
19
19
  test: Node,
20
20
  env: Environment,
21
21
  ): [trueEnv: Environment, falseEnv: Environment] {
22
+ // a || b:fallthrough(both-false)是链式交集 —— narrow(a) 的 false
23
+ // env 再 narrow(b) 的 false。这是 `if (typeof x !== 'object' || x === null)
24
+ // return x;` 守卫模式的关键:后续代码里 x 只剩 object 成员。true 侧
25
+ // 环境无法并集合并,保守退回未 narrow 的 env。
26
+ // a && b 对称:true 侧链式(both-true),false 侧保守。
27
+ if (test.type === "LogicalExpression" && (test.operator === "||" || test.operator === "&&")) {
28
+ const [trueOfLeft, falseOfLeft] = narrow(test.left, env);
29
+ const [, falseOfBoth] = narrow(test.right, falseOfLeft);
30
+ const [trueOfBoth] = narrow(test.right, trueOfLeft);
31
+ return test.operator === "||"
32
+ ? [env, falseOfBoth]
33
+ : [trueOfBoth, env];
34
+ }
35
+
22
36
  // typeof x === "string"
23
37
  if (
24
38
  test.type === "BinaryExpression" &&
@@ -26,9 +40,9 @@ export function narrow(
26
40
  isTypeofExpr(test.left) &&
27
41
  test.right.type === "StringLiteral"
28
42
  ) {
29
- const varName = getTypeofTarget(test.left);
30
- if (varName) {
31
- return narrowByTypeof(varName, test.right.value, env);
43
+ const target = getTypeofTarget(test.left);
44
+ if (target) {
45
+ return target ? narrowByTypeof(target, test.right.value, env) : [env, env];
32
46
  }
33
47
  }
34
48
 
@@ -39,9 +53,9 @@ export function narrow(
39
53
  test.left.type === "StringLiteral" &&
40
54
  isTypeofExpr(test.right)
41
55
  ) {
42
- const varName = getTypeofTarget(test.right);
43
- if (varName) {
44
- return narrowByTypeof(varName, test.left.value, env);
56
+ const target = getTypeofTarget(test.right);
57
+ if (target) {
58
+ return target ? narrowByTypeof(target, test.left.value, env) : [env, env];
45
59
  }
46
60
  }
47
61
 
@@ -52,9 +66,9 @@ export function narrow(
52
66
  isTypeofExpr(test.left) &&
53
67
  test.right.type === "StringLiteral"
54
68
  ) {
55
- const varName = getTypeofTarget(test.left);
56
- if (varName) {
57
- const [trueEnv, falseEnv] = narrowByTypeof(varName, test.right.value, env);
69
+ const target = getTypeofTarget(test.left);
70
+ if (target) {
71
+ const [trueEnv, falseEnv] = target ? narrowByTypeof(target, test.right.value, env) : [env, env];
58
72
  return [falseEnv, trueEnv];
59
73
  }
60
74
  }
@@ -66,13 +80,50 @@ export function narrow(
66
80
  test.left.type === "StringLiteral" &&
67
81
  isTypeofExpr(test.right)
68
82
  ) {
69
- const varName = getTypeofTarget(test.right);
70
- if (varName) {
71
- const [trueEnv, falseEnv] = narrowByTypeof(varName, test.left.value, env);
83
+ const target = getTypeofTarget(test.right);
84
+ if (target) {
85
+ const [trueEnv, falseEnv] = narrowByTypeof(target, test.left.value, env);
72
86
  return [falseEnv, trueEnv];
73
87
  }
74
88
  }
75
89
 
90
+ // obj.prop === literal (discriminated union)
91
+ if (
92
+ test.type === "BinaryExpression" &&
93
+ test.operator === "===" &&
94
+ test.left.type === "MemberExpression" &&
95
+ test.left.object.type === "Identifier" &&
96
+ test.left.property.type === "Identifier" &&
97
+ isLiteralNode(test.right)
98
+ ) {
99
+ return narrowByDiscriminant(test.left.object.name, test.left.property.name, getLiteralValue(test.right), env);
100
+ }
101
+
102
+ // literal === obj.prop
103
+ if (
104
+ test.type === "BinaryExpression" &&
105
+ test.operator === "===" &&
106
+ isLiteralNode(test.left) &&
107
+ test.right.type === "MemberExpression" &&
108
+ test.right.object.type === "Identifier" &&
109
+ test.right.property.type === "Identifier"
110
+ ) {
111
+ return narrowByDiscriminant(test.right.object.name, test.right.property.name, getLiteralValue(test.left), env);
112
+ }
113
+
114
+ // obj.prop !== literal
115
+ if (
116
+ test.type === "BinaryExpression" &&
117
+ test.operator === "!==" &&
118
+ test.left.type === "MemberExpression" &&
119
+ test.left.object.type === "Identifier" &&
120
+ test.left.property.type === "Identifier" &&
121
+ isLiteralNode(test.right)
122
+ ) {
123
+ const [trueEnv, falseEnv] = narrowByDiscriminant(test.left.object.name, test.left.property.name, getLiteralValue(test.right), env);
124
+ return [falseEnv, trueEnv];
125
+ }
126
+
76
127
  // x === literal
77
128
  if (
78
129
  test.type === "BinaryExpression" &&
@@ -152,12 +203,41 @@ export function narrow(
152
203
  }
153
204
  }
154
205
 
206
+ // "key" in obj
207
+ if (
208
+ test.type === "BinaryExpression" &&
209
+ test.operator === "in" &&
210
+ test.left.type === "StringLiteral" &&
211
+ test.right.type === "Identifier"
212
+ ) {
213
+ return narrowByPropertyIn(test.left.value, test.right.name, env);
214
+ }
215
+
216
+ // Array.isArray(x)
217
+ if (
218
+ test.type === "CallExpression" &&
219
+ test.callee.type === "MemberExpression" &&
220
+ test.callee.object.type === "Identifier" &&
221
+ test.callee.object.name === "Array" &&
222
+ test.callee.property.type === "Identifier" &&
223
+ test.callee.property.name === "isArray" &&
224
+ test.arguments.length === 1 &&
225
+ test.arguments[0].type === "Identifier"
226
+ ) {
227
+ return narrowByIsArray(test.arguments[0].name, env);
228
+ }
229
+
155
230
  // !expr (negate)
156
231
  if (test.type === "UnaryExpression" && test.operator === "!") {
157
232
  const [trueEnv, falseEnv] = narrow(test.argument, env);
158
233
  return [falseEnv, trueEnv];
159
234
  }
160
235
 
236
+ // Truthiness narrowing: if (x)
237
+ if (test.type === "Identifier") {
238
+ return narrowByTruthy(test.name, env);
239
+ }
240
+
161
241
  return [env, env];
162
242
  }
163
243
 
@@ -165,13 +245,24 @@ function isTypeofExpr(node: Node): boolean {
165
245
  return node.type === "UnaryExpression" && node.operator === "typeof";
166
246
  }
167
247
 
168
- function getTypeofTarget(node: Node): string | null {
248
+ /** typeof 目标:标识符(x)或成员访问链(ref.has / ns.fn) */
249
+ function getTypeofTarget(node: Node): { kind: "ident"; name: string } | { kind: "member"; objName: string; propName: string } | null {
169
250
  if (
170
251
  node.type === "UnaryExpression" &&
171
252
  node.operator === "typeof" &&
172
253
  node.argument.type === "Identifier"
173
254
  ) {
174
- return node.argument.name;
255
+ return { kind: "ident", name: node.argument.name };
256
+ }
257
+ if (
258
+ node.type === "UnaryExpression" &&
259
+ node.operator === "typeof" &&
260
+ node.argument.type === "MemberExpression" &&
261
+ node.argument.object.type === "Identifier" &&
262
+ !node.argument.computed &&
263
+ node.argument.property.type === "Identifier"
264
+ ) {
265
+ return { kind: "member", objName: node.argument.object.name, propName: node.argument.property.name };
175
266
  }
176
267
  return null;
177
268
  }
@@ -203,11 +294,56 @@ const typeofToPrimitive: Record<string, TypeValue> = {
203
294
  symbol: T.symbol,
204
295
  };
205
296
 
297
+ /** 内置类实例的原型方法归属(与 evaluator classMethods 表对齐的子集) */
298
+ const INSTANCE_METHOD_OWNERS: Record<string, Set<string>> = {
299
+ has: new Set(["Set", "Map", "WeakSet", "WeakMap"]),
300
+ get: new Set(["Map", "WeakMap"]),
301
+ set: new Set(["Map", "WeakMap", "Set"]),
302
+ add: new Set(["Set", "WeakSet"]),
303
+ delete: new Set(["Set", "Map", "WeakSet", "WeakMap"]),
304
+ clear: new Set(["Set", "Map"]),
305
+ forEach: new Set(["Set", "Map", "Array"]),
306
+ size: new Set(["Set", "Map"]),
307
+ keys: new Set(["Set", "Map"]),
308
+ values: new Set(["Set", "Map"]),
309
+ entries: new Set(["Set", "Map"]),
310
+ test: new Set(["RegExp"]),
311
+ exec: new Set(["RegExp"]),
312
+ };
313
+
206
314
  function narrowByTypeof(
207
- varName: string,
315
+ target: { kind: "ident"; name: string } | { kind: "member"; objName: string; propName: string },
208
316
  typeStr: string,
209
317
  env: Environment,
210
318
  ): [Environment, Environment] {
319
+ // 成员目标(typeof ref.has === 'function'):按对象成员中 prop 的类型
320
+ // 过滤 union——留下 has 为函数的成员(如 Set),剔除 tuple/{}
321
+ if (target.kind === "member") {
322
+ const current = env.lookup(target.objName);
323
+ if (!current || current.kind !== "union") return [env, env];
324
+ const propMatches = (m: TypeValue): boolean => {
325
+ let prop: TypeValue | undefined;
326
+ if (m.kind === "object") prop = m.properties[target.propName];
327
+ else if (m.kind === "instance") {
328
+ // instance 的 properties 不含原型方法:按内置类方法表判断
329
+ // (Set/Map 的 has 等——与 evaluator 的 classMethods 表保持一致)
330
+ if (INSTANCE_METHOD_OWNERS[target.propName]?.has(m.className)) return true;
331
+ prop = m.properties[target.propName];
332
+ }
333
+ if (typeStr === "function") return prop?.kind === "function" || prop?.kind === "unknown";
334
+ return true;
335
+ };
336
+ const narrowed = narrowType(current, propMatches);
337
+ const excluded = subtractType(current, propMatches);
338
+ if (narrowed.kind === "never") return [env, env];
339
+ const trueEnv = env.extend({});
340
+ trueEnv.bind(target.objName, narrowed);
341
+ const falseEnv = env.extend({});
342
+ falseEnv.bind(target.objName, excluded.kind === "never" ? current : excluded);
343
+ return [trueEnv, falseEnv];
344
+ }
345
+
346
+ const varName = target.name;
211
347
  const current = env.lookup(varName);
212
348
  const targetPrimitive = typeofToPrimitive[typeStr];
213
349
 
@@ -320,3 +456,82 @@ function narrowByComparison(
320
456
  falseEnv.bind(varName, falseRange);
321
457
  return [trueEnv, falseEnv];
322
458
  }
459
+
460
+ function narrowByPropertyIn(propName: string, varName: string, env: Environment): [Environment, Environment] {
461
+ const current = env.lookup(varName);
462
+
463
+ const narrowed = narrowType(current, (m) =>
464
+ m.kind === "object" && propName in m.properties
465
+ );
466
+ const excluded = subtractType(current, (m) =>
467
+ m.kind === "object" && propName in m.properties
468
+ );
469
+
470
+ const trueEnv = env.extend({});
471
+ trueEnv.bind(varName, narrowed.kind === "never" ? current : narrowed);
472
+ const falseEnv = env.extend({});
473
+ falseEnv.bind(varName, excluded.kind === "never" ? current : excluded);
474
+ return [trueEnv, falseEnv];
475
+ }
476
+
477
+ function narrowByIsArray(varName: string, env: Environment): [Environment, Environment] {
478
+ const current = env.lookup(varName);
479
+
480
+ const narrowed = narrowType(current, (m) => m.kind === "array" || m.kind === "tuple");
481
+ const excluded = subtractType(current, (m) => m.kind === "array" || m.kind === "tuple");
482
+
483
+ const trueEnv = env.extend({});
484
+ trueEnv.bind(varName, narrowed.kind === "never" ? T.array(T.unknown) : narrowed);
485
+ const falseEnv = env.extend({});
486
+ falseEnv.bind(varName, excluded.kind === "never" ? current : excluded);
487
+ return [trueEnv, falseEnv];
488
+ }
489
+
490
+ function narrowByDiscriminant(
491
+ objName: string,
492
+ propName: string,
493
+ literalValue: TypeValue,
494
+ env: Environment,
495
+ ): [Environment, Environment] {
496
+ const current = env.lookup(objName);
497
+
498
+ const narrowed = narrowType(current, (m) => {
499
+ if (m.kind !== "object") return false;
500
+ const propType = m.properties[propName];
501
+ if (!propType) return false;
502
+ return typeValueEquals(propType, literalValue);
503
+ });
504
+
505
+ const excluded = subtractType(current, (m) => {
506
+ if (m.kind !== "object") return false;
507
+ const propType = m.properties[propName];
508
+ if (!propType) return false;
509
+ return typeValueEquals(propType, literalValue);
510
+ });
511
+
512
+ const trueEnv = env.extend({});
513
+ trueEnv.bind(objName, narrowed.kind === "never" ? current : narrowed);
514
+ const falseEnv = env.extend({});
515
+ falseEnv.bind(objName, excluded.kind === "never" ? current : excluded);
516
+ return [trueEnv, falseEnv];
517
+ }
518
+
519
+ function isFalsyType(tv: TypeValue): boolean {
520
+ if (tv.kind === "literal") {
521
+ return tv.value === null || tv.value === undefined || tv.value === false || tv.value === 0 || tv.value === "";
522
+ }
523
+ return false;
524
+ }
525
+
526
+ function narrowByTruthy(varName: string, env: Environment): [Environment, Environment] {
527
+ const current = env.lookup(varName);
528
+
529
+ const narrowed = narrowType(current, (m) => !isFalsyType(m));
530
+ const excluded = subtractType(current, (m) => !isFalsyType(m));
531
+
532
+ const trueEnv = env.extend({});
533
+ trueEnv.bind(varName, narrowed.kind === "never" ? current : narrowed);
534
+ const falseEnv = env.extend({});
535
+ falseEnv.bind(varName, excluded.kind === "never" ? current : excluded);
536
+ return [trueEnv, falseEnv];
537
+ }
@@ -0,0 +1,63 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { resolve, join, dirname } from "node:path";
3
+
4
+ function findNodeModules(startDir: string): string | null {
5
+ let dir = resolve(startDir);
6
+ const root = resolve("/");
7
+
8
+ while (dir !== root) {
9
+ const nmPath = join(dir, "node_modules");
10
+ if (existsSync(nmPath)) return nmPath;
11
+ const parent = dirname(dir);
12
+ if (parent === dir) break;
13
+ dir = parent;
14
+ }
15
+ return null;
16
+ }
17
+
18
+ function resolveExportsNudo(exports: unknown, subpath: string): string | null {
19
+ if (!exports || typeof exports !== "object") return null;
20
+
21
+ const entry = (exports as Record<string, unknown>)[subpath];
22
+ if (!entry) return null;
23
+
24
+ if (typeof entry === "object" && entry !== null && "nudo" in entry) {
25
+ const nudoEntry = (entry as Record<string, unknown>)["nudo"];
26
+ if (typeof nudoEntry === "string") return nudoEntry;
27
+ }
28
+
29
+ return null;
30
+ }
31
+
32
+ export function resolveNpmNudo(
33
+ source: string,
34
+ fromDir: string,
35
+ ): string | null {
36
+ const isRelative = source.startsWith(".") || source.startsWith("/");
37
+ if (isRelative) return null;
38
+
39
+ const parts = source.startsWith("@")
40
+ ? source.split("/").slice(0, 2)
41
+ : source.split("/").slice(0, 1);
42
+ const pkgName = parts.join("/");
43
+ const subpath = source.slice(pkgName.length) || ".";
44
+
45
+ const nodeModules = findNodeModules(fromDir);
46
+ if (!nodeModules) return null;
47
+
48
+ const pkgJsonPath = join(nodeModules, pkgName, "package.json");
49
+ if (!existsSync(pkgJsonPath)) return null;
50
+
51
+ try {
52
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
53
+ const nudoEntry = resolveExportsNudo(pkg.exports, subpath);
54
+ if (nudoEntry) {
55
+ const resolved = resolve(dirname(pkgJsonPath), nudoEntry);
56
+ if (existsSync(resolved)) return resolved;
57
+ }
58
+ } catch {
59
+ // ignore parse errors
60
+ }
61
+
62
+ return null;
63
+ }