crawlemon 0.2.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.
Files changed (3) hide show
  1. package/README.md +11 -3
  2. package/dist/index.js +1554 -197
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/index.ts
2
- import fs6 from "node:fs";
2
+ import fs7 from "node:fs";
3
3
  import os from "node:os";
4
- import path8 from "node:path";
4
+ import path9 from "node:path";
5
5
  import { spawnSync } from "node:child_process";
6
6
 
7
7
  // ../next-adapter/src/parser.ts
@@ -197,20 +197,646 @@ function parsePageSource(content, framework = "nextjs") {
197
197
  }
198
198
 
199
199
  // ../next-adapter/src/scanner.ts
200
- import fs2 from "node:fs";
201
- import path2 from "node:path";
200
+ import fs3 from "node:fs";
201
+ import path3 from "node:path";
202
202
 
203
203
  // ../next-adapter/src/dependency-graph.ts
204
204
  import fs from "node:fs";
205
205
  import path from "node:path";
206
+
207
+ // ../next-adapter/src/value-parser.ts
208
+ function isWhitespace(ch) {
209
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\xA0";
210
+ }
211
+ function skipTrivia(text, index) {
212
+ let i = index;
213
+ while (i < text.length) {
214
+ if (isWhitespace(text[i])) {
215
+ i += 1;
216
+ continue;
217
+ }
218
+ if (text[i] === "/" && text[i + 1] === "/") {
219
+ while (i < text.length && text[i] !== "\n") i += 1;
220
+ continue;
221
+ }
222
+ if (text[i] === "/" && text[i + 1] === "*") {
223
+ const end = text.indexOf("*/", i + 2);
224
+ i = end === -1 ? text.length : end + 2;
225
+ continue;
226
+ }
227
+ break;
228
+ }
229
+ return i;
230
+ }
231
+ function readStringLiteral(text, index) {
232
+ const quote = text[index];
233
+ let i = index + 1;
234
+ let value = "";
235
+ while (i < text.length) {
236
+ const ch = text[i];
237
+ if (ch === "\\") {
238
+ value += text[i + 1] ?? "";
239
+ i += 2;
240
+ continue;
241
+ }
242
+ if (ch === quote) return { value, end: i + 1 };
243
+ value += ch;
244
+ i += 1;
245
+ }
246
+ return { value, end: i };
247
+ }
248
+ function readTemplateLiteral(text, index) {
249
+ let i = index + 1;
250
+ let depth = 0;
251
+ let value = "";
252
+ while (i < text.length) {
253
+ const ch = text[i];
254
+ if (ch === "\\") {
255
+ value += text[i + 1] ?? "";
256
+ i += 2;
257
+ continue;
258
+ }
259
+ if (ch === "`" && depth === 0) return { value, end: i + 1 };
260
+ if (ch === "$" && text[i + 1] === "{") {
261
+ depth += 1;
262
+ value += "${";
263
+ i += 2;
264
+ continue;
265
+ }
266
+ if (depth > 0 && ch === "}") {
267
+ depth -= 1;
268
+ value += "}";
269
+ i += 1;
270
+ continue;
271
+ }
272
+ value += ch;
273
+ i += 1;
274
+ }
275
+ return { value, end: i };
276
+ }
277
+ function readIdentifier(text, index) {
278
+ let i = index;
279
+ while (i < text.length && /[A-Za-z0-9_$]/.test(text[i])) i += 1;
280
+ return { name: text.slice(index, i), end: i };
281
+ }
282
+ function readTemplateWhole(text, index) {
283
+ return readTemplateLiteral(text, index);
284
+ }
285
+ function readBalanced(text, open) {
286
+ const closer = { "(": ")", "[": "]", "{": "}" };
287
+ const expected = closer[text[open]];
288
+ if (!expected) return open + 1;
289
+ let depth = 0;
290
+ let i = open;
291
+ while (i < text.length) {
292
+ const ch = text[i];
293
+ if (ch === '"' || ch === "'") {
294
+ i = readStringLiteral(text, i).end;
295
+ continue;
296
+ }
297
+ if (ch === "`") {
298
+ i = readTemplateWhole(text, i).end;
299
+ continue;
300
+ }
301
+ if (ch === "/" && text[i + 1] === "/") {
302
+ i = skipTrivia(text, i);
303
+ continue;
304
+ }
305
+ if (ch === "/" && text[i + 1] === "*") {
306
+ i = skipTrivia(text, i);
307
+ continue;
308
+ }
309
+ if (ch === "{" || ch === "[" || ch === "(") depth += 1;
310
+ else if (ch === "}" || ch === "]" || ch === ")") {
311
+ depth -= 1;
312
+ if (depth === 0) return i;
313
+ }
314
+ i += 1;
315
+ }
316
+ return text.length;
317
+ }
318
+ function stripTsSuffix(raw) {
319
+ let text = raw.trim();
320
+ text = text.replace(/^as\s+const\s*/i, "");
321
+ text = text.replace(/\s+as\s+const\s*$/i, "").replace(/\s+satisfies\s+[A-Za-z_][\w.\[\]'"<>|&]*\s*$/i, "");
322
+ return text.trim();
323
+ }
324
+ function parseTemplateValue(text, index) {
325
+ const { value, end } = readTemplateWhole(text, index);
326
+ const parts = [];
327
+ let cursor = 0;
328
+ while (cursor < value.length) {
329
+ const dollar = value.indexOf("${", cursor);
330
+ if (dollar === -1) {
331
+ if (cursor < value.length) parts.push({ t: "str", value: value.slice(cursor) });
332
+ break;
333
+ }
334
+ if (dollar > cursor) parts.push({ t: "str", value: value.slice(cursor, dollar) });
335
+ const exprEnd = findClosingBrace(value, dollar + 1);
336
+ const exprRaw = value.slice(dollar + 2, exprEnd);
337
+ let parsed = { t: "unresolved", raw: exprRaw };
338
+ try {
339
+ const parsedExpr = parseValue(exprRaw);
340
+ if (parsedExpr.t !== "unresolved") parsed = parsedExpr;
341
+ } catch {
342
+ parsed = { t: "unresolved", raw: exprRaw };
343
+ }
344
+ parts.push(parsed);
345
+ cursor = exprEnd + 1;
346
+ }
347
+ return { node: { t: "template", parts, raw: value }, end };
348
+ }
349
+ function findClosingBrace(text, atOpen) {
350
+ let depth = 0;
351
+ let i = atOpen;
352
+ while (i < text.length) {
353
+ const ch = text[i];
354
+ if (ch === "'" || ch === '"') {
355
+ i = readStringLiteral(text, i).end;
356
+ continue;
357
+ }
358
+ if (ch === "`") {
359
+ i = readTemplateWhole(text, i).end;
360
+ continue;
361
+ }
362
+ if (ch === "{") depth += 1;
363
+ else if (ch === "}") {
364
+ depth -= 1;
365
+ if (depth === 0) return i;
366
+ }
367
+ i += 1;
368
+ }
369
+ return text.length;
370
+ }
371
+ function scanContainerElementEnd(text, index) {
372
+ let depth = 0;
373
+ let i = index;
374
+ while (i < text.length) {
375
+ const ch = text[i];
376
+ if (ch === '"' || ch === "'") {
377
+ i = readStringLiteral(text, i).end;
378
+ continue;
379
+ }
380
+ if (ch === "`") {
381
+ i = readTemplateWhole(text, i).end;
382
+ continue;
383
+ }
384
+ if (ch === "/" && text[i + 1] === "/") {
385
+ while (i < text.length && text[i] !== "\n") i += 1;
386
+ continue;
387
+ }
388
+ if (ch === "/" && text[i + 1] === "*") {
389
+ const end = text.indexOf("*/", i + 2);
390
+ i = end === -1 ? text.length : end + 2;
391
+ continue;
392
+ }
393
+ if (ch === "{" || ch === "[" || ch === "(") {
394
+ depth += 1;
395
+ } else if (ch === "}" || ch === "]" || ch === ")") {
396
+ if (depth === 0) return i;
397
+ depth -= 1;
398
+ } else if (depth === 0 && ch === ",") {
399
+ return i;
400
+ }
401
+ i += 1;
402
+ }
403
+ return text.length;
404
+ }
405
+ function parseObjectValue(text, index) {
406
+ const props = [];
407
+ let i = skipTrivia(text, index + 1);
408
+ if (text[i] === "}") return { node: { t: "obj", props }, end: i + 1 };
409
+ while (i < text.length) {
410
+ i = skipTrivia(text, i);
411
+ const ch = text[i];
412
+ if (ch === "}") return { node: { t: "obj", props }, end: i + 1 };
413
+ if (ch === ",") {
414
+ i += 1;
415
+ continue;
416
+ }
417
+ if (ch === ".") {
418
+ const restEnd = scanContainerElementEnd(text, i);
419
+ if (restEnd === text.length) return { node: { t: "obj", props }, end: text.length };
420
+ i = restEnd;
421
+ continue;
422
+ }
423
+ let key;
424
+ if (ch === '"' || ch === "'") {
425
+ key = readStringLiteral(text, i).value;
426
+ i = readStringLiteral(text, i).end;
427
+ } else {
428
+ const ident = readIdentifier(text, i);
429
+ if (ident.end === i) {
430
+ const skipEnd = scanContainerElementEnd(text, i);
431
+ if (skipEnd === text.length) return { node: { t: "obj", props }, end: text.length };
432
+ i = skipEnd;
433
+ continue;
434
+ }
435
+ key = ident.name;
436
+ i = ident.end;
437
+ }
438
+ i = skipTrivia(text, i);
439
+ if (text[i] === ":") {
440
+ const valueStart = skipTrivia(text, i + 1);
441
+ const boundary = scanContainerElementEnd(text, valueStart);
442
+ const raw = text.slice(valueStart, boundary).trim();
443
+ let node;
444
+ try {
445
+ node = parseValue(raw) || { t: "unresolved", raw };
446
+ } catch {
447
+ node = { t: "unresolved", raw };
448
+ }
449
+ props.push({ key, value: node, raw });
450
+ i = boundary;
451
+ } else {
452
+ props.push({ key, value: { t: "ident", name: key }, raw: key });
453
+ i = skipTrivia(text, i);
454
+ if (text[i] === ",") i += 1;
455
+ }
456
+ }
457
+ return { node: { t: "obj", props }, end: text.length };
458
+ }
459
+ function parseArrayValue(text, index) {
460
+ const items = [];
461
+ let i = skipTrivia(text, index + 1);
462
+ if (text[i] === "]") return { node: { t: "arr", items }, end: i + 1 };
463
+ while (i < text.length) {
464
+ i = skipTrivia(text, i);
465
+ const ch = text[i];
466
+ if (ch === "]") return { node: { t: "arr", items }, end: i + 1 };
467
+ if (ch === ",") {
468
+ i += 1;
469
+ continue;
470
+ }
471
+ const boundary = scanContainerElementEnd(text, i);
472
+ const raw = text.slice(i, boundary).trim();
473
+ let node;
474
+ try {
475
+ node = parseValue(raw) || { t: "unresolved", raw };
476
+ } catch {
477
+ node = { t: "unresolved", raw };
478
+ }
479
+ items.push(node);
480
+ i = boundary;
481
+ }
482
+ return { node: { t: "arr", items }, end: text.length };
483
+ }
484
+ function parseValueAt(text, index) {
485
+ let i = skipTrivia(text, index);
486
+ const ch = text[i];
487
+ if (ch === '"' || ch === "'") {
488
+ const { value, end } = readStringLiteral(text, i);
489
+ return { node: { t: "str", value }, end };
490
+ }
491
+ if (ch === "`") {
492
+ return parseTemplateValue(text, i);
493
+ }
494
+ if (ch === "{") {
495
+ return parseObjectValue(text, i);
496
+ }
497
+ if (ch === "[") {
498
+ return parseArrayValue(text, i);
499
+ }
500
+ if (text.startsWith("new ", i)) {
501
+ const after = skipTrivia(text, i + 4);
502
+ const callee = readIdentifier(text, after);
503
+ if (callee.name.toLowerCase() === "url") {
504
+ const open = skipTrivia(text, callee.end);
505
+ if (text[open] === "(") {
506
+ const argResult = parseValueAt(text, skipTrivia(text, open + 1));
507
+ return {
508
+ node: { t: "new-url", arg: argResult.node },
509
+ end: readBalanced(text, open) + 1
510
+ };
511
+ }
512
+ }
513
+ }
514
+ const firstChar = text[i];
515
+ if (/[A-Za-z_$]/.test(firstChar)) {
516
+ const ident = readIdentifier(text, i);
517
+ let node = { t: "ident", name: ident.name };
518
+ let cursor = ident.end;
519
+ while (true) {
520
+ const nxt = skipTrivia(text, cursor);
521
+ if (text[nxt] === "." || text[nxt] === "?" && text[nxt + 1] === ".") {
522
+ const dotEnd = text[nxt] === "?" ? nxt + 2 : nxt + 1;
523
+ const propStart = skipTrivia(text, dotEnd);
524
+ const prop2 = readIdentifier(text, propStart);
525
+ if (prop2.end > propStart) {
526
+ node = { t: "member", object: node, prop: prop2.name };
527
+ cursor = prop2.end;
528
+ continue;
529
+ }
530
+ break;
531
+ }
532
+ if (text[nxt] === "(") {
533
+ return { node: { t: "unresolved", raw: text.slice(index, readBalanced(text, nxt) + 1) }, end: readBalanced(text, nxt) + 1 };
534
+ }
535
+ break;
536
+ }
537
+ return { node, end: cursor };
538
+ }
539
+ if (/[0-9]/.test(firstChar) || firstChar === "-" && /[0-9]/.test(text[i + 1])) {
540
+ let cursor = i;
541
+ while (cursor < text.length && /[0-9.eE_+-]/.test(text[cursor])) cursor += 1;
542
+ const raw = text.slice(i, cursor);
543
+ const num = Number(raw.replace(/_/g, ""));
544
+ return { node: { t: "num", value: Number.isFinite(num) ? num : NaN }, end: cursor };
545
+ }
546
+ if (text.startsWith("true", i) || text.startsWith("false", i)) {
547
+ return { node: { t: "bool", value: text.startsWith("true", i) }, end: i + (text.startsWith("true", i) ? 4 : 5) };
548
+ }
549
+ return { node: { t: "unresolved", raw: text.slice(index, i + 1).trim() }, end: i + 1 };
550
+ }
551
+ function parseValue(rawInput) {
552
+ const text = stripTsSuffix(rawInput);
553
+ const { node, end } = parseValueAt(text, 0);
554
+ const rest = skipTrivia(text, end);
555
+ if (rest < text.length) {
556
+ let current = node;
557
+ let cursor = rest;
558
+ while (true) {
559
+ cursor = skipTrivia(text, cursor);
560
+ if (text[cursor] === "+") {
561
+ const rhs = parseValueAt(text, skipTrivia(text, cursor + 1));
562
+ if (rhs.node.t === "unresolved" && current.t === "unresolved") break;
563
+ const parts = [];
564
+ const collect = (n) => n.t === "concat" ? parts.push(...n.parts) : parts.push(n);
565
+ collect(current);
566
+ collect(rhs.node);
567
+ current = { t: "concat", parts };
568
+ cursor = rhs.end;
569
+ continue;
570
+ }
571
+ break;
572
+ }
573
+ if (cursor >= text.length) return current;
574
+ }
575
+ return node;
576
+ }
577
+
578
+ // ../next-adapter/src/next-metadata.ts
579
+ function prop(node, key) {
580
+ if (!node || node.t !== "obj") return void 0;
581
+ const found = node.props.find((p) => p.key === key);
582
+ if (!found) return void 0;
583
+ return { value: found.value, raw: found.raw };
584
+ }
585
+ function skipQuoted(text, index) {
586
+ const quote = text[index];
587
+ let i = index + 1;
588
+ while (i < text.length) {
589
+ if (text[i] === "\\") {
590
+ i += 2;
591
+ continue;
592
+ }
593
+ if (quote === "`" && text[i] === "`") return i;
594
+ if (quote !== "`" && text[i] === quote) return i;
595
+ if (quote === "`" && text[i] === "$" && text[i + 1] === "{") {
596
+ const close = text.indexOf("}", i);
597
+ i = close === -1 ? text.length : close + 1;
598
+ continue;
599
+ }
600
+ i += 1;
601
+ }
602
+ return index;
603
+ }
604
+ function flatten(node, path10 = "") {
605
+ const map = /* @__PURE__ */ new Map();
606
+ if (node.t !== "obj") return map;
607
+ for (const p of node.props) {
608
+ const full = path10 ? `${path10}.${p.key}` : p.key;
609
+ map.set(full, p.value);
610
+ }
611
+ return map;
612
+ }
613
+ function rawOfValue(file, resolver, raw) {
614
+ if (!resolver) return void 0;
615
+ const result = resolver.foldExpression(file, raw);
616
+ if (result.ok && typeof result.value === "string") return result.value;
617
+ return void 0;
618
+ }
619
+ function fieldFromRaw(file, resolver, raw) {
620
+ if (raw === void 0) return { declared: false };
621
+ const value = rawOfValue(file, resolver, raw);
622
+ return { declared: true, raw, value };
623
+ }
624
+ function robotsFieldFromRaw(raw) {
625
+ if (raw === void 0) return { declared: false };
626
+ const parsed = parseValue(raw);
627
+ if (parsed.t === "str") return { declared: true, raw, value: parsed.value };
628
+ const value = /noindex|\bindex\s*:\s*false\b/.test(raw) ? "noindex" : "";
629
+ return { declared: true, raw, value };
630
+ }
631
+ function extractFromObject(file, resolver, rawObject) {
632
+ let node;
633
+ try {
634
+ node = parseValue(rawObject);
635
+ } catch {
636
+ node = { t: "unresolved", raw: rawObject };
637
+ }
638
+ const props = flatten(node);
639
+ const titleProp = props.get("title");
640
+ let title = { declared: false };
641
+ let titleObject;
642
+ if (titleProp) {
643
+ if (titleProp.t === "obj") {
644
+ const absoluteRaw = prop(titleProp, "absolute")?.raw;
645
+ const defaultRaw = prop(titleProp, "default")?.raw;
646
+ const templateRaw = prop(titleProp, "template")?.raw;
647
+ const absolute = fieldFromRaw(file, resolver, absoluteRaw);
648
+ const defaultField = fieldFromRaw(file, resolver, defaultRaw);
649
+ const template = fieldFromRaw(file, resolver, templateRaw);
650
+ titleObject = { absolute, default: defaultField, template };
651
+ title = {
652
+ declared: true,
653
+ kind: "object",
654
+ value: absolute.value ?? defaultField.value,
655
+ raw: absoluteRaw ?? defaultRaw
656
+ };
657
+ } else {
658
+ const value = fieldFromRaw(file, resolver, prop(node, "title")?.raw);
659
+ title = { ...value, kind: "string" };
660
+ }
661
+ } else {
662
+ titleObject = void 0;
663
+ }
664
+ const alternatesNode = props.get("alternates");
665
+ const canonicalRaw = prop(alternatesNode, "canonical")?.raw ?? prop(node, "canonical")?.raw;
666
+ return {
667
+ source: "export-metadata",
668
+ dynamic: false,
669
+ hasParams: false,
670
+ title,
671
+ titleObject,
672
+ description: fieldFromRaw(file, resolver, prop(node, "description")?.raw),
673
+ canonical: fieldFromRaw(file, resolver, canonicalRaw),
674
+ robots: robotsFieldFromRaw(prop(node, "robots")?.raw),
675
+ metadataBase: fieldFromRaw(file, resolver, prop(node, "metadataBase")?.raw)
676
+ };
677
+ }
678
+ function findObjectAfter(content, keywordPattern) {
679
+ for (const match of content.matchAll(keywordPattern)) {
680
+ const eq = content.indexOf("=", match.index);
681
+ if (eq === -1) continue;
682
+ const brace = content.indexOf("{", eq);
683
+ if (brace === -1) continue;
684
+ const end = readBalanced(content, brace);
685
+ return { raw: content.slice(brace, end + 1), index: brace };
686
+ }
687
+ return null;
688
+ }
689
+ function extractNextMetadata(content, file, resolver) {
690
+ const exportObj = findObjectAfter(content, /export\s+const\s+metadata\b/g);
691
+ const fnMatch = content.match(/export\s+(?:async\s+)?function\s+generateMetadata\b/g);
692
+ const fnObj = fnMatch ? findObjectAfter(content, /function\s+generateMetadata\s*\([^)]*\)\s*\{/g) : null;
693
+ if (!exportObj && !fnMatch) return null;
694
+ if (exportObj) {
695
+ return extractFromObject(file, resolver, exportObj.raw);
696
+ }
697
+ const candidates = [];
698
+ const fnAt = content.indexOf("generateMetadata");
699
+ let hasParams = false;
700
+ let bodyStart = -1;
701
+ let bodyEnd = -1;
702
+ if (fnAt !== -1) {
703
+ const openParen = content.indexOf("(", fnAt);
704
+ if (openParen !== -1) {
705
+ const afterParen = readBalanced(content, openParen) + 1;
706
+ const signature = content.slice(openParen, afterParen);
707
+ hasParams = /\b(params|searchParams)\b/.test(signature);
708
+ let cursor = afterParen;
709
+ while (cursor < content.length && content[cursor] !== "{") cursor += 1;
710
+ if (content[cursor] === "{") {
711
+ bodyStart = cursor;
712
+ bodyEnd = readBalanced(content, cursor);
713
+ }
714
+ }
715
+ }
716
+ if (bodyStart !== -1) {
717
+ let scan = bodyStart + 1;
718
+ while (scan < bodyEnd) {
719
+ if (content[scan] === "{") {
720
+ const end = readBalanced(content, scan);
721
+ if (end < bodyEnd) candidates.push(content.slice(scan, end + 1));
722
+ scan = end + 1;
723
+ continue;
724
+ }
725
+ if (content[scan] === '"' || content[scan] === "'") {
726
+ scan = skipQuoted(content, scan) + 1;
727
+ continue;
728
+ }
729
+ if (content[scan] === "`") {
730
+ scan = skipQuoted(content, scan) + 1;
731
+ continue;
732
+ }
733
+ scan += 1;
734
+ }
735
+ }
736
+ if (candidates.length === 0) return null;
737
+ const merged = {
738
+ source: "generate-metadata",
739
+ dynamic: true,
740
+ hasParams,
741
+ title: { declared: false },
742
+ description: { declared: false },
743
+ canonical: { declared: false },
744
+ robots: { declared: false },
745
+ metadataBase: { declared: false }
746
+ };
747
+ merged.dynamic = hasParams || candidates.length > 1;
748
+ const prefer = (target, candidate) => {
749
+ if (!target.declared) return candidate;
750
+ if (target.value === void 0 && candidate.value !== void 0) return candidate;
751
+ return target;
752
+ };
753
+ for (const raw of candidates) {
754
+ const extract = extractFromObject(file, resolver, raw);
755
+ merged.title = prefer(merged.title, extract.title);
756
+ if (extract.titleObject && (!merged.titleObject || merged.title.value === void 0)) {
757
+ merged.titleObject = extract.titleObject;
758
+ }
759
+ merged.description = prefer(merged.description, extract.description);
760
+ merged.canonical = prefer(merged.canonical, extract.canonical);
761
+ merged.robots = prefer(merged.robots, extract.robots);
762
+ merged.metadataBase = prefer(merged.metadataBase, extract.metadataBase);
763
+ }
764
+ return merged;
765
+ }
766
+ function applyTitleTemplate(title, template) {
767
+ if (!template || !template.includes("%s")) return title;
768
+ return template.replace(/%s/g, title);
769
+ }
770
+ function buildEffectiveMetadata(extract, inherited, ctx) {
771
+ const out = { ...inherited };
772
+ const base = extract.metadataBase.value ?? inherited.metadataBase ?? ctx.origin;
773
+ if (base) out.metadataBase = base.replace(/\/$/, "");
774
+ const titleTemplate = extract.titleObject?.template?.value ?? inherited.titleTemplate;
775
+ if (titleTemplate) out.titleTemplate = titleTemplate;
776
+ out.titleDeclared = extract.title.declared;
777
+ if (extract.title.declared) {
778
+ if (extract.title.kind === "object" || extract.titleObject) {
779
+ const absolute = extract.titleObject?.absolute?.value;
780
+ const def = extract.titleObject?.default?.value;
781
+ const value = absolute ?? def;
782
+ if (value !== void 0) {
783
+ out.title = absolute ? value : applyTitleTemplate(value, ctx.isLayout ? void 0 : titleTemplate);
784
+ } else {
785
+ out.title = void 0;
786
+ }
787
+ } else if (extract.title.value !== void 0) {
788
+ out.title = ctx.isLayout ? extract.title.value : applyTitleTemplate(extract.title.value, titleTemplate);
789
+ } else {
790
+ out.title = void 0;
791
+ }
792
+ }
793
+ out.descriptionDeclared = extract.description.declared;
794
+ if (extract.description.value !== void 0) out.description = extract.description.value;
795
+ else if (extract.description.declared) out.description = void 0;
796
+ out.canonicalDeclared = extract.canonical.declared;
797
+ if (extract.canonical.value !== void 0) {
798
+ out.canonical = absolutize(extract.canonical.value, base);
799
+ } else if (extract.canonical.declared) {
800
+ out.canonical = void 0;
801
+ }
802
+ if (extract.robots.value !== void 0) out.robots = extract.robots.value;
803
+ out.metadataSource = extract.source;
804
+ if (extract.dynamic) out.dynamicMetadata = true;
805
+ return out;
806
+ }
807
+ function absolutize(value, base) {
808
+ if (/^https?:\/\//i.test(value)) return value;
809
+ if (value.startsWith("/") && base) {
810
+ return `${base.replace(/\/$/, "")}${value}`;
811
+ }
812
+ return value;
813
+ }
814
+ function resolvePageMetadata(content, file, inherited, ctx) {
815
+ const extract = extractNextMetadata(content, file, ctx.resolver);
816
+ if (!extract) {
817
+ return inherited;
818
+ }
819
+ return buildEffectiveMetadata(extract, inherited, { ...ctx, isLayout: false });
820
+ }
821
+ function resolveLayoutMetadata(content, file, ctx) {
822
+ const extract = extractNextMetadata(content, file, ctx.resolver);
823
+ if (!extract) return {};
824
+ return buildEffectiveMetadata(extract, {}, { ...ctx, isLayout: true });
825
+ }
826
+
827
+ // ../next-adapter/src/dependency-graph.ts
206
828
  var RouteDependencyGraph = class {
207
829
  projectRoot;
208
830
  appDir;
209
831
  layoutsByDir = /* @__PURE__ */ new Map();
210
832
  routes = [];
211
- constructor(projectRoot, appDir) {
833
+ resolver;
834
+ origin;
835
+ constructor(projectRoot, appDir, resolver, origin) {
212
836
  this.projectRoot = projectRoot;
213
837
  this.appDir = appDir;
838
+ this.resolver = resolver;
839
+ this.origin = origin;
214
840
  }
215
841
  indexLayouts() {
216
842
  this.layoutsByDir.clear();
@@ -225,7 +851,7 @@ var RouteDependencyGraph = class {
225
851
  }
226
852
  } else if (entry.isFile() && /^layout\.(tsx|jsx|js|ts)$/.test(entry.name)) {
227
853
  const content = fs.readFileSync(fullPath, "utf8");
228
- const parsed = parsePageSource(content, "nextjs");
854
+ const metadata = this.resolver ? resolveLayoutMetadata(content, fullPath, { resolver: this.resolver, origin: this.origin, file: fullPath }) : parsePageSource(content, "nextjs").metadata;
229
855
  const relativeDir = path.relative(this.appDir, currentDir);
230
856
  let canonicalLine;
231
857
  let robotsLine;
@@ -240,7 +866,7 @@ var RouteDependencyGraph = class {
240
866
  this.layoutsByDir.set(relativeDir, {
241
867
  filePath: fullPath,
242
868
  relativeDir,
243
- metadata: parsed.metadata,
869
+ metadata,
244
870
  metadataSourceLocation: {
245
871
  canonicalLine,
246
872
  robotsLine,
@@ -374,6 +1000,563 @@ var RouteDependencyGraph = class {
374
1000
  }
375
1001
  };
376
1002
 
1003
+ // ../next-adapter/src/project-resolver.ts
1004
+ import fs2 from "node:fs";
1005
+ import path2 from "node:path";
1006
+ function readStringEnd(text, index) {
1007
+ const quote = text[index];
1008
+ let i = index + 1;
1009
+ while (i < text.length) {
1010
+ if (text[i] === "\\") {
1011
+ i += 2;
1012
+ continue;
1013
+ }
1014
+ if (text[i] === quote) return i + 1;
1015
+ i += 1;
1016
+ }
1017
+ return text.length;
1018
+ }
1019
+ function splitTopLevelStatements(code) {
1020
+ const out = [];
1021
+ let depth = 0;
1022
+ let statementStart = -1;
1023
+ let i = 0;
1024
+ const flush = (end) => {
1025
+ if (statementStart !== -1) {
1026
+ const raw = code.slice(statementStart, end);
1027
+ if (raw.trim()) out.push({ statement: raw, start: statementStart, end });
1028
+ }
1029
+ statementStart = -1;
1030
+ };
1031
+ while (i < code.length) {
1032
+ const ch = code[i];
1033
+ if (ch === " " || ch === " " || ch === "\r") {
1034
+ if (statementStart === -1 && depth === 0 && out.length === 0 && i < code.length) {
1035
+ }
1036
+ i += 1;
1037
+ continue;
1038
+ }
1039
+ if (ch === "\n") {
1040
+ if (depth === 0) flush(i);
1041
+ i += 1;
1042
+ continue;
1043
+ }
1044
+ if (statementStart === -1) statementStart = i;
1045
+ if (ch === "/" && code[i + 1] === "/") {
1046
+ while (i < code.length && code[i] !== "\n") i += 1;
1047
+ continue;
1048
+ }
1049
+ if (ch === "/" && code[i + 1] === "*") {
1050
+ const end = code.indexOf("*/", i + 2);
1051
+ i = end === -1 ? code.length : end + 2;
1052
+ continue;
1053
+ }
1054
+ if (ch === '"' || ch === "'") {
1055
+ i = readStringEnd(code, i);
1056
+ continue;
1057
+ }
1058
+ if (ch === "`") {
1059
+ let t = i + 1;
1060
+ while (t < code.length) {
1061
+ if (code[t] === "\\") {
1062
+ t += 2;
1063
+ continue;
1064
+ }
1065
+ if (code[t] === "`") break;
1066
+ t += 1;
1067
+ }
1068
+ i = Math.min(t + 1, code.length);
1069
+ continue;
1070
+ }
1071
+ if (ch === "{" || ch === "[" || ch === "(") {
1072
+ depth += 1;
1073
+ } else if (ch === "}" || ch === "]" || ch === ")") {
1074
+ depth = Math.max(0, depth - 1);
1075
+ } else if (ch === ";" && depth === 0) {
1076
+ flush(i + 1);
1077
+ i += 1;
1078
+ continue;
1079
+ }
1080
+ i += 1;
1081
+ }
1082
+ flush(code.length);
1083
+ return out;
1084
+ }
1085
+ var EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte"];
1086
+ var ProjectResolver = class {
1087
+ projectRoot;
1088
+ cache = /* @__PURE__ */ new Map();
1089
+ aliasRoots = [];
1090
+ loading = /* @__PURE__ */ new Set();
1091
+ origin;
1092
+ constructor(projectRoot) {
1093
+ this.projectRoot = projectRoot;
1094
+ this.loadAliases();
1095
+ }
1096
+ loadAliases() {
1097
+ const roots = ["tsconfig.json", "tsconfig.app.json"];
1098
+ for (const name of roots) {
1099
+ const file = path2.join(this.projectRoot, name);
1100
+ if (!fs2.existsSync(file)) continue;
1101
+ try {
1102
+ const json = JSON.parse(fs2.readFileSync(file, "utf8"));
1103
+ const paths = json.compilerOptions?.paths || {};
1104
+ const baseUrl = json.compilerOptions?.baseUrl || ".";
1105
+ for (const [key, targets] of Object.entries(paths)) {
1106
+ if (!key.endsWith("/*") || !Array.isArray(targets)) continue;
1107
+ const dir = path2.resolve(this.projectRoot, baseUrl, String(targets[0]).replace(/\*$/, ""));
1108
+ this.aliasRoots.push({ prefix: key.slice(0, -2), dir });
1109
+ }
1110
+ } catch {
1111
+ }
1112
+ }
1113
+ if (this.aliasRoots.length === 0) {
1114
+ this.aliasRoots.push({ prefix: "@", dir: path2.join(this.projectRoot, "src") });
1115
+ }
1116
+ this.aliasRoots.push({ prefix: "~", dir: path2.join(this.projectRoot, "src") });
1117
+ }
1118
+ readFile(filePath) {
1119
+ try {
1120
+ return fs2.readFileSync(filePath, "utf8");
1121
+ } catch {
1122
+ return void 0;
1123
+ }
1124
+ }
1125
+ /**
1126
+ * Converts a module specifier (relative or `@/`/`~/` alias) into an absolute
1127
+ * path when it points at a resolvable local file.
1128
+ */
1129
+ resolveModule(specifier, fromFile) {
1130
+ let base = specifier;
1131
+ let fromDir = fromFile ? path2.dirname(fromFile) : this.projectRoot;
1132
+ const alias = this.aliasRoots.find((a) => base.startsWith(a.prefix + "/"));
1133
+ if (alias) {
1134
+ fromDir = alias.dir;
1135
+ base = base.slice(alias.prefix.length + 1);
1136
+ } else if (base.startsWith(".")) {
1137
+ } else {
1138
+ return null;
1139
+ }
1140
+ if (!fromDir.startsWith(this.projectRoot)) return null;
1141
+ const candidate = path2.resolve(fromDir, base);
1142
+ return this.resolveAsFile(candidate) || this.resolveAsIndex(candidate);
1143
+ }
1144
+ resolveAsFile(candidate) {
1145
+ if (!fs2.existsSync(candidate)) {
1146
+ for (const ext of EXTENSIONS) {
1147
+ if (fs2.existsSync(candidate + ext)) return candidate + ext;
1148
+ }
1149
+ return null;
1150
+ }
1151
+ const stat = fs2.statSync(candidate);
1152
+ if (stat.isFile()) return candidate;
1153
+ return null;
1154
+ }
1155
+ resolveAsIndex(base) {
1156
+ if (fs2.existsSync(base) && fs2.statSync(base).isDirectory()) {
1157
+ for (const name of ["index.ts", "index.tsx", "index.js", "index.jsx"]) {
1158
+ const candidate = path2.join(base, name);
1159
+ if (fs2.existsSync(candidate)) return candidate;
1160
+ }
1161
+ }
1162
+ return null;
1163
+ }
1164
+ getModule(filePath) {
1165
+ if (!filePath) return void 0;
1166
+ const normalized = path2.resolve(filePath);
1167
+ const cached = this.cache.get(normalized);
1168
+ if (cached) return cached;
1169
+ const content = this.readFile(normalized);
1170
+ if (content === void 0) return void 0;
1171
+ const info = {
1172
+ filePath: normalized,
1173
+ content,
1174
+ consts: /* @__PURE__ */ new Map(),
1175
+ destructure: /* @__PURE__ */ new Map(),
1176
+ imports: /* @__PURE__ */ new Map(),
1177
+ importsDefault: /* @__PURE__ */ new Map()
1178
+ };
1179
+ this.cache.set(normalized, info);
1180
+ this.parseModule(info);
1181
+ return info;
1182
+ }
1183
+ parseModule(info) {
1184
+ if (info.parsed) return;
1185
+ info.parsed = true;
1186
+ for (const { statement } of splitTopLevelStatements(info.content)) {
1187
+ const stmt = statement.trim();
1188
+ const importMatch = stmt.match(/^import\s+([\s\S]+?)\s+from\s+["']([^"']+)["']/);
1189
+ if (importMatch) {
1190
+ const spec = importMatch[2];
1191
+ const resolved = this.resolveModule(spec, info.filePath);
1192
+ const clause = importMatch[1].trim();
1193
+ const namedBlock = clause.match(/\{([\s\S]*?)\}/);
1194
+ const namedRaw = namedBlock ? namedBlock[1] : "";
1195
+ let defaultName;
1196
+ if (namedBlock) {
1197
+ const before = clause.slice(0, namedBlock.index).replace(/,\s*$/, "").trim();
1198
+ if (/^[A-Za-z_$][\w$]*$/.test(before)) defaultName = before;
1199
+ } else if (/^[A-Za-z_$][\w$]*$/.test(clause)) {
1200
+ defaultName = clause;
1201
+ }
1202
+ if (defaultName) {
1203
+ info.imports.set(defaultName, resolved || spec);
1204
+ info.importsDefault.set(defaultName, resolved || spec);
1205
+ }
1206
+ for (const raw of namedRaw.split(",")) {
1207
+ const pair = raw.trim();
1208
+ if (!pair) continue;
1209
+ const parts = pair.match(/^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/);
1210
+ if (parts) info.imports.set(parts[2] || parts[1], resolved || spec);
1211
+ }
1212
+ continue;
1213
+ }
1214
+ const nsImport = stmt.match(/^import\s*\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+["']([^"']+)["']/);
1215
+ if (nsImport) {
1216
+ const resolved = this.resolveModule(nsImport[2], info.filePath);
1217
+ info.imports.set(nsImport[1], resolved || nsImport[2]);
1218
+ continue;
1219
+ }
1220
+ const constStmt = stmt.match(/^(?:export\s+|declare\s+)?const\s+([\s\S]+?)=\s*([\s\S]+)$/);
1221
+ if (constStmt) {
1222
+ const lhs = constStmt[1].trim();
1223
+ const rawValue = constStmt[2].trim().replace(/;\s*$/, "");
1224
+ if (lhs.startsWith("{") || lhs.startsWith("[")) {
1225
+ const names = [...lhs.matchAll(/([A-Za-z_$][\w$]*)(?:\s*:\s*([A-Za-z_$][\w$]*))?/g)];
1226
+ for (const name of names) {
1227
+ const sourceProp = name[1];
1228
+ const target = name[2] || sourceProp;
1229
+ info.destructure.set(target, `${rawValue}.${sourceProp}`);
1230
+ }
1231
+ } else if (/^[A-Za-z_$][\w$]*$/.test(lhs)) {
1232
+ info.consts.set(lhs, rawValue);
1233
+ }
1234
+ continue;
1235
+ }
1236
+ const exportMatch = stmt.match(/^export\s+default\s+([\s\S]+)$/);
1237
+ if (exportMatch) {
1238
+ info.defaultRaw = exportMatch[1].trim().replace(/;\s*$/, "");
1239
+ }
1240
+ }
1241
+ }
1242
+ /**
1243
+ * Looks up an identifier's raw expression within `file`, following imports,
1244
+ * module destructuring, and default-export bindings.
1245
+ */
1246
+ resolveIdentifier(file, name) {
1247
+ const info = this.getModule(file);
1248
+ if (!info) return void 0;
1249
+ if (info.destructure.has(name)) return info.destructure.get(name);
1250
+ if (info.consts.has(name)) return info.consts.get(name);
1251
+ const importedFrom = info.imports.get(name);
1252
+ if (!importedFrom) return void 0;
1253
+ const targetModule = this.getModule(importedFrom);
1254
+ if (!targetModule) return void 0;
1255
+ if (targetModule.consts.has(name)) return targetModule.consts.get(name);
1256
+ if (info.importsDefault.has(name) && targetModule.defaultRaw !== void 0) {
1257
+ return targetModule.defaultRaw;
1258
+ }
1259
+ return void 0;
1260
+ }
1261
+ /** Folds an expression string in the context of `file`. */
1262
+ foldExpression(file, raw) {
1263
+ let node;
1264
+ try {
1265
+ node = parseValue(raw);
1266
+ } catch {
1267
+ return { ok: false, unresolved: [raw.trim().slice(0, 80)] };
1268
+ }
1269
+ const state = { ok: true };
1270
+ const value = this.foldNode(file, node, /* @__PURE__ */ new Set(), state);
1271
+ if (!state.ok) {
1272
+ return { ok: false, value, unresolved: state.unresolved };
1273
+ }
1274
+ return { ok: true, value };
1275
+ }
1276
+ foldNode(file, node, seen, state) {
1277
+ switch (node.t) {
1278
+ case "str":
1279
+ return node.value;
1280
+ case "num":
1281
+ return node.value;
1282
+ case "bool":
1283
+ return node.value;
1284
+ case "template": {
1285
+ let out = "";
1286
+ for (const part of node.parts) {
1287
+ if (part.t === "str") {
1288
+ out += part.value;
1289
+ continue;
1290
+ }
1291
+ const folded = this.foldNode(file, part, seen, state);
1292
+ if (typeof folded === "string") out += folded;
1293
+ else if (typeof folded === "number" || typeof folded === "boolean") out += String(folded);
1294
+ else {
1295
+ state.ok = false;
1296
+ (state.unresolved = state.unresolved || []).push(node.raw.slice(0, 60));
1297
+ return void 0;
1298
+ }
1299
+ }
1300
+ return out;
1301
+ }
1302
+ case "concat": {
1303
+ let out = "";
1304
+ for (const part of node.parts) {
1305
+ const folded = this.foldNode(file, part, seen, state);
1306
+ if (typeof folded === "string") out += folded;
1307
+ else if (typeof folded === "number" || typeof folded === "boolean") out += String(folded);
1308
+ else {
1309
+ state.ok = false;
1310
+ (state.unresolved = state.unresolved || []).push("concat");
1311
+ return void 0;
1312
+ }
1313
+ }
1314
+ return out;
1315
+ }
1316
+ case "new-url": {
1317
+ const folded = this.foldNode(file, node.arg, seen, state);
1318
+ return typeof folded === "string" ? folded : (state.ok = false, void 0);
1319
+ }
1320
+ case "ident": {
1321
+ if (node.name === "undefined") return void 0;
1322
+ if (seen.has(node.name)) {
1323
+ state.ok = false;
1324
+ (state.unresolved = state.unresolved || []).push(node.name);
1325
+ return void 0;
1326
+ }
1327
+ const raw = this.resolveIdentifier(file, node.name);
1328
+ if (raw === void 0) {
1329
+ state.ok = false;
1330
+ (state.unresolved = state.unresolved || []).push(node.name);
1331
+ return void 0;
1332
+ }
1333
+ const nextSeen = new Set(seen);
1334
+ nextSeen.add(node.name);
1335
+ let sub;
1336
+ try {
1337
+ sub = parseValue(raw);
1338
+ } catch {
1339
+ state.ok = false;
1340
+ return void 0;
1341
+ }
1342
+ return this.foldNode(file, sub, nextSeen, state);
1343
+ }
1344
+ case "member": {
1345
+ const prevOk = state.ok;
1346
+ const prevUnresolved = state.unresolved ? state.unresolved.slice() : [];
1347
+ const object = this.foldNode(file, node.object, seen, state);
1348
+ const isRecord = object !== null && object !== void 0 && typeof object === "object" && !Array.isArray(object);
1349
+ if (isRecord) {
1350
+ const record = object;
1351
+ if (node.prop in record && record[node.prop] !== void 0) {
1352
+ if (!state.ok) {
1353
+ state.ok = true;
1354
+ if (state.unresolved) state.unresolved = prevUnresolved;
1355
+ }
1356
+ return record[node.prop];
1357
+ }
1358
+ }
1359
+ state.ok = prevOk;
1360
+ state.unresolved = prevUnresolved;
1361
+ (state.unresolved = state.unresolved || []).push(node.object.t === "ident" ? `${node.object.name}.${node.prop}` : `.${node.prop}`);
1362
+ return void 0;
1363
+ }
1364
+ case "arr": {
1365
+ const items = [];
1366
+ for (const item of node.items) {
1367
+ items.push(this.foldNode(file, item, seen, state));
1368
+ }
1369
+ return items;
1370
+ }
1371
+ case "obj": {
1372
+ const record = {};
1373
+ for (const prop2 of node.props) {
1374
+ record[prop2.key] = this.foldNode(file, prop2.value, seen, state);
1375
+ }
1376
+ return record;
1377
+ }
1378
+ case "unresolved":
1379
+ state.ok = false;
1380
+ (state.unresolved = state.unresolved || []).push(node.raw.slice(0, 60));
1381
+ return void 0;
1382
+ }
1383
+ }
1384
+ /** Reads raw const initializer by name in file (for generateStaticParams etc.). */
1385
+ rawConst(file, name) {
1386
+ return this.resolveIdentifier(file, name);
1387
+ }
1388
+ /** Reads the default export expression of a module. */
1389
+ defaultExport(file) {
1390
+ return this.getModule(file)?.defaultRaw;
1391
+ }
1392
+ /**
1393
+ * Best-effort guess of the production origin (scheme + host) by scanning
1394
+ * common configuration modules and the root layout for an http(s) URL that
1395
+ * is clearly not a placeholder.
1396
+ */
1397
+ guessOrigin(knownUrls = []) {
1398
+ if (this.origin) return this.origin;
1399
+ const candidates = [...knownUrls];
1400
+ const filesToScan = [
1401
+ path2.join(this.projectRoot, "config", "site.ts"),
1402
+ path2.join(this.projectRoot, "config", "site.tsx"),
1403
+ path2.join(this.projectRoot, "lib", "site.ts"),
1404
+ path2.join(this.projectRoot, "src", "config", "site.ts"),
1405
+ path2.join(this.projectRoot, "src", "config", "site.tsx"),
1406
+ path2.join(this.projectRoot, "src", "lib", "site.ts"),
1407
+ path2.join(this.projectRoot, "src", "app", "layout.tsx"),
1408
+ path2.join(this.projectRoot, "app", "layout.tsx"),
1409
+ path2.join(this.projectRoot, "package.json")
1410
+ ];
1411
+ for (const file of filesToScan) {
1412
+ const content = this.readFile(file);
1413
+ if (!content) continue;
1414
+ for (const m of content.matchAll(/https?:\/\/[A-Za-z0-9.-]+(?::\d+)?/g)) {
1415
+ candidates.push(m[0]);
1416
+ }
1417
+ }
1418
+ const preferred = candidates.find(
1419
+ (u) => /^https:/.test(u) && !/example\.|localhost|127\.0\.0\.1|test\.|\.invalid/i.test(u)
1420
+ );
1421
+ const fallback = candidates.find((u) => !/example\.|localhost|127\.0\.0\.1|test\.|\.invalid/i.test(u));
1422
+ const chosen = preferred || fallback;
1423
+ if (chosen) {
1424
+ try {
1425
+ this.origin = new URL(chosen).origin;
1426
+ } catch {
1427
+ this.origin = void 0;
1428
+ }
1429
+ }
1430
+ return this.origin;
1431
+ }
1432
+ /** Parses a project file into an AST-slim statement list (for heading discovery). */
1433
+ readModuleFile(filePath) {
1434
+ const info = this.getModule(filePath);
1435
+ return info?.content;
1436
+ }
1437
+ };
1438
+
1439
+ // ../next-adapter/src/enrich.ts
1440
+ function stringLiterals(text) {
1441
+ const out = [];
1442
+ for (const m of text.matchAll(/["'`]([^"'`\n]{1,200})["'`]/g)) out.push(m[1]);
1443
+ return out;
1444
+ }
1445
+ function headingRegexFor(level) {
1446
+ return new RegExp(`<h${level}\\b[^>]*>([\\s\\S]*?)</h${level}>`, "gi");
1447
+ }
1448
+ function stripTags(text) {
1449
+ return text.replace(/<[^>]+>/g, "").replace(/\{[^{}]*\}/g, " ").replace(/\{\{[\s\S]*?\}\}/g, " ").replace(/\s+/g, " ").trim();
1450
+ }
1451
+ function lineAt2(text, index) {
1452
+ return text.slice(0, index).split("\n").length;
1453
+ }
1454
+ function collectInlineHeadings(content) {
1455
+ const headings = [];
1456
+ for (let level = 1; level <= 6; level += 1) {
1457
+ const re = headingRegexFor(level);
1458
+ for (const match of content.matchAll(re)) {
1459
+ headings.push({ level, text: stripTags(match[1]), line: lineAt2(content, match.index || 0) });
1460
+ }
1461
+ }
1462
+ return headings.sort((a, b) => (a.line || 0) - (b.line || 0));
1463
+ }
1464
+ function localComponentImports(content, fromFile, resolver) {
1465
+ const files = [];
1466
+ for (const m of content.matchAll(/import[\s\S]*?from\s+["']([^"']+)["']/g)) {
1467
+ const spec = m[1];
1468
+ const resolved = resolver.resolveModule(spec, fromFile);
1469
+ if (resolved) files.push(resolved);
1470
+ }
1471
+ return files;
1472
+ }
1473
+ function fileHasFlag(content, flag) {
1474
+ const re = new RegExp(`\\b${flag}\\s*=\\s*false\\b`);
1475
+ return re.test(content);
1476
+ }
1477
+ var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1478
+ function addSlugCandidates(params, values) {
1479
+ for (const literal of values) {
1480
+ if (SLUG_RE.test(literal) && literal.length <= 64) params.add(literal);
1481
+ }
1482
+ }
1483
+ function dynamicParamsForFile(filePath, resolver) {
1484
+ const content = resolver.readModuleFile(filePath);
1485
+ if (content === void 0) return { dynamicParams: void 0, generatedParams: [] };
1486
+ const dynamicParams = fileHasFlag(content, "dynamicParams") ? false : void 0;
1487
+ const params = /* @__PURE__ */ new Set();
1488
+ const fnIdx = content.search(/generateStaticParams\s*\(/);
1489
+ if (fnIdx !== -1) {
1490
+ const bodyStart = content.indexOf("{", fnIdx);
1491
+ if (bodyStart !== -1) {
1492
+ const bodyEnd = readBalanced(content, bodyStart);
1493
+ const body = content.slice(bodyStart, bodyEnd);
1494
+ addSlugCandidates(params, stringLiterals(body));
1495
+ }
1496
+ }
1497
+ const visitedFiles = /* @__PURE__ */ new Set([filePath]);
1498
+ const queue = [];
1499
+ const enqueueFile = (file, depth) => {
1500
+ if (!visitedFiles.has(file) && depth <= 4) queue.push({ file, depth });
1501
+ };
1502
+ for (const m of content.matchAll(/import[\s\S]*?from\s+["']([^"']+)["']/g)) {
1503
+ const resolved = resolver.resolveModule(m[1], filePath);
1504
+ if (resolved) enqueueFile(resolved, 1);
1505
+ }
1506
+ let budget = 400;
1507
+ while (queue.length > 0 && budget > 0) {
1508
+ budget -= 1;
1509
+ const { file, depth } = queue.shift();
1510
+ if (visitedFiles.has(file)) continue;
1511
+ visitedFiles.add(file);
1512
+ const nextContent = resolver.readModuleFile(file);
1513
+ if (nextContent === void 0) continue;
1514
+ addSlugCandidates(params, stringLiterals(nextContent));
1515
+ for (const match of nextContent.matchAll(/(?:^|\n)\s*(?:export\s+)?const\s+[A-Za-z_$][\w$]*\s*(?::[^=\n]+)?=\s*\[/g)) {
1516
+ const open = nextContent.indexOf("[", match.index);
1517
+ if (open === -1) continue;
1518
+ const end = readBalanced(nextContent, open);
1519
+ const arrBody = nextContent.slice(open, end + 1);
1520
+ addSlugCandidates(params, stringLiterals(arrBody));
1521
+ }
1522
+ for (const match of nextContent.matchAll(/(?:\bslug\s*|\bid\s*|\bkind\s*|\bhref\s*)\s*:\s*["']([^"'\n]+)["']/g)) {
1523
+ addSlugCandidates(params, [match[1]]);
1524
+ }
1525
+ if (depth < 4) {
1526
+ for (const m of nextContent.matchAll(/import[\s\S]*?from\s+["']([^"']+)["']/g)) {
1527
+ const resolved = resolver.resolveModule(m[1], file);
1528
+ if (resolved) enqueueFile(resolved, depth + 1);
1529
+ }
1530
+ }
1531
+ }
1532
+ return { dynamicParams, generatedParams: Array.from(params) };
1533
+ }
1534
+ function discoverImportedH1(filePath, resolver, maxDepth = 3) {
1535
+ const content = resolver.readModuleFile(filePath);
1536
+ if (content === void 0) return [];
1537
+ const visited = /* @__PURE__ */ new Set([filePath]);
1538
+ const result = [];
1539
+ const queue = localComponentImports(content, filePath, resolver).map(
1540
+ (file) => ({ file, depth: 1 })
1541
+ );
1542
+ while (queue.length > 0) {
1543
+ const { file, depth } = queue.shift();
1544
+ if (depth > maxDepth || visited.has(file)) continue;
1545
+ visited.add(file);
1546
+ const nextContent = resolver.readModuleFile(file);
1547
+ if (nextContent === void 0) continue;
1548
+ const h1s = collectInlineHeadings(nextContent).filter((h) => h.level === 1);
1549
+ if (h1s.length > 0) {
1550
+ result.push(...h1s);
1551
+ continue;
1552
+ }
1553
+ for (const next of localComponentImports(nextContent, file, resolver)) {
1554
+ queue.push({ file: next, depth: depth + 1 });
1555
+ }
1556
+ }
1557
+ return result;
1558
+ }
1559
+
377
1560
  // ../next-adapter/src/scanner.ts
378
1561
  function normalizeRouteGroup(segment) {
379
1562
  return segment.startsWith("(") && segment.endsWith(")") ? "" : segment;
@@ -383,39 +1566,39 @@ var NextJsAdapter = class {
383
1566
  * Detects if the given directory contains a Next.js application.
384
1567
  */
385
1568
  static async detect(projectRoot) {
386
- const pkgPath = path2.join(projectRoot, "package.json");
387
- if (fs2.existsSync(pkgPath)) {
1569
+ const pkgPath = path3.join(projectRoot, "package.json");
1570
+ if (fs3.existsSync(pkgPath)) {
388
1571
  try {
389
- const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
1572
+ const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf8"));
390
1573
  const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
391
1574
  if (deps.next) return true;
392
1575
  } catch {
393
1576
  }
394
1577
  }
395
- return fs2.existsSync(path2.join(projectRoot, "app")) || fs2.existsSync(path2.join(projectRoot, "src", "app")) || fs2.existsSync(path2.join(projectRoot, "pages")) || fs2.existsSync(path2.join(projectRoot, "src", "pages")) || fs2.existsSync(path2.join(projectRoot, "next.config.js")) || fs2.existsSync(path2.join(projectRoot, "next.config.mjs"));
1578
+ return fs3.existsSync(path3.join(projectRoot, "app")) || fs3.existsSync(path3.join(projectRoot, "src", "app")) || fs3.existsSync(path3.join(projectRoot, "pages")) || fs3.existsSync(path3.join(projectRoot, "src", "pages")) || fs3.existsSync(path3.join(projectRoot, "next.config.js")) || fs3.existsSync(path3.join(projectRoot, "next.config.mjs"));
396
1579
  }
397
1580
  /**
398
1581
  * Loads optional seo.config.ts or returns default configuration.
399
1582
  */
400
1583
  static async loadConfig(projectRoot) {
401
1584
  const configCandidates = [
402
- path2.join(projectRoot, "crawlemon.config.ts"),
403
- path2.join(projectRoot, "crawlemon.config.js"),
404
- path2.join(projectRoot, "crawlemon.config.mjs"),
405
- path2.join(projectRoot, "crawlemon.config.json"),
406
- path2.join(projectRoot, "crawlemon.yml"),
407
- path2.join(projectRoot, "crawlemon.yaml"),
408
- path2.join(projectRoot, "seo.config.ts"),
409
- path2.join(projectRoot, "seo.config.js"),
410
- path2.join(projectRoot, "seo.config.json")
1585
+ path3.join(projectRoot, "crawlemon.config.ts"),
1586
+ path3.join(projectRoot, "crawlemon.config.js"),
1587
+ path3.join(projectRoot, "crawlemon.config.mjs"),
1588
+ path3.join(projectRoot, "crawlemon.config.json"),
1589
+ path3.join(projectRoot, "crawlemon.yml"),
1590
+ path3.join(projectRoot, "crawlemon.yaml"),
1591
+ path3.join(projectRoot, "seo.config.ts"),
1592
+ path3.join(projectRoot, "seo.config.js"),
1593
+ path3.join(projectRoot, "seo.config.json")
411
1594
  ];
412
1595
  for (const candidate of configCandidates) {
413
- if (fs2.existsSync(candidate)) {
1596
+ if (fs3.existsSync(candidate)) {
414
1597
  try {
415
1598
  if (candidate.endsWith(".json")) {
416
- return JSON.parse(fs2.readFileSync(candidate, "utf8"));
1599
+ return JSON.parse(fs3.readFileSync(candidate, "utf8"));
417
1600
  }
418
- const content = fs2.readFileSync(candidate, "utf8");
1601
+ const content = fs3.readFileSync(candidate, "utf8");
419
1602
  const siteUrlMatch = content.match(/siteUrl\s*:\s*["']([^"']+)["']/);
420
1603
  const ignoreMatch = content.match(/ignore\s*:\s*\[([\s\S]*?)\]/);
421
1604
  const ignore = ignoreMatch ? Array.from(ignoreMatch[1].matchAll(/["']([^"']+)["']/g), (match) => match[1]) : void 0;
@@ -463,61 +1646,71 @@ var NextJsAdapter = class {
463
1646
  static async scan(projectRoot) {
464
1647
  const isNext = await this.detect(projectRoot);
465
1648
  const config = await this.loadConfig(projectRoot);
466
- let appDir = path2.join(projectRoot, "app");
467
- if (!fs2.existsSync(appDir) && fs2.existsSync(path2.join(projectRoot, "src", "app"))) {
468
- appDir = path2.join(projectRoot, "src", "app");
1649
+ let appDir = path3.join(projectRoot, "app");
1650
+ if (!fs3.existsSync(appDir) && fs3.existsSync(path3.join(projectRoot, "src", "app"))) {
1651
+ appDir = path3.join(projectRoot, "src", "app");
469
1652
  }
470
- let pagesDir = path2.join(projectRoot, "pages");
471
- if (!fs2.existsSync(pagesDir) && fs2.existsSync(path2.join(projectRoot, "src", "pages"))) {
472
- pagesDir = path2.join(projectRoot, "src", "pages");
1653
+ let pagesDir = path3.join(projectRoot, "pages");
1654
+ if (!fs3.existsSync(pagesDir) && fs3.existsSync(path3.join(projectRoot, "src", "pages"))) {
1655
+ pagesDir = path3.join(projectRoot, "src", "pages");
473
1656
  }
474
- const isAppRouter = fs2.existsSync(appDir);
1657
+ const isAppRouter = fs3.existsSync(appDir);
475
1658
  const routes = [];
476
1659
  const redirects = [];
477
1660
  for (const configName of ["next.config.js", "next.config.mjs", "next.config.ts"]) {
478
- const configPath = path2.join(projectRoot, configName);
479
- if (!fs2.existsSync(configPath)) continue;
480
- const content = fs2.readFileSync(configPath, "utf8");
1661
+ const configPath = path3.join(projectRoot, configName);
1662
+ if (!fs3.existsSync(configPath)) continue;
1663
+ const content = fs3.readFileSync(configPath, "utf8");
481
1664
  for (const match of content.matchAll(/source\s*:\s*["'`]([^"'`]+)["'`][\s\S]{0,500}?destination\s*:\s*["'`]([^"'`]+)["'`][\s\S]{0,200}?permanent\s*:\s*(true|false)/g)) {
482
1665
  redirects.push({ source: match[1], destination: match[2], permanent: match[3] === "true" });
483
1666
  }
484
1667
  break;
485
1668
  }
486
- const dependencyGraph = isAppRouter ? new RouteDependencyGraph(projectRoot, appDir) : void 0;
1669
+ const resolver = isAppRouter ? new ProjectResolver(projectRoot) : void 0;
1670
+ const origin = resolveProjectOrigin(config, resolver);
1671
+ const dependencyGraph = isAppRouter ? new RouteDependencyGraph(projectRoot, appDir, resolver, origin) : void 0;
487
1672
  if (dependencyGraph) {
488
1673
  dependencyGraph.indexLayouts();
489
1674
  }
490
1675
  if (isAppRouter) {
491
1676
  const scanAppDir = (currentDir, relativePath = "") => {
492
- const entries = fs2.readdirSync(currentDir, { withFileTypes: true });
1677
+ const entries = fs3.readdirSync(currentDir, { withFileTypes: true });
493
1678
  for (const entry of entries) {
494
- const fullPath = path2.join(currentDir, entry.name);
1679
+ const fullPath = path3.join(currentDir, entry.name);
495
1680
  if (entry.isDirectory()) {
496
1681
  if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "api") {
497
1682
  continue;
498
1683
  }
499
1684
  const normalized = normalizeRouteGroup(entry.name);
500
- const nextRelative = normalized ? path2.join(relativePath, normalized) : relativePath;
1685
+ const nextRelative = normalized ? path3.join(relativePath, normalized) : relativePath;
501
1686
  scanAppDir(fullPath, nextRelative);
502
1687
  } else if (entry.isFile()) {
503
1688
  if (/^page\.(tsx|jsx|js|ts)$/.test(entry.name)) {
504
1689
  const routePath = relativePath === "" ? "/" : `/${relativePath.replace(/\\/g, "/")}`;
505
- const content = fs2.readFileSync(fullPath, "utf8");
1690
+ const content = fs3.readFileSync(fullPath, "utf8");
506
1691
  const parsed = parsePageSource(content);
507
1692
  const inheritedMetadata = dependencyGraph ? dependencyGraph.getInheritedMetadata(fullPath) : {};
1693
+ let metadata = resolver ? resolvePageMetadata(content, fullPath, inheritedMetadata, { resolver, origin, file: fullPath }) : { ...inheritedMetadata, ...parsed.metadata };
1694
+ if (parsed.metadata.hasConflictingDeclarations) metadata.hasConflictingDeclarations = true;
1695
+ if (parsed.metadata.jsonLd) metadata.jsonLd = parsed.metadata.jsonLd;
1696
+ const headings = [...parsed.headings];
1697
+ if (!headings.some((h) => h.level === 1) && resolver) {
1698
+ headings.push(...discoverImportedH1(fullPath, resolver));
1699
+ }
1700
+ const dynamic = resolver ? dynamicParamsForFile(fullPath, resolver) : void 0;
508
1701
  routes.push({
509
1702
  route: routePath,
510
1703
  filePath: fullPath,
511
- metadata: {
512
- ...inheritedMetadata,
513
- ...parsed.metadata
514
- },
515
- headings: parsed.headings,
1704
+ metadata,
1705
+ headings,
516
1706
  images: parsed.images,
517
1707
  links: parsed.links,
518
1708
  textContent: parsed.textContent,
519
1709
  hasLittleContent: parsed.hasLittleContent,
520
- hasDynamicSegments: routePath.includes("[")
1710
+ hasDynamicSegments: routePath.includes("["),
1711
+ dynamicParams: dynamic?.dynamicParams,
1712
+ generatedParams: dynamic?.generatedParams,
1713
+ isRedirect: isRedirectOnly(content)
521
1714
  });
522
1715
  }
523
1716
  }
@@ -525,25 +1718,25 @@ var NextJsAdapter = class {
525
1718
  };
526
1719
  scanAppDir(appDir);
527
1720
  }
528
- if (fs2.existsSync(pagesDir)) {
1721
+ if (fs3.existsSync(pagesDir)) {
529
1722
  const scanPagesDir = (currentDir, relativePath = "") => {
530
- const entries = fs2.readdirSync(currentDir, { withFileTypes: true });
1723
+ const entries = fs3.readdirSync(currentDir, { withFileTypes: true });
531
1724
  for (const entry of entries) {
532
- const fullPath = path2.join(currentDir, entry.name);
1725
+ const fullPath = path3.join(currentDir, entry.name);
533
1726
  if (entry.isDirectory()) {
534
1727
  if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "api") {
535
1728
  continue;
536
1729
  }
537
- scanPagesDir(fullPath, path2.join(relativePath, entry.name));
1730
+ scanPagesDir(fullPath, path3.join(relativePath, entry.name));
538
1731
  } else if (entry.isFile()) {
539
1732
  if (/\.(tsx|jsx|js)$/.test(entry.name)) {
540
1733
  const baseName = entry.name.replace(/\.(tsx|jsx|js)$/, "");
541
1734
  if (baseName.startsWith("_") || baseName === "api") {
542
1735
  continue;
543
1736
  }
544
- let routePath = `/${path2.join(relativePath, baseName === "index" ? "" : baseName).replace(/\\/g, "/")}`;
1737
+ let routePath = `/${path3.join(relativePath, baseName === "index" ? "" : baseName).replace(/\\/g, "/")}`;
545
1738
  if (routePath === "//" || routePath === "") routePath = "/";
546
- const content = fs2.readFileSync(fullPath, "utf8");
1739
+ const content = fs3.readFileSync(fullPath, "utf8");
547
1740
  const parsed = parsePageSource(content);
548
1741
  routes.push({
549
1742
  route: routePath,
@@ -563,36 +1756,36 @@ var NextJsAdapter = class {
563
1756
  scanPagesDir(pagesDir);
564
1757
  }
565
1758
  const sitemapCandidates = [
566
- path2.join(appDir, "sitemap.ts"),
567
- path2.join(appDir, "sitemap.js"),
568
- path2.join(projectRoot, "public", "sitemap.xml")
1759
+ path3.join(appDir, "sitemap.ts"),
1760
+ path3.join(appDir, "sitemap.js"),
1761
+ path3.join(projectRoot, "public", "sitemap.xml")
569
1762
  ];
570
1763
  let sitemapFound = false;
571
1764
  let sitemapMalformed = false;
572
1765
  const sitemapUrls = [];
573
1766
  for (const candidate of sitemapCandidates) {
574
- if (fs2.existsSync(candidate)) {
1767
+ if (fs3.existsSync(candidate)) {
575
1768
  sitemapFound = true;
576
- const content = fs2.readFileSync(candidate, "utf8");
1769
+ const content = fs3.readFileSync(candidate, "utf8");
577
1770
  if (candidate.endsWith(".xml") && (!/<urlset\b/i.test(content) || !/<loc>[^<]+<\/loc>/i.test(content))) {
578
1771
  sitemapMalformed = true;
579
1772
  }
580
- const urlMatches = content.matchAll(/url\s*:\s*['"`]([^'"`]+)['"`]|<loc>([^<]+)<\/loc>/gi);
581
- for (const m of urlMatches) {
582
- const url = m[1] || m[2];
583
- if (url) sitemapUrls.push(url.trim());
1773
+ if (candidate.endsWith(".xml")) {
1774
+ for (const m of content.matchAll(/<loc>([^<]+)<\/loc>/gi)) sitemapUrls.push(m[1].trim());
1775
+ } else {
1776
+ resolveTsSitemapUrls(content, candidate, resolver, sitemapUrls, origin);
584
1777
  }
585
1778
  break;
586
1779
  }
587
1780
  }
588
1781
  const robotsCandidates = [
589
- path2.join(appDir, "robots.ts"),
590
- path2.join(appDir, "robots.js"),
591
- path2.join(projectRoot, "public", "robots.txt")
1782
+ path3.join(appDir, "robots.ts"),
1783
+ path3.join(appDir, "robots.js"),
1784
+ path3.join(projectRoot, "public", "robots.txt")
592
1785
  ];
593
- const robotsFile = robotsCandidates.find((candidate) => fs2.existsSync(candidate));
1786
+ const robotsFile = robotsCandidates.find((candidate) => fs3.existsSync(candidate));
594
1787
  const robotsFound = Boolean(robotsFile);
595
- const robotsContent = robotsFile ? fs2.readFileSync(robotsFile, "utf8") : "";
1788
+ const robotsContent = robotsFile ? fs3.readFileSync(robotsFile, "utf8") : "";
596
1789
  routes.sort((a, b) => a.route.localeCompare(b.route));
597
1790
  if (dependencyGraph) {
598
1791
  dependencyGraph.setRoutes(routes);
@@ -613,10 +1806,64 @@ var NextJsAdapter = class {
613
1806
  };
614
1807
  }
615
1808
  };
1809
+ function resolveProjectOrigin(config, resolver) {
1810
+ if (config.siteUrl) {
1811
+ try {
1812
+ const url = new URL(config.siteUrl);
1813
+ if (url.protocol === "http:" || url.protocol === "https:") return url.origin;
1814
+ } catch {
1815
+ }
1816
+ }
1817
+ return resolver ? resolver.guessOrigin(config.siteUrl ? [config.siteUrl] : []) : void 0;
1818
+ }
1819
+ function isRedirectOnly(content) {
1820
+ const usesNextNavigation = /from\s+["']next\/navigation["']/.test(content);
1821
+ if (!usesNextNavigation) return false;
1822
+ const hasRedirectCall = /\b(redirect|permanentRedirect)\s*\(/.test(content);
1823
+ if (!hasRedirectCall) return false;
1824
+ return !/<[A-Za-z]/.test(content);
1825
+ }
1826
+ function resolveTsSitemapUrls(content, file, resolver, out, origin) {
1827
+ const foldWithOrigin = (raw) => {
1828
+ if (!resolver) return void 0;
1829
+ const direct = resolver.foldExpression(file, raw);
1830
+ if (direct.ok && typeof direct.value === "string" && direct.value) return direct.value;
1831
+ if (!origin) return void 0;
1832
+ const substituted = raw.replace(/\$\{\s*([A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*)*)\s*\}/g, (whole, inner) => {
1833
+ const last = inner.split(/\.|\s/).pop() || "";
1834
+ if (last === "url" || last === "origin" || last === "URL" || /^siteUrl$/i.test(inner)) return origin;
1835
+ return whole;
1836
+ }).replace(/\b(?:siteConfig|siteURL|siteUrl)\.url\b/g, origin);
1837
+ const retry = resolver.foldExpression(file, substituted);
1838
+ if (retry.ok && typeof retry.value === "string" && retry.value) return retry.value;
1839
+ return void 0;
1840
+ };
1841
+ const pushExpressionUrl = (start) => {
1842
+ const i = skipTrivia(content, start);
1843
+ if (i >= content.length) return;
1844
+ const { end } = parseValueAt(content, i);
1845
+ if (end <= i) return;
1846
+ const raw = content.slice(i, end).trim();
1847
+ const value = foldWithOrigin(raw);
1848
+ if (value) out.push(value);
1849
+ };
1850
+ let search = 0;
1851
+ while (search < content.length) {
1852
+ const idx = content.indexOf("url:", search);
1853
+ if (idx === -1) break;
1854
+ const before = content.slice(Math.max(0, idx - 1), idx);
1855
+ if (!/[{,:\s]/.test(before) || /[A-Za-z0-9_$]/.test(before)) {
1856
+ search = idx + 1;
1857
+ continue;
1858
+ }
1859
+ pushExpressionUrl(idx + "url:".length);
1860
+ search = idx + "url:".length;
1861
+ }
1862
+ }
616
1863
 
617
1864
  // ../next-adapter/src/framework-adapter.ts
618
- import fs3 from "node:fs";
619
- import path3 from "node:path";
1865
+ import fs4 from "node:fs";
1866
+ import path4 from "node:path";
620
1867
  var INHERITED_METADATA_FIELDS = ["title", "description", "canonical", "robots"];
621
1868
  var LABELS = {
622
1869
  nextjs: "Next.js",
@@ -629,23 +1876,23 @@ var LABELS = {
629
1876
  };
630
1877
  function dependencies(root) {
631
1878
  try {
632
- const pkg = JSON.parse(fs3.readFileSync(path3.join(root, "package.json"), "utf8"));
1879
+ const pkg = JSON.parse(fs4.readFileSync(path4.join(root, "package.json"), "utf8"));
633
1880
  return { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
634
1881
  } catch {
635
1882
  return {};
636
1883
  }
637
1884
  }
638
1885
  function walk(dir, visit) {
639
- if (!fs3.existsSync(dir)) return;
640
- for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
1886
+ if (!fs4.existsSync(dir)) return;
1887
+ for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
641
1888
  if (entry.name.startsWith(".") || ["node_modules", "dist", "build", ".output"].includes(entry.name)) continue;
642
- const full = path3.join(dir, entry.name);
1889
+ const full = path4.join(dir, entry.name);
643
1890
  if (entry.isDirectory()) walk(full, visit);
644
1891
  else if (entry.isFile()) visit(full);
645
1892
  }
646
1893
  }
647
1894
  function routeNode(file, route, framework) {
648
- const parsed = parsePageSource(fs3.readFileSync(file, "utf8"), framework);
1895
+ const parsed = parsePageSource(fs4.readFileSync(file, "utf8"), framework);
649
1896
  return { route, filePath: file, ...parsed, hasDynamicSegments: /[:[*]/.test(route) };
650
1897
  }
651
1898
  function cleanRoute(value) {
@@ -657,27 +1904,27 @@ function discoverRoutes(root, framework) {
657
1904
  const routes = [];
658
1905
  const add = (base, extensions, convert) => {
659
1906
  walk(base, (file) => {
660
- const relative = path3.relative(base, file);
1907
+ const relative = path4.relative(base, file);
661
1908
  if (!extensions.test(relative) || /(^|\/)api(\/|\.|$)/.test(relative)) return;
662
1909
  routes.push(routeNode(file, convert(relative), framework));
663
1910
  });
664
1911
  };
665
1912
  if (framework === "nuxt") {
666
- const base = fs3.existsSync(path3.join(root, "pages")) ? path3.join(root, "pages") : path3.join(root, "app", "pages");
1913
+ const base = fs4.existsSync(path4.join(root, "pages")) ? path4.join(root, "pages") : path4.join(root, "app", "pages");
667
1914
  add(base, /\.vue$/, (r) => cleanRoute(r.replace(/\.vue$/, "").replace(/\[\.\.\.([^\]]+)\]/g, "*$1").replace(/\[([^\]]+)\]/g, ":$1")));
668
1915
  } else if (framework === "sveltekit") {
669
- const base = path3.join(root, "src", "routes");
1916
+ const base = path4.join(root, "src", "routes");
670
1917
  add(base, /(^|\/)\+page\.svelte$/, (r) => cleanRoute(r.replace(/\/\+page\.svelte$/, "").replace(/^\+page\.svelte$/, "")));
671
1918
  } else if (framework === "astro") {
672
- const base = path3.join(root, "src", "pages");
1919
+ const base = path4.join(root, "src", "pages");
673
1920
  add(base, /\.(astro|md|mdx)$/, (r) => cleanRoute(r.replace(/\.(astro|md|mdx)$/, "")));
674
1921
  } else if (framework === "remix") {
675
- const base = path3.join(root, "app", "routes");
1922
+ const base = path4.join(root, "app", "routes");
676
1923
  add(base, /\.(tsx|jsx|ts|js)$/, (r) => cleanRoute(r.replace(/\.(tsx|jsx|ts|js)$/, "").replace(/\._index$/, "").replace(/^_index$/, "").replace(/\./g, "/").replace(/\$([^/]+)/g, ":$1")));
677
1924
  } else if (framework === "vite") {
678
1925
  for (const candidate of ["src/App.tsx", "src/App.jsx", "src/App.vue", "src/App.svelte", "index.html"]) {
679
- const file = path3.join(root, candidate);
680
- if (fs3.existsSync(file)) routes.push(routeNode(file, "/", framework));
1926
+ const file = path4.join(root, candidate);
1927
+ if (fs4.existsSync(file)) routes.push(routeNode(file, "/", framework));
681
1928
  }
682
1929
  } else {
683
1930
  add(root, /\.html?$/, (r) => cleanRoute(r.replace(/\.html?$/, "")));
@@ -687,30 +1934,30 @@ function discoverRoutes(root, framework) {
687
1934
  }
688
1935
  function svelteKitLayouts(root) {
689
1936
  const layoutByDir = /* @__PURE__ */ new Map();
690
- const base = path3.join(root, "src", "routes");
1937
+ const base = path4.join(root, "src", "routes");
691
1938
  const readLayout = (dir) => {
692
- const layoutFile = path3.join(dir, "+layout.svelte");
693
- if (fs3.existsSync(layoutFile)) {
694
- layoutByDir.set(dir, parsePageSource(fs3.readFileSync(layoutFile, "utf8"), "sveltekit").metadata);
1939
+ const layoutFile = path4.join(dir, "+layout.svelte");
1940
+ if (fs4.existsSync(layoutFile)) {
1941
+ layoutByDir.set(dir, parsePageSource(fs4.readFileSync(layoutFile, "utf8"), "sveltekit").metadata);
695
1942
  }
696
- for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
1943
+ for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
697
1944
  if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
698
- readLayout(path3.join(dir, entry.name));
1945
+ readLayout(path4.join(dir, entry.name));
699
1946
  }
700
1947
  }
701
1948
  };
702
- if (fs3.existsSync(base)) readLayout(base);
1949
+ if (fs4.existsSync(base)) readLayout(base);
703
1950
  return layoutByDir;
704
1951
  }
705
1952
  function mergeLayoutMetadata(routes, framework, root) {
706
1953
  if (framework !== "sveltekit") return routes;
707
1954
  const layoutByDir = svelteKitLayouts(root);
708
1955
  if (layoutByDir.size === 0) return routes;
709
- const base = path3.join(root, "src", "routes");
1956
+ const base = path4.join(root, "src", "routes");
710
1957
  return routes.map((route) => {
711
1958
  const merged = {};
712
- let dir = path3.dirname(route.filePath);
713
- while (dir === base || dir.startsWith(`${base}${path3.sep}`)) {
1959
+ let dir = path4.dirname(route.filePath);
1960
+ while (dir === base || dir.startsWith(`${base}${path4.sep}`)) {
714
1961
  const meta = layoutByDir.get(dir);
715
1962
  if (meta) {
716
1963
  for (const field of INHERITED_METADATA_FIELDS) {
@@ -718,7 +1965,7 @@ function mergeLayoutMetadata(routes, framework, root) {
718
1965
  }
719
1966
  }
720
1967
  if (dir === base) break;
721
- dir = path3.dirname(dir);
1968
+ dir = path4.dirname(dir);
722
1969
  }
723
1970
  if (Object.keys(merged).length === 0) return route;
724
1971
  for (const field of INHERITED_METADATA_FIELDS) {
@@ -728,16 +1975,16 @@ function mergeLayoutMetadata(routes, framework, root) {
728
1975
  });
729
1976
  }
730
1977
  function crawlFiles(root) {
731
- const roots = [path3.join(root, "public"), path3.join(root, "static"), root];
732
- const sitemap = roots.map((dir) => path3.join(dir, "sitemap.xml")).find(fs3.existsSync);
733
- const robots = roots.map((dir) => path3.join(dir, "robots.txt")).find(fs3.existsSync);
734
- const sitemapContent = sitemap ? fs3.readFileSync(sitemap, "utf8") : "";
1978
+ const roots = [path4.join(root, "public"), path4.join(root, "static"), root];
1979
+ const sitemap = roots.map((dir) => path4.join(dir, "sitemap.xml")).find(fs4.existsSync);
1980
+ const robots = roots.map((dir) => path4.join(dir, "robots.txt")).find(fs4.existsSync);
1981
+ const sitemapContent = sitemap ? fs4.readFileSync(sitemap, "utf8") : "";
735
1982
  return {
736
1983
  sitemapFound: Boolean(sitemap),
737
1984
  sitemapUrls: [...sitemapContent.matchAll(/<loc>([^<]+)<\/loc>/gi)].map((m) => m[1].trim()),
738
1985
  sitemapMalformed: Boolean(sitemap && (!/<urlset\b/i.test(sitemapContent) || !/<loc>[^<]+<\/loc>/i.test(sitemapContent))),
739
1986
  robotsFound: Boolean(robots),
740
- robotsContent: robots ? fs3.readFileSync(robots, "utf8") : ""
1987
+ robotsContent: robots ? fs4.readFileSync(robots, "utf8") : ""
741
1988
  };
742
1989
  }
743
1990
  var FrameworkAdapter = class {
@@ -750,11 +1997,11 @@ var FrameworkAdapter = class {
750
1997
  if (deps["@remix-run/react"] || deps["@remix-run/node"]) return "remix";
751
1998
  if (deps.vite) return "vite";
752
1999
  if (await NextJsAdapter.detect(root)) return "nextjs";
753
- if (fs3.existsSync(path3.join(root, "nuxt.config.ts")) || fs3.existsSync(path3.join(root, "nuxt.config.js"))) return "nuxt";
754
- if (fs3.existsSync(path3.join(root, "svelte.config.js")) && fs3.existsSync(path3.join(root, "src", "routes"))) {
2000
+ if (fs4.existsSync(path4.join(root, "nuxt.config.ts")) || fs4.existsSync(path4.join(root, "nuxt.config.js"))) return "nuxt";
2001
+ if (fs4.existsSync(path4.join(root, "svelte.config.js")) && fs4.existsSync(path4.join(root, "src", "routes"))) {
755
2002
  return "sveltekit";
756
2003
  }
757
- if (fs3.existsSync(path3.join(root, "index.html"))) return "static";
2004
+ if (fs4.existsSync(path4.join(root, "index.html"))) return "static";
758
2005
  return null;
759
2006
  }
760
2007
  static async scan(root) {
@@ -790,8 +2037,12 @@ var metadataTitleRule = {
790
2037
  const findings = [];
791
2038
  const titleMap = /* @__PURE__ */ new Map();
792
2039
  for (const route of context.routes) {
2040
+ if (route.isRedirect) continue;
793
2041
  const title = route.metadata.title;
2042
+ const dynamic = route.metadata.dynamicMetadata === true;
2043
+ const robots = (route.metadata.robots || "").toLowerCase();
794
2044
  if (!title || title.trim() === "") {
2045
+ if (route.metadata.titleDeclared || dynamic) continue;
795
2046
  findings.push({
796
2047
  id: `title-missing-${route.route}`,
797
2048
  rule: "metadata-title",
@@ -806,9 +2057,13 @@ var metadataTitleRule = {
806
2057
  continue;
807
2058
  }
808
2059
  const trimmed = title.trim();
809
- const existing = titleMap.get(trimmed) || [];
810
- existing.push(route.route);
811
- titleMap.set(trimmed, existing);
2060
+ const indexable = !robots.includes("noindex");
2061
+ if (route.metadata.titleDeclared && !dynamic && indexable) {
2062
+ const existing = titleMap.get(trimmed) || [];
2063
+ existing.push(route.route);
2064
+ titleMap.set(trimmed, existing);
2065
+ }
2066
+ if (dynamic) continue;
812
2067
  if (trimmed.length < 10) {
813
2068
  findings.push({
814
2069
  id: `title-short-${route.route}`,
@@ -864,8 +2119,12 @@ var metadataDescriptionRule = {
864
2119
  const findings = [];
865
2120
  const descMap = /* @__PURE__ */ new Map();
866
2121
  for (const route of context.routes) {
2122
+ if (route.isRedirect) continue;
867
2123
  const desc = route.metadata.description;
2124
+ const dynamic = route.metadata.dynamicMetadata === true;
2125
+ const robots = (route.metadata.robots || "").toLowerCase();
868
2126
  if (!desc || desc.trim() === "") {
2127
+ if (route.metadata.descriptionDeclared || dynamic) continue;
869
2128
  findings.push({
870
2129
  id: `desc-missing-${route.route}`,
871
2130
  rule: "metadata-description",
@@ -880,9 +2139,13 @@ var metadataDescriptionRule = {
880
2139
  continue;
881
2140
  }
882
2141
  const trimmed = desc.trim();
883
- const existing = descMap.get(trimmed) || [];
884
- existing.push(route.route);
885
- descMap.set(trimmed, existing);
2142
+ const indexable = !robots.includes("noindex");
2143
+ if (route.metadata.descriptionDeclared && !dynamic && indexable) {
2144
+ const existing = descMap.get(trimmed) || [];
2145
+ existing.push(route.route);
2146
+ descMap.set(trimmed, existing);
2147
+ }
2148
+ if (dynamic) continue;
886
2149
  if (trimmed.length < 50) {
887
2150
  findings.push({
888
2151
  id: `desc-short-${route.route}`,
@@ -956,8 +2219,11 @@ var canonicalRule = {
956
2219
  }
957
2220
  }
958
2221
  for (const route of context.routes) {
2222
+ if (route.isRedirect) continue;
2223
+ if (route.metadata.dynamicMetadata) continue;
959
2224
  const canonical = route.metadata.canonical;
960
2225
  if (!canonical || canonical.trim() === "") {
2226
+ if (route.metadata.canonicalDeclared) continue;
961
2227
  findings.push({
962
2228
  id: `canonical-missing-${route.route}`,
963
2229
  rule: "canonical",
@@ -1031,6 +2297,20 @@ var canonicalRule = {
1031
2297
  }
1032
2298
  }
1033
2299
  } catch {
2300
+ const looksLikePath = canonical.startsWith("/");
2301
+ if (looksLikePath) {
2302
+ findings.push({
2303
+ id: `canonical-relative-no-base-${route.route}`,
2304
+ rule: "canonical",
2305
+ severity: "warning",
2306
+ category: "technical",
2307
+ message: `Canonical URL "${canonical}" on route "${route.route}" is relative and no siteUrl/metadataBase was found to resolve it.`,
2308
+ file: route.filePath,
2309
+ route: route.route,
2310
+ fixable: false
2311
+ });
2312
+ continue;
2313
+ }
1034
2314
  findings.push({
1035
2315
  id: `canonical-malformed-${route.route}`,
1036
2316
  rule: "canonical",
@@ -1069,6 +2349,7 @@ var headingsRule = {
1069
2349
  analyze(context) {
1070
2350
  const findings = [];
1071
2351
  for (const route of context.routes) {
2352
+ if (route.isRedirect) continue;
1072
2353
  const headings = route.headings;
1073
2354
  const h1s = headings.filter((h) => h.level === 1);
1074
2355
  if (h1s.length === 0) {
@@ -1177,6 +2458,44 @@ var imagesRule = {
1177
2458
  }
1178
2459
  };
1179
2460
 
2461
+ // ../core/src/route-match.ts
2462
+ function segmentsOf(route) {
2463
+ return route.split("/").filter((segment) => segment !== "");
2464
+ }
2465
+ function normalizeHref(href) {
2466
+ const clean = href.split("?")[0].split("#")[0];
2467
+ const withoutSlash = clean.length > 1 && clean.endsWith("/") ? clean.slice(0, -1) : clean;
2468
+ return withoutSlash === "" ? "/" : withoutSlash;
2469
+ }
2470
+ function isCatchAllSegment(segment) {
2471
+ return segment.startsWith("[...") || segment.startsWith("[[...");
2472
+ }
2473
+ function isDynamicSegment(segment) {
2474
+ return segment.startsWith("[") && segment.endsWith("]") || isCatchAllSegment(segment);
2475
+ }
2476
+ function routePatternMatches(pattern, href) {
2477
+ const patSegs = segmentsOf(pattern);
2478
+ const hrefSegs = segmentsOf(href);
2479
+ let hi = 0;
2480
+ for (let pi = 0; pi < patSegs.length; pi += 1) {
2481
+ const seg = patSegs[pi];
2482
+ if (seg.startsWith("[[...") && seg.endsWith("]")) {
2483
+ return true;
2484
+ }
2485
+ if (seg.startsWith("[...") && seg.endsWith("]")) {
2486
+ return hi < hrefSegs.length;
2487
+ }
2488
+ if (hi >= hrefSegs.length) return false;
2489
+ if (isDynamicSegment(seg)) {
2490
+ hi += 1;
2491
+ continue;
2492
+ }
2493
+ if (seg !== hrefSegs[hi]) return false;
2494
+ hi += 1;
2495
+ }
2496
+ return hi === hrefSegs.length;
2497
+ }
2498
+
1180
2499
  // ../core/src/rules/links.ts
1181
2500
  var linksRule = {
1182
2501
  id: "links",
@@ -1184,11 +2503,29 @@ var linksRule = {
1184
2503
  category: "links",
1185
2504
  analyze(context) {
1186
2505
  const findings = [];
1187
- const validRoutes = new Set(context.routes.map((r) => r.route));
1188
2506
  const normalizeRoute2 = (r) => r.endsWith("/") && r.length > 1 ? r.slice(0, -1) : r;
1189
- const normalizedValidRoutes = new Set([...validRoutes].map(normalizeRoute2));
2507
+ const validRoutes = new Set(context.routes.map((r) => normalizeRoute2(r.route)));
1190
2508
  const redirects = new Map((context.redirects || []).map((redirect) => [normalizeRoute2(redirect.source), redirect.destination]));
2509
+ const dynamicRoutes = context.routes.filter((r) => r.hasDynamicSegments);
2510
+ const dynamicByPattern = new Map(dynamicRoutes.map((r) => [normalizeRoute2(r.route), r]));
2511
+ const isDynamicReachable = (cleanHref) => {
2512
+ for (const [pattern, routeNode2] of dynamicByPattern) {
2513
+ if (routePatternMatches(pattern, cleanHref)) {
2514
+ if (routeNode2.dynamicParams !== false) return { reachable: true, route: routeNode2 };
2515
+ const candidates = new Set(routeNode2.generatedParams || []);
2516
+ const targetSegment = cleanHref.split("/").pop() || "";
2517
+ if (targetSegment !== "" && candidates.has(targetSegment)) return { reachable: true, route: routeNode2 };
2518
+ return {
2519
+ reachable: false,
2520
+ route: routeNode2,
2521
+ reason: `route "${pattern}" only renders its generateStaticParams output and "${targetSegment}" could not be verified`
2522
+ };
2523
+ }
2524
+ }
2525
+ return { reachable: false };
2526
+ };
1191
2527
  for (const route of context.routes) {
2528
+ if (route.isRedirect) continue;
1192
2529
  for (const link of route.links) {
1193
2530
  if (!link.isInternal) continue;
1194
2531
  const href = link.href.trim();
@@ -1207,7 +2544,7 @@ var linksRule = {
1207
2544
  });
1208
2545
  continue;
1209
2546
  }
1210
- const cleanHref = normalizeRoute2(href.split("?")[0].split("#")[0]);
2547
+ const cleanHref = normalizeHref(href);
1211
2548
  if (cleanHref.startsWith("/")) {
1212
2549
  const redirectTarget = redirects.get(cleanHref);
1213
2550
  if (redirectTarget) {
@@ -1223,10 +2560,28 @@ var linksRule = {
1223
2560
  fixable: false,
1224
2561
  explanation: "Link directly to the final internal route to avoid unnecessary crawl hops."
1225
2562
  });
2563
+ continue;
1226
2564
  }
1227
- if (!normalizedValidRoutes.has(cleanHref) && !redirectTarget) {
2565
+ if (!validRoutes.has(cleanHref)) {
2566
+ const dynamic = isDynamicReachable(cleanHref);
2567
+ if (dynamic.reachable) continue;
2568
+ if (dynamic.route) {
2569
+ findings.push({
2570
+ id: `link-unverifiable-dynamic-${route.route}-${cleanHref}`,
2571
+ rule: "links",
2572
+ severity: "warning",
2573
+ category: "links",
2574
+ message: `Internal link "${href}" on "${route.route}" targets dynamic ${dynamic.route.route}, but ${dynamic.reason}.`,
2575
+ file: route.filePath,
2576
+ line: link.line,
2577
+ route: route.route,
2578
+ fixable: false,
2579
+ explanation: "The target route renders only its generateStaticParams output; this link cannot be statically verified as reachable."
2580
+ });
2581
+ continue;
2582
+ }
1228
2583
  let isTypoFixable = false;
1229
- for (const valid of normalizedValidRoutes) {
2584
+ for (const valid of validRoutes) {
1230
2585
  if (valid.toLowerCase() === cleanHref.toLowerCase()) {
1231
2586
  isTypoFixable = true;
1232
2587
  break;
@@ -1324,11 +2679,13 @@ var crawlabilityRule = {
1324
2679
  });
1325
2680
  }
1326
2681
  const validRoutes = new Set(context.routes.map((r) => r.route));
2682
+ const dynamicPatterns = context.routes.filter((r) => r.hasDynamicSegments).map((r) => r.route);
1327
2683
  for (const url of context.sitemapUrls) {
1328
2684
  try {
1329
2685
  const pathname = url.startsWith("http") ? new URL(url).pathname : url;
1330
- const clean = pathname.endsWith("/") && pathname.length > 1 ? pathname.slice(0, -1) : pathname;
1331
- if (!validRoutes.has(clean) && clean !== "") {
2686
+ const clean = normalizeHref(pathname);
2687
+ const isKnown = validRoutes.has(clean) || dynamicPatterns.some((pattern) => routePatternMatches(pattern, clean));
2688
+ if (!isKnown && clean !== "" && clean !== "/") {
1332
2689
  findings.push({
1333
2690
  id: `sitemap-orphan-url-${clean}`,
1334
2691
  rule: "crawlability",
@@ -1637,7 +2994,7 @@ function buildLinkGraph(routes, redirects = []) {
1637
2994
  outgoingCount[node] = 0;
1638
2995
  adjacencyList[node] = [];
1639
2996
  }
1640
- const normalize = (path9) => path9.endsWith("/") && path9.length > 1 ? path9.slice(0, -1) : path9;
2997
+ const normalize = (path10) => path10.endsWith("/") && path10.length > 1 ? path10.slice(0, -1) : path10;
1641
2998
  const redirectMap = new Map(redirects.map((redirect) => [normalize(redirect.source), normalize(redirect.destination)]));
1642
2999
  for (const route of routes) {
1643
3000
  const source = route.route;
@@ -1903,8 +3260,8 @@ function detectContentOpportunities(routes, providedKeywords = {}) {
1903
3260
  }
1904
3261
 
1905
3262
  // ../core/src/fixer.ts
1906
- import path4 from "node:path";
1907
- import fs4 from "node:fs";
3263
+ import path5 from "node:path";
3264
+ import fs5 from "node:fs";
1908
3265
  function createUnifiedDiff(filename, oldText, newText) {
1909
3266
  const oldLines = oldText ? oldText.split("\n") : [];
1910
3267
  const newLines = newText ? newText.split("\n") : [];
@@ -1958,7 +3315,7 @@ function generateRevertSafeFix(options) {
1958
3315
  filePath,
1959
3316
  originalContent: headContent,
1960
3317
  newContent: updated,
1961
- diff: createUnifiedDiff(path4.relative(projectRoot, filePath), headContent, updated),
3318
+ diff: createUnifiedDiff(path5.relative(projectRoot, filePath), headContent, updated),
1962
3319
  description: `Restored canonical value "${canonicalVal}" from BASE revision.`
1963
3320
  };
1964
3321
  }
@@ -1973,7 +3330,7 @@ function generateRevertSafeFix(options) {
1973
3330
  filePath,
1974
3331
  originalContent: headContent,
1975
3332
  newContent: updated,
1976
- diff: createUnifiedDiff(path4.relative(projectRoot, filePath), headContent, updated),
3333
+ diff: createUnifiedDiff(path5.relative(projectRoot, filePath), headContent, updated),
1977
3334
  description: "Restored indexable robots directive present in BASE revision."
1978
3335
  };
1979
3336
  }
@@ -2072,19 +3429,19 @@ ${content}`;
2072
3429
  }
2073
3430
  function assetLayout(projectRoot, framework, isAppRouter) {
2074
3431
  if (framework === "nextjs" && isAppRouter) {
2075
- const appDirectory = fs4.existsSync(path4.join(projectRoot, "app")) ? path4.join(projectRoot, "app") : path4.join(projectRoot, "src", "app");
3432
+ const appDirectory = fs5.existsSync(path5.join(projectRoot, "app")) ? path5.join(projectRoot, "app") : path5.join(projectRoot, "src", "app");
2076
3433
  return { kind: "app-router", directory: appDirectory };
2077
3434
  }
2078
3435
  let directory;
2079
3436
  switch (framework) {
2080
3437
  case "sveltekit":
2081
- directory = path4.join(projectRoot, "static");
3438
+ directory = path5.join(projectRoot, "static");
2082
3439
  break;
2083
3440
  case "static":
2084
3441
  directory = projectRoot;
2085
3442
  break;
2086
3443
  default:
2087
- directory = path4.join(projectRoot, "public");
3444
+ directory = path5.join(projectRoot, "public");
2088
3445
  }
2089
3446
  return { kind: "file", directory };
2090
3447
  }
@@ -2115,21 +3472,21 @@ async function applySafeFixes(options) {
2115
3472
  const fixedFindingIds = /* @__PURE__ */ new Set();
2116
3473
  const virtualFiles = /* @__PURE__ */ new Map();
2117
3474
  const safeFile = (candidate) => {
2118
- const root = path4.resolve(projectRoot);
2119
- const resolved = path4.resolve(candidate);
2120
- return resolved === root || resolved.startsWith(`${root}${path4.sep}`) ? resolved : null;
3475
+ const root = path5.resolve(projectRoot);
3476
+ const resolved = path5.resolve(candidate);
3477
+ return resolved === root || resolved.startsWith(`${root}${path5.sep}`) ? resolved : null;
2121
3478
  };
2122
3479
  const readCurrent = (candidate) => {
2123
3480
  if (virtualFiles.has(candidate)) return virtualFiles.get(candidate);
2124
- return fs4.existsSync(candidate) ? fs4.readFileSync(candidate, "utf8") : "";
3481
+ return fs5.existsSync(candidate) ? fs5.readFileSync(candidate, "utf8") : "";
2125
3482
  };
2126
3483
  const recordChange = (change, findingId) => {
2127
3484
  appliedChanges.push(change);
2128
3485
  fixedFindingIds.add(findingId);
2129
3486
  virtualFiles.set(change.filePath, change.newContent);
2130
3487
  if (!dryRun) {
2131
- fs4.mkdirSync(path4.dirname(change.filePath), { recursive: true });
2132
- fs4.writeFileSync(change.filePath, change.newContent, "utf8");
3488
+ fs5.mkdirSync(path5.dirname(change.filePath), { recursive: true });
3489
+ fs5.writeFileSync(change.filePath, change.newContent, "utf8");
2133
3490
  }
2134
3491
  };
2135
3492
  const layout = assetLayout(projectRoot, framework, isAppRouter);
@@ -2140,7 +3497,7 @@ async function applySafeFixes(options) {
2140
3497
  for (const f of revertSafeFindings) {
2141
3498
  const targetFile = f.file ? safeFile(f.file) : null;
2142
3499
  if (!targetFile) continue;
2143
- const relPath = path4.relative(projectRoot, targetFile).replace(/\\/g, "/");
3500
+ const relPath = path5.relative(projectRoot, targetFile).replace(/\\/g, "/");
2144
3501
  const baseContent = options.baseFiles.get(relPath) || options.baseFiles.get(targetFile);
2145
3502
  if (!baseContent) continue;
2146
3503
  const headContent = readCurrent(targetFile);
@@ -2159,9 +3516,9 @@ async function applySafeFixes(options) {
2159
3516
  const robotsFinding = findings.find((f) => f.rule === "crawlability" && f.id === "robots-missing");
2160
3517
  if (robotsFinding) {
2161
3518
  if (layout.kind === "app-router") {
2162
- const robotsFilePath = path4.join(layout.directory, "robots.ts");
2163
- const relativePath = path4.relative(projectRoot, robotsFilePath);
2164
- const oldContent = fs4.existsSync(robotsFilePath) ? fs4.readFileSync(robotsFilePath, "utf8") : "";
3519
+ const robotsFilePath = path5.join(layout.directory, "robots.ts");
3520
+ const relativePath = path5.relative(projectRoot, robotsFilePath);
3521
+ const oldContent = fs5.existsSync(robotsFilePath) ? fs5.readFileSync(robotsFilePath, "utf8") : "";
2165
3522
  const sitemapLine = siteUrl ? `
2166
3523
  sitemap: '${siteUrl}/sitemap.xml',` : "";
2167
3524
  const newContent = `import { MetadataRoute } from 'next';
@@ -2185,9 +3542,9 @@ export default function robots(): MetadataRoute.Robots {
2185
3542
  description: "Generated app/robots.ts with standard crawler rules and sitemap reference."
2186
3543
  }, robotsFinding.id);
2187
3544
  } else {
2188
- const robotsFilePath = path4.join(layout.directory, "robots.txt");
2189
- const relativePath = path4.relative(projectRoot, robotsFilePath);
2190
- const oldContent = fs4.existsSync(robotsFilePath) ? fs4.readFileSync(robotsFilePath, "utf8") : "";
3545
+ const robotsFilePath = path5.join(layout.directory, "robots.txt");
3546
+ const relativePath = path5.relative(projectRoot, robotsFilePath);
3547
+ const oldContent = fs5.existsSync(robotsFilePath) ? fs5.readFileSync(robotsFilePath, "utf8") : "";
2191
3548
  const newContent = ROBOTS_TXT(siteUrl);
2192
3549
  recordChange({
2193
3550
  filePath: robotsFilePath,
@@ -2203,9 +3560,9 @@ export default function robots(): MetadataRoute.Robots {
2203
3560
  );
2204
3561
  if (sitemapFinding && siteUrl) {
2205
3562
  if (layout.kind === "app-router") {
2206
- const sitemapFilePath = path4.join(layout.directory, "sitemap.ts");
2207
- const relativePath = path4.relative(projectRoot, sitemapFilePath);
2208
- const oldContent = fs4.existsSync(sitemapFilePath) ? fs4.readFileSync(sitemapFilePath, "utf8") : "";
3563
+ const sitemapFilePath = path5.join(layout.directory, "sitemap.ts");
3564
+ const relativePath = path5.relative(projectRoot, sitemapFilePath);
3565
+ const oldContent = fs5.existsSync(sitemapFilePath) ? fs5.readFileSync(sitemapFilePath, "utf8") : "";
2209
3566
  const routeEntries = validRoutes.map(
2210
3567
  (r) => ` {
2211
3568
  url: '${siteUrl}${r === "/" ? "" : r}',
@@ -2230,9 +3587,9 @@ ${routeEntries}
2230
3587
  description: `Generated app/sitemap.ts containing ${validRoutes.length} discovered routes.`
2231
3588
  }, sitemapFinding.id);
2232
3589
  } else {
2233
- const sitemapFilePath = path4.join(layout.directory, "sitemap.xml");
2234
- const relativePath = path4.relative(projectRoot, sitemapFilePath);
2235
- const oldContent = fs4.existsSync(sitemapFilePath) ? fs4.readFileSync(sitemapFilePath, "utf8") : "";
3590
+ const sitemapFilePath = path5.join(layout.directory, "sitemap.xml");
3591
+ const relativePath = path5.relative(projectRoot, sitemapFilePath);
3592
+ const oldContent = fs5.existsSync(sitemapFilePath) ? fs5.readFileSync(sitemapFilePath, "utf8") : "";
2236
3593
  const xmlEntries = validRoutes.map(
2237
3594
  (r) => ` <url>
2238
3595
  <loc>${siteUrl}${r === "/" ? "" : r}</loc>
@@ -2258,7 +3615,7 @@ ${xmlEntries}
2258
3615
  const canonicalFindings = findings.filter((f) => f.rule === "canonical" && f.fixable && f.file);
2259
3616
  for (const finding of canonicalFindings) {
2260
3617
  const findingFile = finding.file ? safeFile(finding.file) : null;
2261
- if (!findingFile || !fs4.existsSync(findingFile)) continue;
3618
+ if (!findingFile || !fs5.existsSync(findingFile)) continue;
2262
3619
  const fileContent = readCurrent(findingFile);
2263
3620
  const route = finding.route || "/";
2264
3621
  const canonicalUrl = `${siteUrl}${route === "/" ? "" : route}`;
@@ -2268,8 +3625,8 @@ ${xmlEntries}
2268
3625
  filePath: findingFile,
2269
3626
  originalContent: fileContent,
2270
3627
  newContent: updatedContent,
2271
- diff: createUnifiedDiff(path4.relative(projectRoot, findingFile), fileContent, updatedContent),
2272
- description: `Added canonical "${canonicalUrl}" declaration to ${path4.basename(findingFile)}.`
3628
+ diff: createUnifiedDiff(path5.relative(projectRoot, findingFile), fileContent, updatedContent),
3629
+ description: `Added canonical "${canonicalUrl}" declaration to ${path5.basename(findingFile)}.`
2273
3630
  }, finding.id);
2274
3631
  }
2275
3632
  }
@@ -2277,7 +3634,7 @@ ${xmlEntries}
2277
3634
  const fixableLinkFindings = findings.filter((f) => f.rule === "links" && f.fixable && f.file);
2278
3635
  for (const lf of fixableLinkFindings) {
2279
3636
  const linkFile = lf.file ? safeFile(lf.file) : null;
2280
- if (!linkFile || !fs4.existsSync(linkFile)) continue;
3637
+ if (!linkFile || !fs5.existsSync(linkFile)) continue;
2281
3638
  const fileContent = readCurrent(linkFile);
2282
3639
  const match = lf.message.match(/nonexistent route "([^"]+)"/);
2283
3640
  if (match) {
@@ -2296,7 +3653,7 @@ ${xmlEntries}
2296
3653
  filePath: linkFile,
2297
3654
  originalContent: fileContent,
2298
3655
  newContent: updatedContent,
2299
- diff: createUnifiedDiff(path4.relative(projectRoot, linkFile), fileContent, updatedContent),
3656
+ diff: createUnifiedDiff(path5.relative(projectRoot, linkFile), fileContent, updatedContent),
2300
3657
  description: `Fixed casing of internal link: "${brokenHref}" -> "${normalizedTarget}".`
2301
3658
  }, lf.id);
2302
3659
  }
@@ -2446,7 +3803,7 @@ function runSEOAudit(options) {
2446
3803
  recommendations,
2447
3804
  opportunities,
2448
3805
  timestamp,
2449
- engineVersion: "0.2.0"
3806
+ engineVersion: "0.3.0"
2450
3807
  };
2451
3808
  }
2452
3809
 
@@ -2524,7 +3881,7 @@ function evaluateQualityGate(input) {
2524
3881
  }
2525
3882
 
2526
3883
  // ../core/src/diff-engine.ts
2527
- import path5 from "node:path";
3884
+ import path6 from "node:path";
2528
3885
 
2529
3886
  // ../core/src/contracts.ts
2530
3887
  function matchesRoutePattern(route, pattern) {
@@ -3012,19 +4369,19 @@ function consolidateRegressionsByRootCause(rawNewFindings, baseAudit, headAudit,
3012
4369
  const layoutPath = mut.nodeId.replace(/^layout:/, "");
3013
4370
  const downstream = traceDownstreamRoutes(headSEOGraph, layoutPath);
3014
4371
  if (downstream.length === 0) continue;
3015
- const prop = mut.property;
4372
+ const prop2 = mut.property;
3016
4373
  const matching = rawNewFindings.filter((f) => {
3017
4374
  if (handledIds.has(f.id)) return false;
3018
4375
  const routeMatch = f.route && downstream.includes(f.route);
3019
- const propMatch = prop === "canonical" ? f.rule.includes("canonical") : f.rule.includes("robots") || f.message.includes("noindex");
4376
+ const propMatch = prop2 === "canonical" ? f.rule.includes("canonical") : f.rule.includes("robots") || f.message.includes("noindex");
3020
4377
  return routeMatch && propMatch;
3021
4378
  });
3022
4379
  if (matching.length > 0) {
3023
4380
  for (const m of matching) handledIds.add(m.id);
3024
- const isCanonical = prop === "canonical";
3025
- const baseName = path5.basename(layoutPath);
4381
+ const isCanonical = prop2 === "canonical";
4382
+ const baseName = path6.basename(layoutPath);
3026
4383
  consolidated.push({
3027
- id: `regression-${baseName}-${prop}`,
4384
+ id: `regression-${baseName}-${prop2}`,
3028
4385
  rule: isCanonical ? "metadata/canonical-regression" : "crawlability/robots-regression",
3029
4386
  severity: "error",
3030
4387
  category: isCanonical ? "metadata" : "technical",
@@ -3057,7 +4414,7 @@ function consolidateRegressionsByRootCause(rawNewFindings, baseAudit, headAudit,
3057
4414
  sampleRoutes: downstream.slice(0, 3),
3058
4415
  confidence: "STRUCTURAL",
3059
4416
  revertSafeValue: mut.oldValue ? String(mut.oldValue) : void 0,
3060
- fixRecommendation: `Restore previous ${prop} declaration from BASE revision in ${baseName}.`
4417
+ fixRecommendation: `Restore previous ${prop2} declaration from BASE revision in ${baseName}.`
3061
4418
  }
3062
4419
  });
3063
4420
  }
@@ -3087,7 +4444,7 @@ function consolidateRegressionsByRootCause(rawNewFindings, baseAudit, headAudit,
3087
4444
  (n) => n.type === "layout" && (n.path?.includes(dir.replace("/", "")) || n.id.includes(dir.replace("/", "")))
3088
4445
  );
3089
4446
  const inferredLayoutFile = layoutNode?.path || `app${dir}/layout.tsx`;
3090
- const baseName = path5.basename(inferredLayoutFile);
4447
+ const baseName = path6.basename(inferredLayoutFile);
3091
4448
  const affected = dirFindings.map((f) => f.route).sort();
3092
4449
  for (const df of dirFindings) handledIds.add(df.id);
3093
4450
  consolidated.push({
@@ -3807,8 +5164,8 @@ var SessionKeyStore = class {
3807
5164
  var sessionKeyStore = new SessionKeyStore();
3808
5165
 
3809
5166
  // ../ai/src/semantic-resolver.ts
3810
- import fs5 from "node:fs";
3811
- import path6 from "node:path";
5167
+ import fs6 from "node:fs";
5168
+ import path7 from "node:path";
3812
5169
  var METADATA_KEYS = ["canonical", "robots", "title", "description"];
3813
5170
  function helperDefinesProperty(helperCode, property) {
3814
5171
  const escaped = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -3819,8 +5176,8 @@ async function resolveSemanticHelpers(options) {
3819
5176
  if (!aiProvider) return [];
3820
5177
  const results = [];
3821
5178
  for (const route of routes) {
3822
- if (!route.filePath || !fs5.existsSync(route.filePath)) continue;
3823
- const content = fs5.readFileSync(route.filePath, "utf8");
5179
+ if (!route.filePath || !fs6.existsSync(route.filePath)) continue;
5180
+ const content = fs6.readFileSync(route.filePath, "utf8");
3824
5181
  const helperMatch = content.match(
3825
5182
  /export\s+const\s+metadata\s*=\s*([A-Za-z0-9_]+)\s*\(([\s\S]*?)\)/
3826
5183
  );
@@ -3835,16 +5192,16 @@ async function resolveSemanticHelpers(options) {
3835
5192
  if (importMatch) {
3836
5193
  const importPath = importMatch[1];
3837
5194
  const candidates = [
3838
- path6.resolve(path6.dirname(route.filePath), `${importPath}.ts`),
3839
- path6.resolve(path6.dirname(route.filePath), `${importPath}.tsx`),
3840
- path6.resolve(path6.dirname(route.filePath), `${importPath}/index.ts`),
3841
- path6.resolve(projectRoot, `${importPath.replace(/^@\//, "src/").replace(/^~\//, "")}.ts`),
3842
- path6.resolve(projectRoot, `${importPath.replace(/^@\//, "src/").replace(/^~\//, "")}.tsx`)
5195
+ path7.resolve(path7.dirname(route.filePath), `${importPath}.ts`),
5196
+ path7.resolve(path7.dirname(route.filePath), `${importPath}.tsx`),
5197
+ path7.resolve(path7.dirname(route.filePath), `${importPath}/index.ts`),
5198
+ path7.resolve(projectRoot, `${importPath.replace(/^@\//, "src/").replace(/^~\//, "")}.ts`),
5199
+ path7.resolve(projectRoot, `${importPath.replace(/^@\//, "src/").replace(/^~\//, "")}.tsx`)
3843
5200
  ];
3844
5201
  for (const cand of candidates) {
3845
- if (fs5.existsSync(cand)) {
5202
+ if (fs6.existsSync(cand)) {
3846
5203
  helperFilePath = cand;
3847
- helperCode = fs5.readFileSync(cand, "utf8");
5204
+ helperCode = fs6.readFileSync(cand, "utf8");
3848
5205
  break;
3849
5206
  }
3850
5207
  }
@@ -3869,7 +5226,7 @@ async function resolveSemanticHelpers(options) {
3869
5226
  if (hasMeaningfulProps) {
3870
5227
  const verifiedProperties = helperCode ? METADATA_KEYS.filter((property) => resolution.maps[property] && helperDefinesProperty(helperCode, property)) : [];
3871
5228
  const confidence = verifiedProperties.length > 0 ? "AI_VERIFIED" : "AI_ASSISTED";
3872
- const cleanHelperPath = helperFilePath ? path6.relative(projectRoot, helperFilePath).replace(/\\/g, "/") : helperName;
5229
+ const cleanHelperPath = helperFilePath ? path7.relative(projectRoot, helperFilePath).replace(/\\/g, "/") : helperName;
3873
5230
  const resolvedMeta = {};
3874
5231
  if (verifiedProperties.includes("canonical") && !route.metadata?.canonical) {
3875
5232
  resolvedMeta.canonical = resolution.maps.canonical;
@@ -3877,7 +5234,7 @@ async function resolveSemanticHelpers(options) {
3877
5234
  if (verifiedProperties.includes("robots") && !route.metadata?.robots) {
3878
5235
  resolvedMeta.robots = resolution.maps.robots;
3879
5236
  }
3880
- const proofId = `proof-seo-${path6.basename(route.filePath)}-${helperName}`;
5237
+ const proofId = `proof-seo-${path7.basename(route.filePath)}-${helperName}`;
3881
5238
  const evidenceItems = [
3882
5239
  {
3883
5240
  id: `ev-callsite-${helperName}`,
@@ -3898,23 +5255,23 @@ async function resolveSemanticHelpers(options) {
3898
5255
  }
3899
5256
  const verifiedClaims = [];
3900
5257
  const rejectedClaims = [];
3901
- for (const [prop, val] of Object.entries(resolution.maps)) {
5258
+ for (const [prop2, val] of Object.entries(resolution.maps)) {
3902
5259
  if (val) {
3903
- if (helperVerified && verifiedProperties.includes(prop)) {
5260
+ if (helperVerified && verifiedProperties.includes(prop2)) {
3904
5261
  verifiedClaims.push({
3905
- id: `claim-${prop}-${helperName}`,
5262
+ id: `claim-${prop2}-${helperName}`,
3906
5263
  subject: route.route,
3907
- predicate: `has${prop.charAt(0).toUpperCase() + prop.slice(1)}`,
5264
+ predicate: `has${prop2.charAt(0).toUpperCase() + prop2.slice(1)}`,
3908
5265
  observedValue: val,
3909
5266
  evidenceRefs: evidenceItems.map((e) => e.id),
3910
5267
  source: { file: cleanHelperPath, route: route.route }
3911
5268
  });
3912
5269
  } else {
3913
5270
  rejectedClaims.push({
3914
- id: `claim-${prop}-${helperName}`,
5271
+ id: `claim-${prop2}-${helperName}`,
3915
5272
  subject: route.route,
3916
- predicate: `has${prop.charAt(0).toUpperCase() + prop.slice(1)}`,
3917
- reason: helperVerified ? `Helper source does not deterministically define metadata property "${prop}".` : "Helper source code could not be verified on filesystem."
5273
+ predicate: `has${prop2.charAt(0).toUpperCase() + prop2.slice(1)}`,
5274
+ reason: helperVerified ? `Helper source does not deterministically define metadata property "${prop2}".` : "Helper source code could not be verified on filesystem."
3918
5275
  });
3919
5276
  }
3920
5277
  }
@@ -3942,7 +5299,7 @@ async function resolveSemanticHelpers(options) {
3942
5299
  discoveredBy: "AI",
3943
5300
  verifiedBy: verifiedProperties.length > 0 ? "DETERMINISTIC_VERIFIER" : void 0,
3944
5301
  evidence: [
3945
- `Call site: ${path6.basename(route.filePath)}`,
5302
+ `Call site: ${path7.basename(route.filePath)}`,
3946
5303
  helperVerified ? `Helper definition: ${cleanHelperPath}` : `Inferred signature from ${helperName}`,
3947
5304
  ...resolution.evidenceChain || []
3948
5305
  ]
@@ -3965,7 +5322,7 @@ async function resolveSemanticHelpers(options) {
3965
5322
  }
3966
5323
 
3967
5324
  // src/formatter.ts
3968
- import path7 from "node:path";
5325
+ import path8 from "node:path";
3969
5326
  var colors = {
3970
5327
  reset: "\x1B[0m",
3971
5328
  bold: "\x1B[1m",
@@ -4020,7 +5377,7 @@ ${colors.dim}${"\u2501".repeat(45)}${colors.reset}
4020
5377
  console.log(`${colors.dim}${"\u2501".repeat(45)}${colors.reset}
4021
5378
  `);
4022
5379
  const rawFile = f.rootCause?.file || f.sourceFile || f.file || "";
4023
- const relFile = rawFile ? path7.relative(projectRoot, rawFile).replace(/\\/g, "/") : "";
5380
+ const relFile = rawFile ? path8.relative(projectRoot, rawFile).replace(/\\/g, "/") : "";
4024
5381
  const lineSuffix = f.rootCause?.line || f.sourceLine || f.line ? `:${f.rootCause?.line || f.sourceLine || f.line}` : "";
4025
5382
  if (f.rootCause) {
4026
5383
  console.log(`${colors.bold}Root cause${colors.reset}`);
@@ -4041,7 +5398,7 @@ ${colors.dim}${"\u2501".repeat(45)}${colors.reset}
4041
5398
  }
4042
5399
  if (f.evidenceChain?.steps && f.evidenceChain.steps.length > 0) {
4043
5400
  console.log(`${colors.bold}Dependency${colors.reset}`);
4044
- const sourceName = path7.basename(relFile || f.evidenceChain.sourceFile);
5401
+ const sourceName = path8.basename(relFile || f.evidenceChain.sourceFile);
4045
5402
  console.log(`${sourceName}`);
4046
5403
  for (const step of f.evidenceChain.steps) {
4047
5404
  const detail = step.detail ? ` ${step.detail}` : "";
@@ -4165,7 +5522,7 @@ function printAuditReport(result, projectRoot) {
4165
5522
  if (f.severity === "warning") icon = colors.yellow + "\u26A0" + colors.reset;
4166
5523
  const fixTag = f.fixable ? ` ${colors.green}[fixable]${colors.reset}` : "";
4167
5524
  const routeTag = f.route ? ` ${colors.cyan}(${f.route})${colors.reset}` : "";
4168
- const relativeFile = f.file ? path7.relative(projectRoot, f.file) : "";
5525
+ const relativeFile = f.file ? path8.relative(projectRoot, f.file) : "";
4169
5526
  const fileTag = relativeFile ? `
4170
5527
  ${colors.dim}${relativeFile}${f.line ? `:${f.line}` : ""}${colors.reset}` : "";
4171
5528
  console.log(` ${icon} ${f.message}${fixTag}${routeTag}${fileTag}`);
@@ -4261,7 +5618,7 @@ function loadLocalEnv(projectRoot) {
4261
5618
  const dirsToSearch = [projectRoot];
4262
5619
  let curr = projectRoot;
4263
5620
  for (let i = 0; i < 4; i++) {
4264
- const parent = path8.dirname(curr);
5621
+ const parent = path9.dirname(curr);
4265
5622
  if (parent && parent !== curr) {
4266
5623
  dirsToSearch.push(parent);
4267
5624
  curr = parent;
@@ -4271,10 +5628,10 @@ function loadLocalEnv(projectRoot) {
4271
5628
  }
4272
5629
  for (const dir of dirsToSearch) {
4273
5630
  for (const envFile of [".env", ".env.local"]) {
4274
- const fullPath = path8.join(dir, envFile);
4275
- if (fs6.existsSync(fullPath)) {
5631
+ const fullPath = path9.join(dir, envFile);
5632
+ if (fs7.existsSync(fullPath)) {
4276
5633
  try {
4277
- const content = fs6.readFileSync(fullPath, "utf8");
5634
+ const content = fs7.readFileSync(fullPath, "utf8");
4278
5635
  for (const line of content.split("\n")) {
4279
5636
  const trimmed = line.trim();
4280
5637
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -4297,15 +5654,15 @@ function loadLocalEnv(projectRoot) {
4297
5654
  }
4298
5655
  }
4299
5656
  function checkSecretRisk(projectRoot) {
4300
- const envCandidates = fs6.readdirSync(projectRoot).filter((name) => name !== ".env.example" && (name === ".env" || name.startsWith(".env.")));
4301
- const gitignorePath = path8.join(projectRoot, ".gitignore");
5657
+ const envCandidates = fs7.readdirSync(projectRoot).filter((name) => name !== ".env.example" && (name === ".env" || name.startsWith(".env.")));
5658
+ const gitignorePath = path9.join(projectRoot, ".gitignore");
4302
5659
  let gitignoreContent = "";
4303
- if (fs6.existsSync(gitignorePath)) {
4304
- gitignoreContent = fs6.readFileSync(gitignorePath, "utf8");
5660
+ if (fs7.existsSync(gitignorePath)) {
5661
+ gitignoreContent = fs7.readFileSync(gitignorePath, "utf8");
4305
5662
  }
4306
5663
  for (const env of envCandidates) {
4307
- const envPath = path8.join(projectRoot, env);
4308
- if (fs6.existsSync(envPath)) {
5664
+ const envPath = path9.join(projectRoot, env);
5665
+ if (fs7.existsSync(envPath)) {
4309
5666
  const tracked = spawnSync("git", ["ls-files", "--error-unmatch", "--", env], {
4310
5667
  cwd: projectRoot,
4311
5668
  stdio: "ignore"
@@ -4317,11 +5674,11 @@ function checkSecretRisk(projectRoot) {
4317
5674
  }
4318
5675
  }
4319
5676
  async function auditGitRef(projectRoot, ref) {
4320
- const tmpDir = fs6.mkdtempSync(path8.join(os.tmpdir(), "crawlemon-base-"));
5677
+ const tmpDir = fs7.mkdtempSync(path9.join(os.tmpdir(), "crawlemon-base-"));
4321
5678
  try {
4322
5679
  const gitRootRes = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd: projectRoot, encoding: "utf8" });
4323
5680
  const gitRoot = gitRootRes.status === 0 ? gitRootRes.stdout.trim() : projectRoot;
4324
- const relToGitRoot = path8.relative(gitRoot, projectRoot).replace(/\\/g, "/");
5681
+ const relToGitRoot = path9.relative(gitRoot, projectRoot).replace(/\\/g, "/");
4325
5682
  const archiveRef = relToGitRoot && relToGitRoot !== "." ? `${ref}:${relToGitRoot}` : ref;
4326
5683
  const gitArchive = spawnSync("git", ["archive", archiveRef], {
4327
5684
  cwd: gitRoot,
@@ -4348,7 +5705,7 @@ async function auditGitRef(projectRoot, ref) {
4348
5705
  isAppRouter: scanData.isAppRouter
4349
5706
  });
4350
5707
  } finally {
4351
- fs6.rmSync(tmpDir, { recursive: true, force: true });
5708
+ fs7.rmSync(tmpDir, { recursive: true, force: true });
4352
5709
  }
4353
5710
  }
4354
5711
  async function runCli(args) {
@@ -4364,7 +5721,7 @@ async function runCli(args) {
4364
5721
  return;
4365
5722
  }
4366
5723
  if (command === "--version" || command === "-v") {
4367
- console.log("0.2.0");
5724
+ console.log("0.3.0");
4368
5725
  return;
4369
5726
  }
4370
5727
  checkSecretRisk(projectRoot);
@@ -4463,25 +5820,25 @@ async function runCli(args) {
4463
5820
  try {
4464
5821
  const gitRootRes = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd: projectRoot, encoding: "utf8" });
4465
5822
  const gitRoot = gitRootRes.status === 0 ? gitRootRes.stdout.trim() : projectRoot;
4466
- const relToGitRoot = path8.relative(gitRoot, projectRoot).replace(/\\/g, "/");
5823
+ const relToGitRoot = path9.relative(gitRoot, projectRoot).replace(/\\/g, "/");
4467
5824
  const archiveRef = relToGitRoot && relToGitRoot !== "." ? `${baseRefArg}:${relToGitRoot}` : baseRefArg;
4468
5825
  const gitArchive = spawnSync("git", ["archive", archiveRef], { cwd: gitRoot, maxBuffer: 64 * 1024 * 1024 });
4469
5826
  if (gitArchive.status === 0 && gitArchive.stdout) {
4470
- const tmpDir = fs6.mkdtempSync(path8.join(os.tmpdir(), "crawlemon-base-fix-"));
5827
+ const tmpDir = fs7.mkdtempSync(path9.join(os.tmpdir(), "crawlemon-base-fix-"));
4471
5828
  spawnSync("tar", ["-x", "-C", tmpDir], { input: gitArchive.stdout });
4472
5829
  baseFiles = /* @__PURE__ */ new Map();
4473
5830
  const walk2 = (d) => {
4474
- for (const ent of fs6.readdirSync(d, { withFileTypes: true })) {
4475
- const full = path8.join(d, ent.name);
5831
+ for (const ent of fs7.readdirSync(d, { withFileTypes: true })) {
5832
+ const full = path9.join(d, ent.name);
4476
5833
  if (ent.isDirectory()) walk2(full);
4477
5834
  else if (ent.isFile()) {
4478
- const rel = path8.relative(tmpDir, full).replace(/\\/g, "/");
4479
- baseFiles.set(rel, fs6.readFileSync(full, "utf8"));
5835
+ const rel = path9.relative(tmpDir, full).replace(/\\/g, "/");
5836
+ baseFiles.set(rel, fs7.readFileSync(full, "utf8"));
4480
5837
  }
4481
5838
  }
4482
5839
  };
4483
5840
  walk2(tmpDir);
4484
- fs6.rmSync(tmpDir, { recursive: true, force: true });
5841
+ fs7.rmSync(tmpDir, { recursive: true, force: true });
4485
5842
  }
4486
5843
  } catch {
4487
5844
  }