@nudojs/parser 1.4.24 → 1.5.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/dist/index.d.ts CHANGED
@@ -170,6 +170,7 @@ type FunctionWithDirectives = {
170
170
  name: string;
171
171
  directives: Directive[];
172
172
  };
173
+
173
174
  type DirectiveDiag = {
174
175
  code: string;
175
176
  message: string;
@@ -181,12 +182,6 @@ type DirectiveDiag = {
181
182
  * 的 D1 段)。fn 内显式传 diags 的 extract 自成新去重域。
182
183
  */
183
184
  declare function runWithDirectiveDiags<T>(diags: DirectiveDiag[], fn: () => T): T;
184
- /**
185
- * case 实参 / 指令类型表达式唯一文法:约束构建器优先,其余为具体字面量、
186
- * 结构字面量与箭头函数。`T.*` 文法已物理删除。
187
- * depth 按包含关系逐层 +1 线程化传递(兄弟共享父深度,不用共享可变计数器)。
188
- */
189
- declare function parseCaseArgExpr(expr: string, depth?: number): Abs;
190
185
  /**
191
186
  * extractDirectives 显式诊断通道 opts。
192
187
  *
@@ -198,6 +193,14 @@ declare function parseCaseArgExpr(expr: string, depth?: number): Abs;
198
193
  type ExtractDirectivesOpts = {
199
194
  diags?: DirectiveDiag[];
200
195
  };
196
+
197
+ /**
198
+ * case 实参 / 指令类型表达式唯一文法:约束构建器优先,其余为具体字面量、
199
+ * 结构字面量与箭头函数。`T.*` 文法已物理删除。
200
+ * depth 按包含关系逐层 +1 线程化传递(兄弟共享父深度,不用共享可变计数器)。
201
+ */
202
+ declare function parseCaseArgExpr(expr: string, depth?: number): Abs;
203
+
201
204
  /**
202
205
  * D6=G2:指令绑定 AST 最近 Function(含 nested function、class method)。
203
206
  * 作用域清单与 refine 的 @nudo:contract 同源(core directive-scan),
@@ -210,6 +213,7 @@ declare function extractDirectives(ast: Node, opts?: ExtractDirectivesOpts): Fun
210
213
  * 需要诊断的调用方用 extractDirectives(ast, { diags })。
211
214
  */
212
215
  declare function extractDirectivesQuiet(ast: Node): FunctionWithDirectives[];
216
+
213
217
  /**
214
218
  * 行内指令(@nudo:as / @nudo:replace)抽取,源为节点的 leadingComments。
215
219
  * 诊断通道与 extractDirectives 同形:传 opts.diags 落调用方数组(单次调用
@@ -217,6 +221,7 @@ declare function extractDirectivesQuiet(ast: Node): FunctionWithDirectives[];
217
221
  * runWithDirectiveDiags 域(无则丢弃,纯查询形态)。
218
222
  */
219
223
  declare function extractInlineDirectives(node: Node, opts?: ExtractDirectivesOpts): InlineDirective[];
224
+
220
225
  /**
221
226
  * 从 AST 抽文件级指令(`@nudo:env` / `@nudo:mock-module`,只认行注释)。
222
227
  *
package/dist/index.js CHANGED
@@ -175,37 +175,7 @@ function unwrapDefaultExport(mod) {
175
175
  return mod.default;
176
176
  }
177
177
 
178
- // src/directives.ts
179
- import {
180
- stub,
181
- spy,
182
- mock,
183
- abs as makeAbs,
184
- numLit,
185
- strLit,
186
- boolLit,
187
- absFunction,
188
- joinAbs,
189
- execNudoModule,
190
- isNudoConstraint,
191
- constraintToEntryAbs,
192
- CONSTRAINT_BUILDER_NAMES,
193
- CONSTRAINT_EXPR_RE,
194
- SELF,
195
- listFnDirectiveScopes,
196
- parseEnvPayload,
197
- parseMockModulePayload,
198
- scanCaseArgSpans,
199
- scanCaseTags,
200
- extractBalancedParens
201
- } from "@nudojs/core";
202
- import { absLit } from "@nudojs/core/internal";
203
- function absExact(shape) {
204
- return makeAbs(shape, void 0, void 0, "exact");
205
- }
206
- function absUnknown() {
207
- return makeAbs({ k: "unknown" }, void 0, void 0, "partial");
208
- }
178
+ // src/directives/diag.ts
209
179
  var activeDiagSink = null;
210
180
  function runWithDirectiveDiags(diags, fn) {
211
181
  const prev = activeDiagSink;
@@ -223,15 +193,158 @@ function emitDirectiveDiag(d) {
223
193
  activeDiagSink.seen.add(key);
224
194
  activeDiagSink.list.push(d);
225
195
  }
226
- var CASE_TAG_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:case\b[ \t]*([^\n]*)/g;
227
- var JS_IDENT_RE = /^[\p{L}$_][\p{L}\p{N}$_]*$/u;
228
- var MOCK_INLINE_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:mock\b[ \t]+([^\s=]+)[ \t]*=[ \t]*(.+)/g;
229
- var MOCK_FROM_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:mock\b[ \t]+([^\s]+)[ \t]+from[ \t]+(?:"([^"\n]+)"|'([^'\n]+)')/g;
230
- var MOCK_TAG_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:mock\b[ \t]*([^\n]*)/g;
231
- var PURE_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:pure\b/g;
232
- var SKIP_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:skip\b(?:[ \t]+(\S[^\n]*))?/g;
233
- var SAMPLE_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:sample\b(?:[ \t]+(\S+))?/g;
234
- var SAMPLE_NUM_RE = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
196
+
197
+ // src/directives/case-arg.ts
198
+ import {
199
+ abs as makeAbs,
200
+ absFunction,
201
+ joinAbs,
202
+ numLit,
203
+ strLit,
204
+ boolLit,
205
+ execNudoModule,
206
+ isNudoConstraint,
207
+ constraintToEntryAbs,
208
+ CONSTRAINT_BUILDER_NAMES,
209
+ CONSTRAINT_EXPR_RE,
210
+ SELF
211
+ } from "@nudojs/core";
212
+ import { absLit, DIAG } from "@nudojs/core/internal";
213
+
214
+ // src/directives/expr-scan.ts
215
+ function scanWithStrings(s, onChar, onStructural) {
216
+ let depth = 0;
217
+ let inString = null;
218
+ for (let i = 0; i < s.length; i++) {
219
+ const ch = s[i];
220
+ if (inString) {
221
+ onChar(ch, i);
222
+ if (ch === inString) inString = null;
223
+ continue;
224
+ }
225
+ if (ch === '"' || ch === "'") {
226
+ inString = ch;
227
+ onChar(ch, i);
228
+ continue;
229
+ }
230
+ if (ch === "(" || ch === "[" || ch === "{") {
231
+ depth++;
232
+ onChar(ch, i);
233
+ onStructural(ch, i, depth);
234
+ continue;
235
+ }
236
+ if (ch === ")" || ch === "]" || ch === "}") {
237
+ depth--;
238
+ onChar(ch, i);
239
+ onStructural(ch, i, depth);
240
+ continue;
241
+ }
242
+ onChar(ch, i);
243
+ onStructural(ch, i, depth);
244
+ }
245
+ }
246
+ function splitTopLevelArgs(s) {
247
+ const result = [];
248
+ let current = "";
249
+ let depth = 0;
250
+ let inString = null;
251
+ for (let i = 0; i < s.length; i++) {
252
+ const ch = s[i];
253
+ if (inString) {
254
+ current += ch;
255
+ if (ch === inString) inString = null;
256
+ continue;
257
+ }
258
+ if (ch === '"' || ch === "'") {
259
+ inString = ch;
260
+ current += ch;
261
+ continue;
262
+ }
263
+ if (ch === "(" || ch === "[" || ch === "{") depth++;
264
+ if (ch === ")" || ch === "]" || ch === "}") depth--;
265
+ if (ch === "," && depth === 0) {
266
+ result.push(current.trim());
267
+ current = "";
268
+ continue;
269
+ }
270
+ current += ch;
271
+ }
272
+ if (current.trim()) result.push(current.trim());
273
+ return result;
274
+ }
275
+ function findTopLevelColon(s) {
276
+ let found = -1;
277
+ scanWithStrings(
278
+ s,
279
+ () => {
280
+ },
281
+ (ch, i, depth) => {
282
+ if (found === -1 && ch === ":" && depth === 0) found = i;
283
+ }
284
+ );
285
+ return found;
286
+ }
287
+ function findTopLevelArrow(s) {
288
+ let depth = 0;
289
+ let inString = null;
290
+ for (let i = 0; i < s.length - 1; i++) {
291
+ const ch = s[i];
292
+ if (inString) {
293
+ if (ch === "\\") {
294
+ i++;
295
+ continue;
296
+ }
297
+ if (ch === inString) inString = null;
298
+ continue;
299
+ }
300
+ if (ch === '"' || ch === "'") {
301
+ inString = ch;
302
+ continue;
303
+ }
304
+ if (ch === "(" || ch === "[" || ch === "{") depth++;
305
+ else if (ch === ")" || ch === "]" || ch === "}") depth--;
306
+ else if (depth === 0 && ch === "=" && s[i + 1] === ">") return i;
307
+ }
308
+ return -1;
309
+ }
310
+ function parseArrowFunctionExpr(expr) {
311
+ try {
312
+ const wrapped = `const __mock = ${expr};`;
313
+ const ast = parse(wrapped);
314
+ const decl = ast.program.body[0];
315
+ if (decl.type !== "VariableDeclaration") return null;
316
+ const init = decl.declarations[0]?.init;
317
+ if (!init || init.type !== "ArrowFunctionExpression" && init.type !== "FunctionExpression") {
318
+ return null;
319
+ }
320
+ const params = [];
321
+ const paramPatterns = [];
322
+ for (let i = 0; i < init.params.length; i++) {
323
+ const param = init.params[i];
324
+ if (param.type === "Identifier") {
325
+ params.push(param.name);
326
+ paramPatterns.push(param);
327
+ } else if (param.type === "RestElement" && param.argument.type === "Identifier") {
328
+ params.push(`...${param.argument.name}`);
329
+ paramPatterns.push(param);
330
+ } else if (param.type === "ArrayPattern" || param.type === "ObjectPattern") {
331
+ params.push(`__destructured_${i}`);
332
+ paramPatterns.push(param);
333
+ }
334
+ }
335
+ return { params, body: init.body, paramPatterns };
336
+ } catch {
337
+ return null;
338
+ }
339
+ }
340
+
341
+ // src/directives/case-arg.ts
342
+ function absExact(shape) {
343
+ return makeAbs(shape, void 0, void 0, "exact");
344
+ }
345
+ function absUnknown() {
346
+ return makeAbs({ k: "unknown" }, void 0, void 0, "partial");
347
+ }
235
348
  var TYPE_EXPR_CHAIN_METHODS = /* @__PURE__ */ new Set([
236
349
  "gt",
237
350
  "ge",
@@ -306,7 +419,7 @@ function tryParseConstraint(expr) {
306
419
  if (!CONSTRAINT_EXPR_RE.test(s)) return void 0;
307
420
  if (!isSafeTypeExprSource(s)) {
308
421
  emitDirectiveDiag({
309
- code: "nudo:directive-syntax",
422
+ code: DIAG.directiveSyntax,
310
423
  message: `Unsafe constraint expression rejected: ${s.slice(0, 80)} (only builder calls, chain methods, and literals are allowed)`
311
424
  });
312
425
  return void 0;
@@ -385,7 +498,7 @@ function constraintToCaseArgAbs(c, depth) {
385
498
  var MAX_CASE_ARG_DEPTH = 32;
386
499
  function depthCapDiag(expr) {
387
500
  emitDirectiveDiag({
388
- code: "nudo:directive-syntax",
501
+ code: DIAG.directiveSyntax,
389
502
  message: `Type expression nesting exceeds depth ${MAX_CASE_ARG_DEPTH}: ${expr.trim().slice(0, 40)} \u2026 (deeper structure collapses to unknown)`
390
503
  });
391
504
  return absUnknown();
@@ -454,131 +567,6 @@ function parseObjectLiteral(content, depth) {
454
567
  }
455
568
  return absExact({ k: "obj", slots });
456
569
  }
457
- function scanWithStrings(s, onChar, onStructural) {
458
- let depth = 0;
459
- let inString = null;
460
- for (let i = 0; i < s.length; i++) {
461
- const ch = s[i];
462
- if (inString) {
463
- onChar(ch, i);
464
- if (ch === inString) inString = null;
465
- continue;
466
- }
467
- if (ch === '"' || ch === "'") {
468
- inString = ch;
469
- onChar(ch, i);
470
- continue;
471
- }
472
- if (ch === "(" || ch === "[" || ch === "{") {
473
- depth++;
474
- onChar(ch, i);
475
- onStructural(ch, i, depth);
476
- continue;
477
- }
478
- if (ch === ")" || ch === "]" || ch === "}") {
479
- depth--;
480
- onChar(ch, i);
481
- onStructural(ch, i, depth);
482
- continue;
483
- }
484
- onChar(ch, i);
485
- onStructural(ch, i, depth);
486
- }
487
- }
488
- function splitTopLevelArgs(s) {
489
- const result = [];
490
- let current = "";
491
- let depth = 0;
492
- let inString = null;
493
- for (let i = 0; i < s.length; i++) {
494
- const ch = s[i];
495
- if (inString) {
496
- current += ch;
497
- if (ch === inString) inString = null;
498
- continue;
499
- }
500
- if (ch === '"' || ch === "'") {
501
- inString = ch;
502
- current += ch;
503
- continue;
504
- }
505
- if (ch === "(" || ch === "[" || ch === "{") depth++;
506
- if (ch === ")" || ch === "]" || ch === "}") depth--;
507
- if (ch === "," && depth === 0) {
508
- result.push(current.trim());
509
- current = "";
510
- continue;
511
- }
512
- current += ch;
513
- }
514
- if (current.trim()) result.push(current.trim());
515
- return result;
516
- }
517
- function findTopLevelColon(s) {
518
- let found = -1;
519
- scanWithStrings(
520
- s,
521
- () => {
522
- },
523
- (ch, i, depth) => {
524
- if (found === -1 && ch === ":" && depth === 0) found = i;
525
- }
526
- );
527
- return found;
528
- }
529
- function findTopLevelArrow(s) {
530
- let depth = 0;
531
- let inString = null;
532
- for (let i = 0; i < s.length - 1; i++) {
533
- const ch = s[i];
534
- if (inString) {
535
- if (ch === "\\") {
536
- i++;
537
- continue;
538
- }
539
- if (ch === inString) inString = null;
540
- continue;
541
- }
542
- if (ch === '"' || ch === "'") {
543
- inString = ch;
544
- continue;
545
- }
546
- if (ch === "(" || ch === "[" || ch === "{") depth++;
547
- else if (ch === ")" || ch === "]" || ch === "}") depth--;
548
- else if (depth === 0 && ch === "=" && s[i + 1] === ">") return i;
549
- }
550
- return -1;
551
- }
552
- function parseArrowFunctionExpr(expr) {
553
- try {
554
- const wrapped = `const __mock = ${expr};`;
555
- const ast = parse(wrapped);
556
- const decl = ast.program.body[0];
557
- if (decl.type !== "VariableDeclaration") return null;
558
- const init = decl.declarations[0]?.init;
559
- if (!init || init.type !== "ArrowFunctionExpression" && init.type !== "FunctionExpression") {
560
- return null;
561
- }
562
- const params = [];
563
- const paramPatterns = [];
564
- for (let i = 0; i < init.params.length; i++) {
565
- const param = init.params[i];
566
- if (param.type === "Identifier") {
567
- params.push(param.name);
568
- paramPatterns.push(param);
569
- } else if (param.type === "RestElement" && param.argument.type === "Identifier") {
570
- params.push(`...${param.argument.name}`);
571
- paramPatterns.push(param);
572
- } else if (param.type === "ArrayPattern" || param.type === "ObjectPattern") {
573
- params.push(`__destructured_${i}`);
574
- paramPatterns.push(param);
575
- }
576
- }
577
- return { params, body: init.body, paramPatterns };
578
- } catch {
579
- return null;
580
- }
581
- }
582
570
  function isExplicitUnknownExpr(expr) {
583
571
  const s = expr.trim();
584
572
  return s === "unknown" || s === "any" || s === "any()";
@@ -590,42 +578,24 @@ function parseAbsExprDiag(expr, context) {
590
578
  const abs = parseCaseArgExpr(expr);
591
579
  if (abs.shape.k === "unknown" && !abs.term && !isExplicitUnknownExpr(expr)) {
592
580
  emitDirectiveDiag({
593
- code: "nudo:directive-syntax",
581
+ code: DIAG.directiveSyntax,
594
582
  message: `Unrecognized type expression in ${context}: ${expr.trim()} (use constraint builders, literals, or structure syntax)`
595
583
  });
596
584
  }
597
585
  return abs;
598
586
  }
599
- function parseSkipReturnsExpr(rest) {
600
- if (rest === void 0) return void 0;
601
- const s = rest.trim();
602
- if (s === "") return void 0;
603
- if (s.startsWith("=>")) {
604
- const expr = s.slice(2).trim();
605
- if (expr === "") {
606
- emitDirectiveDiag({
607
- code: "nudo:directive-syntax",
608
- message: `@nudo:skip => requires a type expression (e.g. @nudo:skip => number())`
609
- });
610
- return void 0;
611
- }
612
- return parseAbsExprDiag(expr, "@nudo:skip =>");
613
- }
614
- if (s.startsWith("(")) {
615
- const inner = extractBalancedParens(s, 0);
616
- if (inner !== null && s.slice(inner.length + 2).trim() === "") {
617
- return parseAbsExprDiag(inner, "@nudo:skip (...)");
618
- }
619
- if (inner === null) {
620
- emitDirectiveDiag({
621
- code: "nudo:directive-syntax",
622
- message: `@nudo:skip type expression has unclosed parenthesis (expected @nudo:skip (<typeExpr>)) \u2014 got: ${s.slice(0, 60)}`
623
- });
624
- return void 0;
625
- }
626
- }
627
- return void 0;
628
- }
587
+
588
+ // src/directives/fn-directives.ts
589
+ import {
590
+ extractBalancedParens,
591
+ listFnDirectiveScopes,
592
+ scanCaseArgSpans,
593
+ scanCaseTags
594
+ } from "@nudojs/core";
595
+ import { DIAG as DIAG2 } from "@nudojs/core/internal";
596
+
597
+ // src/directives/mock-expr.ts
598
+ import { absFunction as absFunction2, mock, spy, stub } from "@nudojs/core";
629
599
  function parseSinonExpr(expr) {
630
600
  const s = expr.trim();
631
601
  const stubReturnsMatch = s.match(/^sinon\.stub\(\)\.returns\((.+)\)$/);
@@ -690,7 +660,7 @@ function parseSinonExpr(expr) {
690
660
  if (arrowFn) {
691
661
  return {
692
662
  type: "stub",
693
- returnValue: absFunction(arrowFn.params, {
663
+ returnValue: absFunction2(arrowFn.params, {
694
664
  body: arrowFn.body
695
665
  })
696
666
  };
@@ -790,6 +760,47 @@ function parseNudoMockExpr(expr) {
790
760
  }
791
761
  return null;
792
762
  }
763
+
764
+ // src/directives/fn-directives.ts
765
+ var CASE_TAG_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:case\b[ \t]*([^\n]*)/g;
766
+ var JS_IDENT_RE = /^[\p{L}$_][\p{L}\p{N}$_]*$/u;
767
+ var MOCK_INLINE_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:mock\b[ \t]+([^\s=]+)[ \t]*=[ \t]*(.+)/g;
768
+ var MOCK_FROM_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:mock\b[ \t]+([^\s]+)[ \t]+from[ \t]+(?:"([^"\n]+)"|'([^'\n]+)')/g;
769
+ var MOCK_TAG_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:mock\b[ \t]*([^\n]*)/g;
770
+ var PURE_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:pure\b/g;
771
+ var SKIP_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:skip\b(?:[ \t]+(\S[^\n]*))?/g;
772
+ var SAMPLE_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:sample\b(?:[ \t]+(\S+))?/g;
773
+ var SAMPLE_NUM_RE = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
774
+ function parseSkipReturnsExpr(rest) {
775
+ if (rest === void 0) return void 0;
776
+ const s = rest.trim();
777
+ if (s === "") return void 0;
778
+ if (s.startsWith("=>")) {
779
+ const expr = s.slice(2).trim();
780
+ if (expr === "") {
781
+ emitDirectiveDiag({
782
+ code: DIAG2.directiveSyntax,
783
+ message: `@nudo:skip => requires a type expression (e.g. @nudo:skip => number())`
784
+ });
785
+ return void 0;
786
+ }
787
+ return parseAbsExprDiag(expr, "@nudo:skip =>");
788
+ }
789
+ if (s.startsWith("(")) {
790
+ const inner = extractBalancedParens(s, 0);
791
+ if (inner !== null && s.slice(inner.length + 2).trim() === "") {
792
+ return parseAbsExprDiag(inner, "@nudo:skip (...)");
793
+ }
794
+ if (inner === null) {
795
+ emitDirectiveDiag({
796
+ code: DIAG2.directiveSyntax,
797
+ message: `@nudo:skip type expression has unclosed parenthesis (expected @nudo:skip (<typeExpr>)) \u2014 got: ${s.slice(0, 60)}`
798
+ });
799
+ return void 0;
800
+ }
801
+ }
802
+ return void 0;
803
+ }
793
804
  function parseDirectivesFromCommentTexts(comments) {
794
805
  const directives = [];
795
806
  for (const { text, startLine: commentStartLine } of comments) {
@@ -880,7 +891,7 @@ function parseDirectivesFromCommentTexts(comments) {
880
891
  const raw = sampleMatch[1];
881
892
  if (raw === void 0 || !SAMPLE_NUM_RE.test(raw)) {
882
893
  emitDirectiveDiag({
883
- code: "nudo:directive-syntax",
894
+ code: DIAG2.directiveSyntax,
884
895
  message: `Malformed @nudo:sample: expected a numeric count (integer, decimal, or negative) \u2014 got: @nudo:sample ${raw ?? "(missing count)"}`
885
896
  });
886
897
  continue;
@@ -902,7 +913,7 @@ function scanMalformedCaseNames(text, caseArgSpans) {
902
913
  const parenIdx = m.index + m[0].length - m[1].length + legal[0].length - 1;
903
914
  if (extractBalancedParens(text, parenIdx) === null) {
904
915
  emitDirectiveDiag({
905
- code: "nudo:directive-syntax",
916
+ code: DIAG2.directiveSyntax,
906
917
  message: `Malformed @nudo:case: unclosed argument list (missing closing ")") \u2014 got: @nudo:case ${rest.slice(0, 60)}`
907
918
  });
908
919
  }
@@ -921,7 +932,7 @@ function scanMalformedCaseNames(text, caseArgSpans) {
921
932
  reason = `missing quoted name (expected @nudo:case "name" (\u2026))`;
922
933
  }
923
934
  emitDirectiveDiag({
924
- code: "nudo:directive-syntax",
935
+ code: DIAG2.directiveSyntax,
925
936
  message: `Malformed @nudo:case: ${reason} \u2014 got: @nudo:case ${rest.slice(0, 60)}`
926
937
  });
927
938
  }
@@ -936,7 +947,7 @@ function scanMalformedMockNames(text, caseArgSpans) {
936
947
  const nameMatch = rest.match(/^([^\s=]+)(?:\s*=|\s+from\b)/);
937
948
  if (!nameMatch) {
938
949
  emitDirectiveDiag({
939
- code: "nudo:directive-syntax",
950
+ code: DIAG2.directiveSyntax,
940
951
  message: `Malformed @nudo:mock: cannot parse name (expected @nudo:mock <ident> = <expr> or @nudo:mock <ident> from "path" / 'path') \u2014 got: ${rest.slice(0, 60)}`
941
952
  });
942
953
  continue;
@@ -944,7 +955,7 @@ function scanMalformedMockNames(text, caseArgSpans) {
944
955
  const name = nameMatch[1];
945
956
  if (!JS_IDENT_RE.test(name)) {
946
957
  emitDirectiveDiag({
947
- code: "nudo:directive-syntax",
958
+ code: DIAG2.directiveSyntax,
948
959
  message: `Malformed @nudo:mock name '${name}': not a valid identifier (use letters, digits, _, $; Unicode letters allowed)`
949
960
  });
950
961
  continue;
@@ -952,7 +963,7 @@ function scanMalformedMockNames(text, caseArgSpans) {
952
963
  const afterName = rest.slice(name.length);
953
964
  if (/^[ \t]+from[ \t]+(?:"[^"\n]*|'[^'\n]*)$/.test(afterName)) {
954
965
  emitDirectiveDiag({
955
- code: "nudo:directive-syntax",
966
+ code: DIAG2.directiveSyntax,
956
967
  message: `Malformed @nudo:mock from: unclosed quote in path (expected @nudo:mock ${name} from "path" / 'path') \u2014 got: @nudo:mock ${rest.slice(0, 60)}`
957
968
  });
958
969
  }
@@ -960,13 +971,7 @@ function scanMalformedMockNames(text, caseArgSpans) {
960
971
  }
961
972
  function extractDirectives(ast, opts) {
962
973
  if (!opts?.diags) return collectFnDirectives(ast);
963
- const prev = activeDiagSink;
964
- activeDiagSink = { list: opts.diags, seen: /* @__PURE__ */ new Set() };
965
- try {
966
- return collectFnDirectives(ast);
967
- } finally {
968
- activeDiagSink = prev;
969
- }
974
+ return runWithDirectiveDiags(opts.diags, () => collectFnDirectives(ast));
970
975
  }
971
976
  function collectFnDirectives(ast) {
972
977
  const results = [];
@@ -987,6 +992,10 @@ function collectFnDirectives(ast) {
987
992
  function extractDirectivesQuiet(ast) {
988
993
  return extractDirectives(ast, { diags: [] });
989
994
  }
995
+
996
+ // src/directives/inline-directives.ts
997
+ import { CONSTRAINT_EXPR_RE as CONSTRAINT_EXPR_RE2 } from "@nudojs/core";
998
+ import { DIAG as DIAG3 } from "@nudojs/core/internal";
990
999
  function stripInlineCommentPrefix(line, kind) {
991
1000
  const s = kind === "block" ? line.replace(/^\s*\*\s?/, "") : line.replace(/^\//, "");
992
1001
  return s.trim();
@@ -1017,13 +1026,7 @@ function extractInlineDirectives(node, opts) {
1017
1026
  const comments = node.leadingComments;
1018
1027
  if (!comments) return [];
1019
1028
  if (!opts?.diags) return collectInlineDirectivesFromComments(comments);
1020
- const prev = activeDiagSink;
1021
- activeDiagSink = { list: opts.diags, seen: /* @__PURE__ */ new Set() };
1022
- try {
1023
- return collectInlineDirectivesFromComments(comments);
1024
- } finally {
1025
- activeDiagSink = prev;
1026
- }
1029
+ return runWithDirectiveDiags(opts.diags, () => collectInlineDirectivesFromComments(comments));
1027
1030
  }
1028
1031
  function collectInlineDirectivesFromComments(comments) {
1029
1032
  const results = [];
@@ -1039,7 +1042,7 @@ function collectInlineDirectivesFromComments(comments) {
1039
1042
  if (trailingIdx !== -1) {
1040
1043
  const typePart = rawExpr.slice(0, trailingIdx).trim();
1041
1044
  emitDirectiveDiag({
1042
- code: "nudo:directive-syntax",
1045
+ code: DIAG3.directiveSyntax,
1043
1046
  message: `Trailing comment in @nudo:as type expression: '${rawExpr}' (remove the comment; the type must be a clean expression)`
1044
1047
  });
1045
1048
  results.push({ kind: "as", typeAbs: typePart ? parseAbsExprDiag(typePart, "@nudo:as") : absUnknown() });
@@ -1050,7 +1053,7 @@ function collectInlineDirectivesFromComments(comments) {
1050
1053
  }
1051
1054
  if (AS_BARE_RE.test(line)) {
1052
1055
  emitDirectiveDiag({
1053
- code: "nudo:directive-syntax",
1056
+ code: DIAG3.directiveSyntax,
1054
1057
  message: `@nudo:as requires a type expression (e.g. @nudo:as number())`
1055
1058
  });
1056
1059
  continue;
@@ -1069,7 +1072,7 @@ function collectInlineDirectivesFromComments(comments) {
1069
1072
  });
1070
1073
  } else {
1071
1074
  emitDirectiveDiag({
1072
- code: "nudo:directive-syntax",
1075
+ code: DIAG3.directiveSyntax,
1073
1076
  message: `Malformed @nudo:replace: cannot separate target from type (expected @nudo:replace <target> <typeExpr>) \u2014 got: ${raw.slice(0, 60)}`
1074
1077
  });
1075
1078
  }
@@ -1077,7 +1080,7 @@ function collectInlineDirectivesFromComments(comments) {
1077
1080
  }
1078
1081
  if (REPLACE_BARE_RE.test(line)) {
1079
1082
  emitDirectiveDiag({
1080
- code: "nudo:directive-syntax",
1083
+ code: DIAG3.directiveSyntax,
1081
1084
  message: `@nudo:replace requires a target and type expression (expected @nudo:replace <target> <typeExpr>)`
1082
1085
  });
1083
1086
  }
@@ -1108,13 +1111,17 @@ function findReplaceSeparator(raw) {
1108
1111
  }
1109
1112
  if (ch === " " && depth === 0) {
1110
1113
  const rest = raw.slice(i + 1).trimStart();
1111
- if (CONSTRAINT_EXPR_RE.test(rest) || rest.startsWith("{") || rest.startsWith("[") || rest.startsWith('"') || rest.startsWith("'") || rest === "true" || rest === "false" || rest === "null" || rest === "undefined" || rest === "unknown" || rest === "any" || rest === "never" || /^-?\d+(\.\d+)?$/.test(rest) || rest.includes("=>") || /^function\s*[\w$]*\s*\(/.test(rest)) {
1114
+ if (CONSTRAINT_EXPR_RE2.test(rest) || rest.startsWith("{") || rest.startsWith("[") || rest.startsWith('"') || rest.startsWith("'") || rest === "true" || rest === "false" || rest === "null" || rest === "undefined" || rest === "unknown" || rest === "any" || rest === "never" || /^-?\d+(\.\d+)?$/.test(rest) || rest.includes("=>") || /^function\s*[\w$]*\s*\(/.test(rest)) {
1112
1115
  return i;
1113
1116
  }
1114
1117
  }
1115
1118
  }
1116
1119
  return -1;
1117
1120
  }
1121
+
1122
+ // src/directives/file-directives.ts
1123
+ import { parseEnvPayload, parseMockModulePayload } from "@nudojs/core";
1124
+ import { DIAG as DIAG4 } from "@nudojs/core/internal";
1118
1125
  var ENV_REGEX = /^\/?\s*@nudo:env\s+(.+)/;
1119
1126
  var MOCK_MODULE_REGEX = /^\/?\s*@nudo:mock-module\s+(.+)/;
1120
1127
  var FILE_TAG_IN_BLOCK_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*)?@nudo:(env|mock-module)\b/g;
@@ -1124,20 +1131,14 @@ function warnMisplacedFileTags(blockText) {
1124
1131
  while (m = FILE_TAG_IN_BLOCK_REGEX.exec(blockText)) {
1125
1132
  const tag = m[1];
1126
1133
  emitDirectiveDiag({
1127
- code: "nudo:directive-syntax",
1134
+ code: DIAG4.directiveSyntax,
1128
1135
  message: tag === "env" ? `@nudo:env is a file-level directive \u2014 use '/// @nudo:env node' at the top of the file (JSDoc block form is ignored)` : `@nudo:mock-module is a file-level directive \u2014 use '/// @nudo:mock-module "fs" from "./mock-fs.js"' at the top of the file (JSDoc block form is ignored)`
1129
1136
  });
1130
1137
  }
1131
1138
  }
1132
1139
  function extractFileDirectives(ast, opts) {
1133
1140
  if (!opts?.diags) return collectFileDirectives(ast);
1134
- const prev = activeDiagSink;
1135
- activeDiagSink = { list: opts.diags, seen: /* @__PURE__ */ new Set() };
1136
- try {
1137
- return collectFileDirectives(ast);
1138
- } finally {
1139
- activeDiagSink = prev;
1140
- }
1141
+ return runWithDirectiveDiags(opts.diags, () => collectFileDirectives(ast));
1141
1142
  }
1142
1143
  function collectFileDirectives(ast) {
1143
1144
  const results = [];
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@nudojs/parser",
3
- "version": "1.4.24",
3
+ "version": "1.5.0",
4
4
  "engines": {
5
- "node": ">=20"
5
+ "node": ">=22"
6
6
  },
7
7
  "description": "Babel-based parser and directive extraction for Nudo",
8
8
  "type": "module",
@@ -39,7 +39,7 @@
39
39
  "registry": "https://registry.npmjs.org"
40
40
  },
41
41
  "dependencies": {
42
- "@nudojs/core": "1.14.1",
42
+ "@nudojs/core": "1.15.0",
43
43
  "@babel/types": "^8.0.6"
44
44
  },
45
45
  "scripts": {