@nudojs/parser 0.1.0 → 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/src/directives.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Node, Comment } from "@babel/types";
2
- import { type TypeValue, T } from "@nudojs/core";
2
+ import { type TypeValue, type MockHelper, T, createEnvironment, stub, spy, mock } from "@nudojs/core";
3
+ import { parse as babelParse } from "./parse.ts";
3
4
 
4
5
  export type CaseDirective = {
5
6
  kind: "case";
@@ -14,6 +15,16 @@ export type MockDirective = {
14
15
  name: string;
15
16
  expression?: string;
16
17
  fromPath?: string;
18
+ arrowFn?: { params: string[]; body: Node; paramPatterns: Node[] };
19
+ sinonExpr?: SinonExpression;
20
+ nudoMock?: MockHelper;
21
+ };
22
+
23
+ export type SinonExpression = {
24
+ type: "stub" | "spy" | "mock";
25
+ returnValue?: TypeValue;
26
+ resolvedValue?: TypeValue;
27
+ rejectedValue?: TypeValue;
17
28
  };
18
29
 
19
30
  export type PureDirective = {
@@ -30,12 +41,34 @@ export type SampleDirective = {
30
41
  count: number;
31
42
  };
32
43
 
33
- export type ReturnsDirective = {
34
- kind: "returns";
35
- expected: TypeValue;
44
+ export type EnvDirective = {
45
+ kind: "env";
46
+ envs: string[];
47
+ };
48
+
49
+ export type MockModuleDirective = {
50
+ kind: "mock-module";
51
+ source: string;
52
+ names?: string[];
53
+ fromPath: string;
54
+ };
55
+
56
+ export type AsDirective = {
57
+ kind: "as";
58
+ typeExpr: TypeValue;
59
+ };
60
+
61
+ export type ReplaceDirective = {
62
+ kind: "replace";
63
+ targetSource: string;
64
+ typeExpr: TypeValue;
36
65
  };
37
66
 
38
- export type Directive = CaseDirective | MockDirective | PureDirective | SkipDirective | SampleDirective | ReturnsDirective;
67
+ export type InlineDirective = AsDirective | ReplaceDirective;
68
+
69
+ export type FileDirective = EnvDirective | MockModuleDirective;
70
+
71
+ export type Directive = CaseDirective | MockDirective | PureDirective | SkipDirective | SampleDirective;
39
72
 
40
73
  export type FunctionWithDirectives = {
41
74
  node: Node;
@@ -49,7 +82,6 @@ const MOCK_FROM_REGEX = /@nudo:mock\s+(\w+)\s+from\s+"([^"]+)"/g;
49
82
  const PURE_REGEX = /@nudo:pure\b/g;
50
83
  const SKIP_REGEX = /@nudo:skip(?:\s+(.+))?/g;
51
84
  const SAMPLE_REGEX = /@nudo:sample\s+(\d+)/g;
52
- const RETURNS_REGEX = /@nudo:returns\s*\(/g;
53
85
 
54
86
  export function parseTypeValueExpr(expr: string): TypeValue {
55
87
  const s = expr.trim();
@@ -111,6 +143,16 @@ export function parseTypeValueExpr(expr: string): TypeValue {
111
143
  return T.object({});
112
144
  }
113
145
 
146
+ // Function literals: (x) => expr, x => expr, (x, y) => expr, function(x) { ... }
147
+ if (findTopLevelArrow(s) !== -1 || /^function\s*[\w$]*\s*\(/.test(s)) {
148
+ const fnExpr = parseArrowFunctionExpr(s);
149
+ if (fnExpr) {
150
+ const fnType = T.fn(fnExpr.params, fnExpr.body, createEnvironment());
151
+ (fnType as any)._paramPatterns = fnExpr.paramPatterns;
152
+ return fnType;
153
+ }
154
+ }
155
+
114
156
  if (/^-?\d+(\.\d+)?$/.test(s)) return T.literal(Number(s));
115
157
 
116
158
  if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
@@ -183,6 +225,17 @@ function findTopLevelColon(s: string): number {
183
225
  return -1;
184
226
  }
185
227
 
228
+ function findTopLevelArrow(s: string): number {
229
+ let depth = 0;
230
+ for (let i = 0; i < s.length - 1; i++) {
231
+ const ch = s[i];
232
+ if (ch === "(" || ch === "[" || ch === "{") depth++;
233
+ else if (ch === ")" || ch === "]" || ch === "}") depth--;
234
+ else if (depth === 0 && ch === "=" && s[i + 1] === ">") return i;
235
+ }
236
+ return -1;
237
+ }
238
+
186
239
  function extractBalancedParens(text: string, startIdx: number): string | null {
187
240
  if (text[startIdx] !== "(") return null;
188
241
  let depth = 0;
@@ -194,6 +247,276 @@ function extractBalancedParens(text: string, startIdx: number): string | null {
194
247
  return null;
195
248
  }
196
249
 
250
+ function parseArrowFunctionExpr(expr: string): { params: string[]; body: Node; paramPatterns: Node[] } | null {
251
+ // Try to parse as an arrow function expression
252
+ try {
253
+ // Wrap in a variable declaration to make it a valid statement
254
+ const wrapped = `const __mock = ${expr};`;
255
+ const ast = babelParse(wrapped);
256
+
257
+ const decl = ast.program.body[0];
258
+ if (decl.type !== "VariableDeclaration") return null;
259
+
260
+ const init = decl.declarations[0]?.init;
261
+ if (!init || (init.type !== "ArrowFunctionExpression" && init.type !== "FunctionExpression")) {
262
+ return null;
263
+ }
264
+
265
+ const params: string[] = [];
266
+ const paramPatterns: Node[] = [];
267
+ for (let i = 0; i < init.params.length; i++) {
268
+ const param = init.params[i];
269
+ if (param.type === "Identifier") {
270
+ params.push(param.name);
271
+ paramPatterns.push(param);
272
+ } else if (param.type === "RestElement" && param.argument.type === "Identifier") {
273
+ // Handle rest parameters: (...args) => ...
274
+ params.push(`...${param.argument.name}`);
275
+ paramPatterns.push(param);
276
+ } else if (param.type === "ArrayPattern" || param.type === "ObjectPattern") {
277
+ // Handle destructuring patterns: ([a, b]) => ... or ({x, y}) => ...
278
+ params.push(`__destructured_${i}`);
279
+ paramPatterns.push(param);
280
+ }
281
+ }
282
+
283
+ return { params, body: init.body, paramPatterns };
284
+ } catch {
285
+ return null;
286
+ }
287
+ }
288
+
289
+ function parseSinonExpr(expr: string): SinonExpression | null {
290
+ const s = expr.trim();
291
+
292
+ // Match sinon.stub().returns(value)
293
+ const stubReturnsMatch = s.match(/^sinon\.stub\(\)\.returns\((.+)\)$/);
294
+ if (stubReturnsMatch) {
295
+ return {
296
+ type: "stub",
297
+ returnValue: parseTypeValueExpr(stubReturnsMatch[1].trim()),
298
+ };
299
+ }
300
+
301
+ // Match sinon.stub().resolves(value)
302
+ const stubResolvesMatch = s.match(/^sinon\.stub\(\)\.resolves\((.+)\)$/);
303
+ if (stubResolvesMatch) {
304
+ return {
305
+ type: "stub",
306
+ resolvedValue: parseTypeValueExpr(stubResolvesMatch[1].trim()),
307
+ };
308
+ }
309
+
310
+ // Match sinon.stub().rejects(value)
311
+ const stubRejectsMatch = s.match(/^sinon\.stub\(\)\.rejects\((.+)\)$/);
312
+ if (stubRejectsMatch) {
313
+ return {
314
+ type: "stub",
315
+ rejectedValue: parseTypeValueExpr(stubRejectsMatch[1].trim()),
316
+ };
317
+ }
318
+
319
+ // Match sinon.stub().onFirstCall().returns(value)
320
+ const onFirstCallReturnsMatch = s.match(/^sinon\.stub\(\)\.onFirstCall\(\)\.returns\((.+)\)$/);
321
+ if (onFirstCallReturnsMatch) {
322
+ return {
323
+ type: "stub",
324
+ returnValue: parseTypeValueExpr(onFirstCallReturnsMatch[1].trim()),
325
+ };
326
+ }
327
+
328
+ // Match sinon.stub().onSecondCall().returns(value)
329
+ const onSecondCallReturnsMatch = s.match(/^sinon\.stub\(\)\.onSecondCall\(\)\.returns\((.+)\)$/);
330
+ if (onSecondCallReturnsMatch) {
331
+ return {
332
+ type: "stub",
333
+ returnValue: parseTypeValueExpr(onSecondCallReturnsMatch[1].trim()),
334
+ };
335
+ }
336
+
337
+ // Match sinon.stub().onThirdCall().returns(value)
338
+ const onThirdCallReturnsMatch = s.match(/^sinon\.stub\(\)\.onThirdCall\(\)\.returns\((.+)\)$/);
339
+ if (onThirdCallReturnsMatch) {
340
+ return {
341
+ type: "stub",
342
+ returnValue: parseTypeValueExpr(onThirdCallReturnsMatch[1].trim()),
343
+ };
344
+ }
345
+
346
+ // Match sinon.stub().onCall(n).returns(value)
347
+ const onCallReturnsMatch = s.match(/^sinon\.stub\(\)\.onCall\(\d+\)\.returns\((.+)\)$/);
348
+ if (onCallReturnsMatch) {
349
+ return {
350
+ type: "stub",
351
+ returnValue: parseTypeValueExpr(onCallReturnsMatch[1].trim()),
352
+ };
353
+ }
354
+
355
+ // Match sinon.stub().withArgs(args).returns(value)
356
+ const withArgsReturnsMatch = s.match(/^sinon\.stub\(\)\.withArgs\(.+\)\.returns\((.+)\)$/);
357
+ if (withArgsReturnsMatch) {
358
+ return {
359
+ type: "stub",
360
+ returnValue: parseTypeValueExpr(withArgsReturnsMatch[1].trim()),
361
+ };
362
+ }
363
+
364
+ // Match sinon.stub().callsFake(fn)
365
+ const callsFakeMatch = s.match(/^sinon\.stub\(\)\.callsFake\((.+)\)$/);
366
+ if (callsFakeMatch) {
367
+ // For callsFake, we try to parse the function as an arrow function
368
+ const arrowFn = parseArrowFunctionExpr(callsFakeMatch[1].trim());
369
+ if (arrowFn) {
370
+ return {
371
+ type: "stub",
372
+ returnValue: T.fn(arrowFn.params, arrowFn.body, createEnvironment()),
373
+ };
374
+ }
375
+ return { type: "stub" };
376
+ }
377
+
378
+ // Match sinon.stub().returns(value).onFirstCall().returns(otherValue)
379
+ // This is a complex chain - we'll just use the first .returns() value
380
+ const complexChainMatch = s.match(/^sinon\.stub\(\)\.returns\((.+)\)\./);
381
+ if (complexChainMatch) {
382
+ return {
383
+ type: "stub",
384
+ returnValue: parseTypeValueExpr(complexChainMatch[1].trim()),
385
+ };
386
+ }
387
+
388
+ // Match sinon.stub() (no chaining)
389
+ if (s === "sinon.stub()") {
390
+ return { type: "stub" };
391
+ }
392
+
393
+ // Match sinon.spy()
394
+ if (s === "sinon.spy()") {
395
+ return { type: "spy" };
396
+ }
397
+
398
+ // Match sinon.mock()
399
+ if (s === "sinon.mock()") {
400
+ return { type: "mock" };
401
+ }
402
+
403
+ return null;
404
+ }
405
+
406
+ function parseNudoMockExpr(expr: string): MockHelper | null {
407
+ const s = expr.trim();
408
+
409
+ // Match stub().returns(value).onFirstCall()
410
+ const stubReturnsOnFirstMatch = s.match(/^stub\(\)\.returns\((.+)\)\.onFirstCall\(\)$/);
411
+ if (stubReturnsOnFirstMatch) {
412
+ const helper = stub.returns(parseTypeValueExpr(stubReturnsOnFirstMatch[1].trim()));
413
+ helper.onFirstCallValue = helper.returnValue;
414
+ return helper;
415
+ }
416
+
417
+ // Match stub().returns(value).onSecondCall()
418
+ const stubReturnsOnSecondMatch = s.match(/^stub\(\)\.returns\((.+)\)\.onSecondCall\(\)$/);
419
+ if (stubReturnsOnSecondMatch) {
420
+ return stub.returns(parseTypeValueExpr(stubReturnsOnSecondMatch[1].trim()));
421
+ }
422
+
423
+ // Match stub().returns(value).onCall(n)
424
+ const stubReturnsOnCallMatch = s.match(/^stub\(\)\.returns\((.+)\)\.onCall\(\d+\)$/);
425
+ if (stubReturnsOnCallMatch) {
426
+ return stub.returns(parseTypeValueExpr(stubReturnsOnCallMatch[1].trim()));
427
+ }
428
+
429
+ // Match stub().callsFake((args) => body)
430
+ const stubCallsFakeMatch = s.match(/^stub\(\)\.callsFake\((.+)\)$/);
431
+ if (stubCallsFakeMatch) {
432
+ const helper: MockHelper = { kind: "mock-helper" };
433
+ helper.callsFakeImpl = parseTypeValueExpr(stubCallsFakeMatch[1].trim());
434
+ return helper;
435
+ }
436
+
437
+ // Match stub().withArgs(args).returns(value)
438
+ const stubWithArgsMatch = s.match(/^stub\(\)\.withArgs\((.+)\)\.returns\((.+)\)$/);
439
+ if (stubWithArgsMatch) {
440
+ const retVal = parseTypeValueExpr(stubWithArgsMatch[2].trim());
441
+ const argsStr = stubWithArgsMatch[1].trim();
442
+ const args = splitTopLevelArgs(argsStr).map(parseTypeValueExpr);
443
+ // 返回值只挂在 withArgs 分支上(sinon 语义:实参匹配才返回),不设全局
444
+ // returnValue —— 否则未命中调用会错误复用链返回值
445
+ const helper: MockHelper = { kind: "mock-helper" };
446
+ helper.withArgsCases = [{ args, returnValue: retVal }];
447
+ return helper;
448
+ }
449
+
450
+ // Match stub().onFirstCall().returns(value) —— 链在 returns 上取值
451
+ const stubOnFirstReturnsMatch = s.match(/^stub\(\)\.onFirstCall\(\)\.returns\((.+)\)$/);
452
+ if (stubOnFirstReturnsMatch) {
453
+ return stub.returns(parseTypeValueExpr(stubOnFirstReturnsMatch[1].trim()));
454
+ }
455
+
456
+ // Match stub().onFirstCall(value) —— 无 returnValue 时 TypeValue/Abs 均作默认返回
457
+ // [^()]* 避免把 onFirstCall().returns(...) 的尾链吞进实参
458
+ const stubOnFirstValueMatch = s.match(/^stub\(\)\.onFirstCall\(([^()]*)\)$/);
459
+ if (stubOnFirstValueMatch && stubOnFirstValueMatch[1].trim() !== "") {
460
+ const helper: MockHelper = { kind: "mock-helper" };
461
+ helper.onFirstCallValue = parseTypeValueExpr(stubOnFirstValueMatch[1].trim());
462
+ return helper;
463
+ }
464
+
465
+ // Match spy().returns(value)
466
+ const spyReturnsMatch = s.match(/^spy\(\)\.returns\((.+)\)$/);
467
+ if (spyReturnsMatch) {
468
+ return spy.returns(parseTypeValueExpr(spyReturnsMatch[1].trim()));
469
+ }
470
+
471
+ // Match stub().resolves(value).onFirstCall()
472
+ const stubResolvesOnFirstMatch = s.match(/^stub\(\)\.resolves\((.+)\)\.onFirstCall\(\)$/);
473
+ if (stubResolvesOnFirstMatch) {
474
+ return stub.resolves(parseTypeValueExpr(stubResolvesOnFirstMatch[1].trim()));
475
+ }
476
+
477
+ // Match stub().returns(value)
478
+ const stubReturnsMatch = s.match(/^stub\(\)\.returns\((.+)\)$/);
479
+ if (stubReturnsMatch) {
480
+ return stub.returns(parseTypeValueExpr(stubReturnsMatch[1].trim()));
481
+ }
482
+
483
+ // Match stub().resolves(value)
484
+ const stubResolvesMatch = s.match(/^stub\(\)\.resolves\((.+)\)$/);
485
+ if (stubResolvesMatch) {
486
+ return stub.resolves(parseTypeValueExpr(stubResolvesMatch[1].trim()));
487
+ }
488
+
489
+ // Match stub().rejects(value)
490
+ const stubRejectsMatch = s.match(/^stub\(\)\.rejects\((.+)\)$/);
491
+ if (stubRejectsMatch) {
492
+ return stub.rejects(parseTypeValueExpr(stubRejectsMatch[1].trim()));
493
+ }
494
+
495
+ // Match stub()
496
+ if (s === "stub()") {
497
+ return stub();
498
+ }
499
+
500
+ // Match spy()
501
+ if (s === "spy()") {
502
+ return spy();
503
+ }
504
+
505
+ // Match mock()
506
+ if (s === "mock()") {
507
+ return mock();
508
+ }
509
+
510
+ // sinon 前缀链统一为同一 MockHelper 形态:strip 前缀后复用裸 stub 解析,
511
+ // 使下游(applyMocks → mockHelperToTypeValue)两条路径消费一致。
512
+ // 解析不出的 sinon 链仍回落 sinonExpr 路径(parseSinonExpr 自己的分支)。
513
+ if (s.startsWith("sinon.")) {
514
+ return parseNudoMockExpr(s.slice("sinon.".length));
515
+ }
516
+
517
+ return null;
518
+ }
519
+
197
520
  function parseDirectivesFromComments(comments: readonly Comment[]): Directive[] {
198
521
  const directives: Directive[] = [];
199
522
  for (const comment of comments) {
@@ -218,10 +541,29 @@ function parseDirectivesFromComments(comments: readonly Comment[]): Directive[]
218
541
  ([s, e]) => mockMatch!.index >= s && mockMatch!.index < e,
219
542
  );
220
543
  if (inFromRange) continue;
544
+
545
+ const expr = mockMatch[2].trim();
546
+ let arrowFn = parseArrowFunctionExpr(expr);
547
+ const sinonExpr = parseSinonExpr(expr);
548
+ const nudoMock = parseNudoMockExpr(expr);
549
+
550
+ // callsFake 统一:sinon.stub().callsFake(fn) / stub().callsFake(fn) 都把
551
+ // fn 提升到 arrowFn 字段——与 @nudo:mock f = (x) => ... 走完全同一绑定机制
552
+ // (applyMocks 的 T.fn + _paramPatterns + 全局 env 闭包),调用时以实参求值
553
+ if (!arrowFn) {
554
+ const callsFakeChain = expr.match(/^(?:sinon\.)?stub\(\)\.callsFake\((.+)\)$/);
555
+ if (callsFakeChain) {
556
+ arrowFn = parseArrowFunctionExpr(callsFakeChain[1].trim());
557
+ }
558
+ }
559
+
221
560
  directives.push({
222
561
  kind: "mock",
223
562
  name: mockMatch[1],
224
- expression: mockMatch[2].trim(),
563
+ expression: expr,
564
+ arrowFn: arrowFn ?? undefined,
565
+ sinonExpr: sinonExpr ?? undefined,
566
+ nudoMock: nudoMock ?? undefined,
225
567
  });
226
568
  }
227
569
 
@@ -266,21 +608,15 @@ function parseDirectivesFromComments(comments: readonly Comment[]): Directive[]
266
608
  while ((sampleMatch = SAMPLE_REGEX.exec(text)) !== null) {
267
609
  directives.push({ kind: "sample", count: Number(sampleMatch[1]) });
268
610
  }
269
-
270
- RETURNS_REGEX.lastIndex = 0;
271
- let returnsMatch: RegExpExecArray | null;
272
- while ((returnsMatch = RETURNS_REGEX.exec(text)) !== null) {
273
- const parenStart = returnsMatch.index + returnsMatch[0].length - 1;
274
- const argsStr = extractBalancedParens(text, parenStart);
275
- if (argsStr === null) continue;
276
- directives.push({ kind: "returns", expected: parseTypeValueExpr(argsStr) });
277
- }
278
611
  }
279
612
  return directives;
280
613
  }
281
614
 
282
615
  function getFunctionName(node: Node): string {
283
616
  if (node.type === "FunctionDeclaration" && node.id) return node.id.name;
617
+ if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "FunctionDeclaration" && node.declaration.id) {
618
+ return node.declaration.id.name;
619
+ }
284
620
  if (node.type === "ExportDefaultDeclaration" && node.declaration.type === "FunctionDeclaration" && node.declaration.id) {
285
621
  return node.declaration.id.name;
286
622
  }
@@ -310,3 +646,109 @@ export function extractDirectives(ast: Node): FunctionWithDirectives[] {
310
646
 
311
647
  return results;
312
648
  }
649
+
650
+ const AS_REGEX = /^\s*@nudo:as\s+(.+)/;
651
+ const REPLACE_REGEX = /^\s*@nudo:replace\s+(.+)/;
652
+
653
+ export function extractInlineDirectives(node: Node): InlineDirective[] {
654
+ const comments = (node as any).leadingComments as Comment[] | undefined;
655
+ if (!comments) return [];
656
+
657
+ const results: InlineDirective[] = [];
658
+ for (const comment of comments) {
659
+ if (comment.type !== "CommentLine") continue;
660
+ const text = comment.value;
661
+
662
+ const asMatch = text.match(AS_REGEX);
663
+ if (asMatch) {
664
+ results.push({ kind: "as", typeExpr: parseTypeValueExpr(asMatch[1].trim()) });
665
+ continue;
666
+ }
667
+
668
+ const replaceMatch = text.match(REPLACE_REGEX);
669
+ if (replaceMatch) {
670
+ const raw = replaceMatch[1].trim();
671
+ const sepIdx = findReplaceSeparator(raw);
672
+ if (sepIdx !== -1) {
673
+ const targetSource = raw.slice(0, sepIdx).trim();
674
+ const typeExprStr = raw.slice(sepIdx + 1).trim();
675
+ results.push({
676
+ kind: "replace",
677
+ targetSource,
678
+ typeExpr: parseTypeValueExpr(typeExprStr),
679
+ });
680
+ }
681
+ }
682
+ }
683
+ return results;
684
+ }
685
+
686
+ function findReplaceSeparator(raw: string): number {
687
+ let depth = 0;
688
+ let inString: string | null = null;
689
+ for (let i = 0; i < raw.length; i++) {
690
+ const ch = raw[i];
691
+ if (inString) {
692
+ if (ch === inString && raw[i - 1] !== "\\") inString = null;
693
+ continue;
694
+ }
695
+ if (ch === '"' || ch === "'") { inString = ch; continue; }
696
+ if (ch === "(" || ch === "[" || ch === "{") { depth++; continue; }
697
+ if (ch === ")" || ch === "]" || ch === "}") { depth--; continue; }
698
+ if (ch === " " && depth === 0) {
699
+ const rest = raw.slice(i + 1).trimStart();
700
+ if (rest.startsWith("T.") || rest.startsWith("{") || rest.startsWith("[") ||
701
+ rest.startsWith('"') || rest.startsWith("'") ||
702
+ /^-?\d/.test(rest) || rest === "true" || rest === "false" ||
703
+ rest === "null" || rest === "undefined") {
704
+ return i;
705
+ }
706
+ }
707
+ }
708
+ return -1;
709
+ }
710
+
711
+ const ENV_REGEX = /^\/\s*@nudo:env\s+(.+)/;
712
+ const MOCK_MODULE_REGEX = /^\/\s*@nudo:mock-module\s+"([^"]+)"\s+from\s+"([^"]+)"/;
713
+ const MOCK_MODULE_PARTIAL_REGEX = /^\/\s*@nudo:mock-module\s+"([^"]+)"\s*\{([^}]+)\}\s*from\s+"([^"]+)"/;
714
+
715
+ export function extractFileDirectives(ast: Node): FileDirective[] {
716
+ const results: FileDirective[] = [];
717
+ if (ast.type !== "File") return results;
718
+
719
+ const comments: readonly Comment[] = (ast as any).comments ?? [];
720
+ for (const comment of comments) {
721
+ if (comment.type !== "CommentLine") continue;
722
+ const text = comment.value;
723
+
724
+ const partialMatch = text.match(MOCK_MODULE_PARTIAL_REGEX);
725
+ if (partialMatch) {
726
+ const names = partialMatch[2].split(",").map((n) => n.trim()).filter(Boolean);
727
+ results.push({
728
+ kind: "mock-module",
729
+ source: partialMatch[1],
730
+ names,
731
+ fromPath: partialMatch[3],
732
+ });
733
+ continue;
734
+ }
735
+
736
+ const mockModuleMatch = text.match(MOCK_MODULE_REGEX);
737
+ if (mockModuleMatch) {
738
+ results.push({
739
+ kind: "mock-module",
740
+ source: mockModuleMatch[1],
741
+ fromPath: mockModuleMatch[2],
742
+ });
743
+ continue;
744
+ }
745
+
746
+ const envMatch = text.match(ENV_REGEX);
747
+ if (envMatch) {
748
+ const envs = envMatch[1].split(",").map((e) => e.trim()).filter(Boolean);
749
+ results.push({ kind: "env", envs });
750
+ }
751
+ }
752
+
753
+ return results;
754
+ }
package/src/index.ts CHANGED
@@ -1,13 +1,23 @@
1
1
  export { parse } from "./parse.ts";
2
+ // stripTypes 单源在 core(parser 依赖 core,避免双份拷贝漂移)
3
+ export { stripTypes } from "@nudojs/core";
2
4
  export {
3
5
  type Directive,
6
+ type FileDirective,
7
+ type InlineDirective,
8
+ type AsDirective,
9
+ type ReplaceDirective,
4
10
  type CaseDirective,
5
11
  type MockDirective,
6
12
  type PureDirective,
7
13
  type SkipDirective,
8
14
  type SampleDirective,
9
- type ReturnsDirective,
15
+ type EnvDirective,
16
+ type MockModuleDirective,
10
17
  type FunctionWithDirectives,
18
+ type SinonExpression,
11
19
  extractDirectives,
20
+ extractFileDirectives,
21
+ extractInlineDirectives,
12
22
  parseTypeValueExpr,
13
23
  } from "./directives.ts";
package/src/parse.ts CHANGED
@@ -1,10 +1,15 @@
1
- import { parse as babelParse } from "@babel/parser";
1
+ import { parseSource } from "@nudojs/core";
2
2
  import type { File } from "@babel/types";
3
3
 
4
- export function parse(source: string): File {
5
- return babelParse(source, {
6
- sourceType: "module",
7
- plugins: ["typescript", "jsx"],
8
- attachComment: true,
9
- });
4
+ /**
5
+ * 与 core `parseSource` 同一实现与 AST LRU:
6
+ * 决策:parse() 统一做 TS 剥除(不按 .ts 路径开关)。理由:parse() 的产物被
7
+ * CLI/service/LSP/evaluator 全链消费,按调用方 opt-in 会漏掉无法感知文件扩展名
8
+ * 的消费方(analyzer 的模块解析、LSP 等);对纯 JS 源码剥除是结构性 no-op,
9
+ * 默认行为不变,因此无条件执行可让 .ts 输入在所有入口零接线生效。
10
+ *
11
+ * errorRecovery 走 core 的非缓存路径,避免污染会话 AST。
12
+ */
13
+ export function parse(source: string, opts?: { errorRecovery?: boolean }): File {
14
+ return parseSource(source, opts);
10
15
  }