crawlemon 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -3
- package/dist/index.js +1637 -212
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
2
|
+
import fs7 from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
|
-
import
|
|
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
|
|
201
|
-
import
|
|
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
|
-
|
|
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
|
|
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
|
|
869
|
+
metadata,
|
|
244
870
|
metadataSourceLocation: {
|
|
245
871
|
canonicalLine,
|
|
246
872
|
robotsLine,
|
|
@@ -374,6 +1000,612 @@ 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[0]);
|
|
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
|
+
function findDefaultExportBody(content) {
|
|
1560
|
+
const idx = content.search(/export\s+default\b/);
|
|
1561
|
+
if (idx === -1) return null;
|
|
1562
|
+
const head = content.slice(idx);
|
|
1563
|
+
if (/export\s+default\s+(?:async\s+)?function\b/.test(head)) {
|
|
1564
|
+
const openParen = head.indexOf("(", head.indexOf("function"));
|
|
1565
|
+
if (openParen === -1) return null;
|
|
1566
|
+
let cursor = readBalanced(head, openParen) + 1;
|
|
1567
|
+
while (cursor < head.length && head[cursor] !== "{") cursor += 1;
|
|
1568
|
+
if (head[cursor] !== "{") return null;
|
|
1569
|
+
const end = readBalanced(head, cursor);
|
|
1570
|
+
return { start: idx + cursor, end: idx + end };
|
|
1571
|
+
}
|
|
1572
|
+
const arrow = head.indexOf("=>");
|
|
1573
|
+
if (arrow !== -1) {
|
|
1574
|
+
let cursor = arrow + 2;
|
|
1575
|
+
while (cursor < head.length && /\s/.test(head[cursor])) cursor += 1;
|
|
1576
|
+
if (head[cursor] === "{") {
|
|
1577
|
+
const end = readBalanced(head, cursor);
|
|
1578
|
+
return { start: idx + cursor, end: idx + end };
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return null;
|
|
1582
|
+
}
|
|
1583
|
+
function computeH1Concurrency(content, h1Lines) {
|
|
1584
|
+
const body = findDefaultExportBody(content);
|
|
1585
|
+
if (!body) return null;
|
|
1586
|
+
const { start, end } = body;
|
|
1587
|
+
const blockStarts = [];
|
|
1588
|
+
const returnRe = /(?:^|[^A-Za-z0-9_$])return\s*(?:\(\s*)?</g;
|
|
1589
|
+
for (const match of content.slice(start, end).matchAll(returnRe)) {
|
|
1590
|
+
const absolute = start + (match.index || 0);
|
|
1591
|
+
blockStarts.push(lineAt2(content, absolute));
|
|
1592
|
+
}
|
|
1593
|
+
if (blockStarts.length === 0) return null;
|
|
1594
|
+
blockStarts.sort((a, b) => a - b);
|
|
1595
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1596
|
+
for (const h1Line of h1Lines) {
|
|
1597
|
+
let owner = blockStarts[0];
|
|
1598
|
+
for (const block of blockStarts) {
|
|
1599
|
+
if (block <= h1Line) owner = block;
|
|
1600
|
+
else break;
|
|
1601
|
+
}
|
|
1602
|
+
counts.set(owner, (counts.get(owner) || 0) + 1);
|
|
1603
|
+
}
|
|
1604
|
+
let max = 0;
|
|
1605
|
+
for (const count of counts.values()) max = Math.max(max, count);
|
|
1606
|
+
return max;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
377
1609
|
// ../next-adapter/src/scanner.ts
|
|
378
1610
|
function normalizeRouteGroup(segment) {
|
|
379
1611
|
return segment.startsWith("(") && segment.endsWith(")") ? "" : segment;
|
|
@@ -383,39 +1615,39 @@ var NextJsAdapter = class {
|
|
|
383
1615
|
* Detects if the given directory contains a Next.js application.
|
|
384
1616
|
*/
|
|
385
1617
|
static async detect(projectRoot) {
|
|
386
|
-
const pkgPath =
|
|
387
|
-
if (
|
|
1618
|
+
const pkgPath = path3.join(projectRoot, "package.json");
|
|
1619
|
+
if (fs3.existsSync(pkgPath)) {
|
|
388
1620
|
try {
|
|
389
|
-
const pkg = JSON.parse(
|
|
1621
|
+
const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf8"));
|
|
390
1622
|
const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
|
|
391
1623
|
if (deps.next) return true;
|
|
392
1624
|
} catch {
|
|
393
1625
|
}
|
|
394
1626
|
}
|
|
395
|
-
return
|
|
1627
|
+
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
1628
|
}
|
|
397
1629
|
/**
|
|
398
1630
|
* Loads optional seo.config.ts or returns default configuration.
|
|
399
1631
|
*/
|
|
400
1632
|
static async loadConfig(projectRoot) {
|
|
401
1633
|
const configCandidates = [
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
1634
|
+
path3.join(projectRoot, "crawlemon.config.ts"),
|
|
1635
|
+
path3.join(projectRoot, "crawlemon.config.js"),
|
|
1636
|
+
path3.join(projectRoot, "crawlemon.config.mjs"),
|
|
1637
|
+
path3.join(projectRoot, "crawlemon.config.json"),
|
|
1638
|
+
path3.join(projectRoot, "crawlemon.yml"),
|
|
1639
|
+
path3.join(projectRoot, "crawlemon.yaml"),
|
|
1640
|
+
path3.join(projectRoot, "seo.config.ts"),
|
|
1641
|
+
path3.join(projectRoot, "seo.config.js"),
|
|
1642
|
+
path3.join(projectRoot, "seo.config.json")
|
|
411
1643
|
];
|
|
412
1644
|
for (const candidate of configCandidates) {
|
|
413
|
-
if (
|
|
1645
|
+
if (fs3.existsSync(candidate)) {
|
|
414
1646
|
try {
|
|
415
1647
|
if (candidate.endsWith(".json")) {
|
|
416
|
-
return JSON.parse(
|
|
1648
|
+
return JSON.parse(fs3.readFileSync(candidate, "utf8"));
|
|
417
1649
|
}
|
|
418
|
-
const content =
|
|
1650
|
+
const content = fs3.readFileSync(candidate, "utf8");
|
|
419
1651
|
const siteUrlMatch = content.match(/siteUrl\s*:\s*["']([^"']+)["']/);
|
|
420
1652
|
const ignoreMatch = content.match(/ignore\s*:\s*\[([\s\S]*?)\]/);
|
|
421
1653
|
const ignore = ignoreMatch ? Array.from(ignoreMatch[1].matchAll(/["']([^"']+)["']/g), (match) => match[1]) : void 0;
|
|
@@ -463,61 +1695,80 @@ var NextJsAdapter = class {
|
|
|
463
1695
|
static async scan(projectRoot) {
|
|
464
1696
|
const isNext = await this.detect(projectRoot);
|
|
465
1697
|
const config = await this.loadConfig(projectRoot);
|
|
466
|
-
let appDir =
|
|
467
|
-
if (!
|
|
468
|
-
appDir =
|
|
1698
|
+
let appDir = path3.join(projectRoot, "app");
|
|
1699
|
+
if (!fs3.existsSync(appDir) && fs3.existsSync(path3.join(projectRoot, "src", "app"))) {
|
|
1700
|
+
appDir = path3.join(projectRoot, "src", "app");
|
|
469
1701
|
}
|
|
470
|
-
let pagesDir =
|
|
471
|
-
if (!
|
|
472
|
-
pagesDir =
|
|
1702
|
+
let pagesDir = path3.join(projectRoot, "pages");
|
|
1703
|
+
if (!fs3.existsSync(pagesDir) && fs3.existsSync(path3.join(projectRoot, "src", "pages"))) {
|
|
1704
|
+
pagesDir = path3.join(projectRoot, "src", "pages");
|
|
473
1705
|
}
|
|
474
|
-
const isAppRouter =
|
|
1706
|
+
const isAppRouter = fs3.existsSync(appDir);
|
|
475
1707
|
const routes = [];
|
|
476
1708
|
const redirects = [];
|
|
477
1709
|
for (const configName of ["next.config.js", "next.config.mjs", "next.config.ts"]) {
|
|
478
|
-
const configPath =
|
|
479
|
-
if (!
|
|
480
|
-
const content =
|
|
1710
|
+
const configPath = path3.join(projectRoot, configName);
|
|
1711
|
+
if (!fs3.existsSync(configPath)) continue;
|
|
1712
|
+
const content = fs3.readFileSync(configPath, "utf8");
|
|
481
1713
|
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
1714
|
redirects.push({ source: match[1], destination: match[2], permanent: match[3] === "true" });
|
|
483
1715
|
}
|
|
484
1716
|
break;
|
|
485
1717
|
}
|
|
486
|
-
const
|
|
1718
|
+
const resolver = isAppRouter ? new ProjectResolver(projectRoot) : void 0;
|
|
1719
|
+
const origin = resolveProjectOrigin(config, resolver);
|
|
1720
|
+
const dependencyGraph = isAppRouter ? new RouteDependencyGraph(projectRoot, appDir, resolver, origin) : void 0;
|
|
487
1721
|
if (dependencyGraph) {
|
|
488
1722
|
dependencyGraph.indexLayouts();
|
|
489
1723
|
}
|
|
490
1724
|
if (isAppRouter) {
|
|
491
1725
|
const scanAppDir = (currentDir, relativePath = "") => {
|
|
492
|
-
const entries =
|
|
1726
|
+
const entries = fs3.readdirSync(currentDir, { withFileTypes: true });
|
|
493
1727
|
for (const entry of entries) {
|
|
494
|
-
const fullPath =
|
|
1728
|
+
const fullPath = path3.join(currentDir, entry.name);
|
|
495
1729
|
if (entry.isDirectory()) {
|
|
496
1730
|
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "api") {
|
|
497
1731
|
continue;
|
|
498
1732
|
}
|
|
499
1733
|
const normalized = normalizeRouteGroup(entry.name);
|
|
500
|
-
const nextRelative = normalized ?
|
|
1734
|
+
const nextRelative = normalized ? path3.join(relativePath, normalized) : relativePath;
|
|
501
1735
|
scanAppDir(fullPath, nextRelative);
|
|
502
1736
|
} else if (entry.isFile()) {
|
|
503
1737
|
if (/^page\.(tsx|jsx|js|ts)$/.test(entry.name)) {
|
|
504
1738
|
const routePath = relativePath === "" ? "/" : `/${relativePath.replace(/\\/g, "/")}`;
|
|
505
|
-
const content =
|
|
1739
|
+
const content = fs3.readFileSync(fullPath, "utf8");
|
|
506
1740
|
const parsed = parsePageSource(content);
|
|
507
1741
|
const inheritedMetadata = dependencyGraph ? dependencyGraph.getInheritedMetadata(fullPath) : {};
|
|
1742
|
+
let metadata = resolver ? resolvePageMetadata(content, fullPath, inheritedMetadata, { resolver, origin, file: fullPath }) : { ...inheritedMetadata, ...parsed.metadata };
|
|
1743
|
+
if (parsed.metadata.hasConflictingDeclarations) metadata.hasConflictingDeclarations = true;
|
|
1744
|
+
if (parsed.metadata.jsonLd) metadata.jsonLd = parsed.metadata.jsonLd;
|
|
1745
|
+
const headings = [...parsed.headings];
|
|
1746
|
+
if (!headings.some((h) => h.level === 1) && resolver) {
|
|
1747
|
+
headings.push(...discoverImportedH1(fullPath, resolver));
|
|
1748
|
+
}
|
|
1749
|
+
const dynamic = resolver ? dynamicParamsForFile(fullPath, resolver) : void 0;
|
|
1750
|
+
const inlineH1 = parsed.headings.filter((h) => h.level === 1);
|
|
1751
|
+
let maxConcurrentH1;
|
|
1752
|
+
if (inlineH1.length > 0) {
|
|
1753
|
+
const concurrency = computeH1Concurrency(content, inlineH1.map((h) => h.line || 0));
|
|
1754
|
+
maxConcurrentH1 = concurrency === null ? inlineH1.length : concurrency;
|
|
1755
|
+
} else {
|
|
1756
|
+
maxConcurrentH1 = headings.filter((h) => h.level === 1).length;
|
|
1757
|
+
}
|
|
508
1758
|
routes.push({
|
|
509
1759
|
route: routePath,
|
|
510
1760
|
filePath: fullPath,
|
|
511
|
-
metadata
|
|
512
|
-
|
|
513
|
-
...parsed.metadata
|
|
514
|
-
},
|
|
515
|
-
headings: parsed.headings,
|
|
1761
|
+
metadata,
|
|
1762
|
+
headings,
|
|
516
1763
|
images: parsed.images,
|
|
517
1764
|
links: parsed.links,
|
|
518
1765
|
textContent: parsed.textContent,
|
|
519
1766
|
hasLittleContent: parsed.hasLittleContent,
|
|
520
|
-
hasDynamicSegments: routePath.includes("[")
|
|
1767
|
+
hasDynamicSegments: routePath.includes("["),
|
|
1768
|
+
dynamicParams: dynamic?.dynamicParams,
|
|
1769
|
+
generatedParams: dynamic?.generatedParams,
|
|
1770
|
+
isRedirect: isRedirectOnly(content),
|
|
1771
|
+
maxConcurrentH1
|
|
521
1772
|
});
|
|
522
1773
|
}
|
|
523
1774
|
}
|
|
@@ -525,26 +1776,29 @@ var NextJsAdapter = class {
|
|
|
525
1776
|
};
|
|
526
1777
|
scanAppDir(appDir);
|
|
527
1778
|
}
|
|
528
|
-
if (
|
|
1779
|
+
if (fs3.existsSync(pagesDir)) {
|
|
529
1780
|
const scanPagesDir = (currentDir, relativePath = "") => {
|
|
530
|
-
const entries =
|
|
1781
|
+
const entries = fs3.readdirSync(currentDir, { withFileTypes: true });
|
|
531
1782
|
for (const entry of entries) {
|
|
532
|
-
const fullPath =
|
|
1783
|
+
const fullPath = path3.join(currentDir, entry.name);
|
|
533
1784
|
if (entry.isDirectory()) {
|
|
534
1785
|
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "api") {
|
|
535
1786
|
continue;
|
|
536
1787
|
}
|
|
537
|
-
scanPagesDir(fullPath,
|
|
1788
|
+
scanPagesDir(fullPath, path3.join(relativePath, entry.name));
|
|
538
1789
|
} else if (entry.isFile()) {
|
|
539
1790
|
if (/\.(tsx|jsx|js)$/.test(entry.name)) {
|
|
540
1791
|
const baseName = entry.name.replace(/\.(tsx|jsx|js)$/, "");
|
|
541
1792
|
if (baseName.startsWith("_") || baseName === "api") {
|
|
542
1793
|
continue;
|
|
543
1794
|
}
|
|
544
|
-
let routePath = `/${
|
|
1795
|
+
let routePath = `/${path3.join(relativePath, baseName === "index" ? "" : baseName).replace(/\\/g, "/")}`;
|
|
545
1796
|
if (routePath === "//" || routePath === "") routePath = "/";
|
|
546
|
-
const content =
|
|
1797
|
+
const content = fs3.readFileSync(fullPath, "utf8");
|
|
547
1798
|
const parsed = parsePageSource(content);
|
|
1799
|
+
const inlineH1 = parsed.headings.filter((h) => h.level === 1);
|
|
1800
|
+
const concurrency = computeH1Concurrency(content, inlineH1.map((h) => h.line || 0));
|
|
1801
|
+
const maxConcurrentH1 = concurrency === null ? inlineH1.length : concurrency;
|
|
548
1802
|
routes.push({
|
|
549
1803
|
route: routePath,
|
|
550
1804
|
filePath: fullPath,
|
|
@@ -554,7 +1808,8 @@ var NextJsAdapter = class {
|
|
|
554
1808
|
links: parsed.links,
|
|
555
1809
|
textContent: parsed.textContent,
|
|
556
1810
|
hasLittleContent: parsed.hasLittleContent,
|
|
557
|
-
hasDynamicSegments: routePath.includes("[")
|
|
1811
|
+
hasDynamicSegments: routePath.includes("["),
|
|
1812
|
+
maxConcurrentH1
|
|
558
1813
|
});
|
|
559
1814
|
}
|
|
560
1815
|
}
|
|
@@ -563,36 +1818,36 @@ var NextJsAdapter = class {
|
|
|
563
1818
|
scanPagesDir(pagesDir);
|
|
564
1819
|
}
|
|
565
1820
|
const sitemapCandidates = [
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
1821
|
+
path3.join(appDir, "sitemap.ts"),
|
|
1822
|
+
path3.join(appDir, "sitemap.js"),
|
|
1823
|
+
path3.join(projectRoot, "public", "sitemap.xml")
|
|
569
1824
|
];
|
|
570
1825
|
let sitemapFound = false;
|
|
571
1826
|
let sitemapMalformed = false;
|
|
572
1827
|
const sitemapUrls = [];
|
|
573
1828
|
for (const candidate of sitemapCandidates) {
|
|
574
|
-
if (
|
|
1829
|
+
if (fs3.existsSync(candidate)) {
|
|
575
1830
|
sitemapFound = true;
|
|
576
|
-
const content =
|
|
1831
|
+
const content = fs3.readFileSync(candidate, "utf8");
|
|
577
1832
|
if (candidate.endsWith(".xml") && (!/<urlset\b/i.test(content) || !/<loc>[^<]+<\/loc>/i.test(content))) {
|
|
578
1833
|
sitemapMalformed = true;
|
|
579
1834
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
1835
|
+
if (candidate.endsWith(".xml")) {
|
|
1836
|
+
for (const m of content.matchAll(/<loc>([^<]+)<\/loc>/gi)) sitemapUrls.push(m[1].trim());
|
|
1837
|
+
} else {
|
|
1838
|
+
resolveTsSitemapUrls(content, candidate, resolver, sitemapUrls, origin);
|
|
584
1839
|
}
|
|
585
1840
|
break;
|
|
586
1841
|
}
|
|
587
1842
|
}
|
|
588
1843
|
const robotsCandidates = [
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
1844
|
+
path3.join(appDir, "robots.ts"),
|
|
1845
|
+
path3.join(appDir, "robots.js"),
|
|
1846
|
+
path3.join(projectRoot, "public", "robots.txt")
|
|
592
1847
|
];
|
|
593
|
-
const robotsFile = robotsCandidates.find((candidate) =>
|
|
1848
|
+
const robotsFile = robotsCandidates.find((candidate) => fs3.existsSync(candidate));
|
|
594
1849
|
const robotsFound = Boolean(robotsFile);
|
|
595
|
-
const robotsContent = robotsFile ?
|
|
1850
|
+
const robotsContent = robotsFile ? fs3.readFileSync(robotsFile, "utf8") : "";
|
|
596
1851
|
routes.sort((a, b) => a.route.localeCompare(b.route));
|
|
597
1852
|
if (dependencyGraph) {
|
|
598
1853
|
dependencyGraph.setRoutes(routes);
|
|
@@ -613,10 +1868,64 @@ var NextJsAdapter = class {
|
|
|
613
1868
|
};
|
|
614
1869
|
}
|
|
615
1870
|
};
|
|
1871
|
+
function resolveProjectOrigin(config, resolver) {
|
|
1872
|
+
if (config.siteUrl) {
|
|
1873
|
+
try {
|
|
1874
|
+
const url = new URL(config.siteUrl);
|
|
1875
|
+
if (url.protocol === "http:" || url.protocol === "https:") return url.origin;
|
|
1876
|
+
} catch {
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
return resolver ? resolver.guessOrigin(config.siteUrl ? [config.siteUrl] : []) : void 0;
|
|
1880
|
+
}
|
|
1881
|
+
function isRedirectOnly(content) {
|
|
1882
|
+
const usesNextNavigation = /from\s+["']next\/navigation["']/.test(content);
|
|
1883
|
+
if (!usesNextNavigation) return false;
|
|
1884
|
+
const hasRedirectCall = /\b(redirect|permanentRedirect)\s*\(/.test(content);
|
|
1885
|
+
if (!hasRedirectCall) return false;
|
|
1886
|
+
return !/<[A-Za-z]/.test(content);
|
|
1887
|
+
}
|
|
1888
|
+
function resolveTsSitemapUrls(content, file, resolver, out, origin) {
|
|
1889
|
+
const foldWithOrigin = (raw) => {
|
|
1890
|
+
if (!resolver) return void 0;
|
|
1891
|
+
const direct = resolver.foldExpression(file, raw);
|
|
1892
|
+
if (direct.ok && typeof direct.value === "string" && direct.value) return direct.value;
|
|
1893
|
+
if (!origin) return void 0;
|
|
1894
|
+
const substituted = raw.replace(/\$\{\s*([A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*)*)\s*\}/g, (whole, inner) => {
|
|
1895
|
+
const last = inner.split(/\.|\s/).pop() || "";
|
|
1896
|
+
if (last === "url" || last === "origin" || last === "URL" || /^siteUrl$/i.test(inner)) return origin;
|
|
1897
|
+
return whole;
|
|
1898
|
+
}).replace(/\b(?:siteConfig|siteURL|siteUrl)\.url\b/g, origin);
|
|
1899
|
+
const retry = resolver.foldExpression(file, substituted);
|
|
1900
|
+
if (retry.ok && typeof retry.value === "string" && retry.value) return retry.value;
|
|
1901
|
+
return void 0;
|
|
1902
|
+
};
|
|
1903
|
+
const pushExpressionUrl = (start) => {
|
|
1904
|
+
const i = skipTrivia(content, start);
|
|
1905
|
+
if (i >= content.length) return;
|
|
1906
|
+
const { end } = parseValueAt(content, i);
|
|
1907
|
+
if (end <= i) return;
|
|
1908
|
+
const raw = content.slice(i, end).trim();
|
|
1909
|
+
const value = foldWithOrigin(raw);
|
|
1910
|
+
if (value) out.push(value);
|
|
1911
|
+
};
|
|
1912
|
+
let search = 0;
|
|
1913
|
+
while (search < content.length) {
|
|
1914
|
+
const idx = content.indexOf("url:", search);
|
|
1915
|
+
if (idx === -1) break;
|
|
1916
|
+
const before = content.slice(Math.max(0, idx - 1), idx);
|
|
1917
|
+
if (!/[{,:\s]/.test(before) || /[A-Za-z0-9_$]/.test(before)) {
|
|
1918
|
+
search = idx + 1;
|
|
1919
|
+
continue;
|
|
1920
|
+
}
|
|
1921
|
+
pushExpressionUrl(idx + "url:".length);
|
|
1922
|
+
search = idx + "url:".length;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
616
1925
|
|
|
617
1926
|
// ../next-adapter/src/framework-adapter.ts
|
|
618
|
-
import
|
|
619
|
-
import
|
|
1927
|
+
import fs4 from "node:fs";
|
|
1928
|
+
import path4 from "node:path";
|
|
620
1929
|
var INHERITED_METADATA_FIELDS = ["title", "description", "canonical", "robots"];
|
|
621
1930
|
var LABELS = {
|
|
622
1931
|
nextjs: "Next.js",
|
|
@@ -629,23 +1938,23 @@ var LABELS = {
|
|
|
629
1938
|
};
|
|
630
1939
|
function dependencies(root) {
|
|
631
1940
|
try {
|
|
632
|
-
const pkg = JSON.parse(
|
|
1941
|
+
const pkg = JSON.parse(fs4.readFileSync(path4.join(root, "package.json"), "utf8"));
|
|
633
1942
|
return { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
|
|
634
1943
|
} catch {
|
|
635
1944
|
return {};
|
|
636
1945
|
}
|
|
637
1946
|
}
|
|
638
1947
|
function walk(dir, visit) {
|
|
639
|
-
if (!
|
|
640
|
-
for (const entry of
|
|
1948
|
+
if (!fs4.existsSync(dir)) return;
|
|
1949
|
+
for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
|
|
641
1950
|
if (entry.name.startsWith(".") || ["node_modules", "dist", "build", ".output"].includes(entry.name)) continue;
|
|
642
|
-
const full =
|
|
1951
|
+
const full = path4.join(dir, entry.name);
|
|
643
1952
|
if (entry.isDirectory()) walk(full, visit);
|
|
644
1953
|
else if (entry.isFile()) visit(full);
|
|
645
1954
|
}
|
|
646
1955
|
}
|
|
647
1956
|
function routeNode(file, route, framework) {
|
|
648
|
-
const parsed = parsePageSource(
|
|
1957
|
+
const parsed = parsePageSource(fs4.readFileSync(file, "utf8"), framework);
|
|
649
1958
|
return { route, filePath: file, ...parsed, hasDynamicSegments: /[:[*]/.test(route) };
|
|
650
1959
|
}
|
|
651
1960
|
function cleanRoute(value) {
|
|
@@ -657,27 +1966,27 @@ function discoverRoutes(root, framework) {
|
|
|
657
1966
|
const routes = [];
|
|
658
1967
|
const add = (base, extensions, convert) => {
|
|
659
1968
|
walk(base, (file) => {
|
|
660
|
-
const relative =
|
|
1969
|
+
const relative = path4.relative(base, file);
|
|
661
1970
|
if (!extensions.test(relative) || /(^|\/)api(\/|\.|$)/.test(relative)) return;
|
|
662
1971
|
routes.push(routeNode(file, convert(relative), framework));
|
|
663
1972
|
});
|
|
664
1973
|
};
|
|
665
1974
|
if (framework === "nuxt") {
|
|
666
|
-
const base =
|
|
1975
|
+
const base = fs4.existsSync(path4.join(root, "pages")) ? path4.join(root, "pages") : path4.join(root, "app", "pages");
|
|
667
1976
|
add(base, /\.vue$/, (r) => cleanRoute(r.replace(/\.vue$/, "").replace(/\[\.\.\.([^\]]+)\]/g, "*$1").replace(/\[([^\]]+)\]/g, ":$1")));
|
|
668
1977
|
} else if (framework === "sveltekit") {
|
|
669
|
-
const base =
|
|
1978
|
+
const base = path4.join(root, "src", "routes");
|
|
670
1979
|
add(base, /(^|\/)\+page\.svelte$/, (r) => cleanRoute(r.replace(/\/\+page\.svelte$/, "").replace(/^\+page\.svelte$/, "")));
|
|
671
1980
|
} else if (framework === "astro") {
|
|
672
|
-
const base =
|
|
1981
|
+
const base = path4.join(root, "src", "pages");
|
|
673
1982
|
add(base, /\.(astro|md|mdx)$/, (r) => cleanRoute(r.replace(/\.(astro|md|mdx)$/, "")));
|
|
674
1983
|
} else if (framework === "remix") {
|
|
675
|
-
const base =
|
|
1984
|
+
const base = path4.join(root, "app", "routes");
|
|
676
1985
|
add(base, /\.(tsx|jsx|ts|js)$/, (r) => cleanRoute(r.replace(/\.(tsx|jsx|ts|js)$/, "").replace(/\._index$/, "").replace(/^_index$/, "").replace(/\./g, "/").replace(/\$([^/]+)/g, ":$1")));
|
|
677
1986
|
} else if (framework === "vite") {
|
|
678
1987
|
for (const candidate of ["src/App.tsx", "src/App.jsx", "src/App.vue", "src/App.svelte", "index.html"]) {
|
|
679
|
-
const file =
|
|
680
|
-
if (
|
|
1988
|
+
const file = path4.join(root, candidate);
|
|
1989
|
+
if (fs4.existsSync(file)) routes.push(routeNode(file, "/", framework));
|
|
681
1990
|
}
|
|
682
1991
|
} else {
|
|
683
1992
|
add(root, /\.html?$/, (r) => cleanRoute(r.replace(/\.html?$/, "")));
|
|
@@ -687,30 +1996,30 @@ function discoverRoutes(root, framework) {
|
|
|
687
1996
|
}
|
|
688
1997
|
function svelteKitLayouts(root) {
|
|
689
1998
|
const layoutByDir = /* @__PURE__ */ new Map();
|
|
690
|
-
const base =
|
|
1999
|
+
const base = path4.join(root, "src", "routes");
|
|
691
2000
|
const readLayout = (dir) => {
|
|
692
|
-
const layoutFile =
|
|
693
|
-
if (
|
|
694
|
-
layoutByDir.set(dir, parsePageSource(
|
|
2001
|
+
const layoutFile = path4.join(dir, "+layout.svelte");
|
|
2002
|
+
if (fs4.existsSync(layoutFile)) {
|
|
2003
|
+
layoutByDir.set(dir, parsePageSource(fs4.readFileSync(layoutFile, "utf8"), "sveltekit").metadata);
|
|
695
2004
|
}
|
|
696
|
-
for (const entry of
|
|
2005
|
+
for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
|
|
697
2006
|
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
698
|
-
readLayout(
|
|
2007
|
+
readLayout(path4.join(dir, entry.name));
|
|
699
2008
|
}
|
|
700
2009
|
}
|
|
701
2010
|
};
|
|
702
|
-
if (
|
|
2011
|
+
if (fs4.existsSync(base)) readLayout(base);
|
|
703
2012
|
return layoutByDir;
|
|
704
2013
|
}
|
|
705
2014
|
function mergeLayoutMetadata(routes, framework, root) {
|
|
706
2015
|
if (framework !== "sveltekit") return routes;
|
|
707
2016
|
const layoutByDir = svelteKitLayouts(root);
|
|
708
2017
|
if (layoutByDir.size === 0) return routes;
|
|
709
|
-
const base =
|
|
2018
|
+
const base = path4.join(root, "src", "routes");
|
|
710
2019
|
return routes.map((route) => {
|
|
711
2020
|
const merged = {};
|
|
712
|
-
let dir =
|
|
713
|
-
while (dir === base || dir.startsWith(`${base}${
|
|
2021
|
+
let dir = path4.dirname(route.filePath);
|
|
2022
|
+
while (dir === base || dir.startsWith(`${base}${path4.sep}`)) {
|
|
714
2023
|
const meta = layoutByDir.get(dir);
|
|
715
2024
|
if (meta) {
|
|
716
2025
|
for (const field of INHERITED_METADATA_FIELDS) {
|
|
@@ -718,7 +2027,7 @@ function mergeLayoutMetadata(routes, framework, root) {
|
|
|
718
2027
|
}
|
|
719
2028
|
}
|
|
720
2029
|
if (dir === base) break;
|
|
721
|
-
dir =
|
|
2030
|
+
dir = path4.dirname(dir);
|
|
722
2031
|
}
|
|
723
2032
|
if (Object.keys(merged).length === 0) return route;
|
|
724
2033
|
for (const field of INHERITED_METADATA_FIELDS) {
|
|
@@ -728,16 +2037,16 @@ function mergeLayoutMetadata(routes, framework, root) {
|
|
|
728
2037
|
});
|
|
729
2038
|
}
|
|
730
2039
|
function crawlFiles(root) {
|
|
731
|
-
const roots = [
|
|
732
|
-
const sitemap = roots.map((dir) =>
|
|
733
|
-
const robots = roots.map((dir) =>
|
|
734
|
-
const sitemapContent = sitemap ?
|
|
2040
|
+
const roots = [path4.join(root, "public"), path4.join(root, "static"), root];
|
|
2041
|
+
const sitemap = roots.map((dir) => path4.join(dir, "sitemap.xml")).find(fs4.existsSync);
|
|
2042
|
+
const robots = roots.map((dir) => path4.join(dir, "robots.txt")).find(fs4.existsSync);
|
|
2043
|
+
const sitemapContent = sitemap ? fs4.readFileSync(sitemap, "utf8") : "";
|
|
735
2044
|
return {
|
|
736
2045
|
sitemapFound: Boolean(sitemap),
|
|
737
2046
|
sitemapUrls: [...sitemapContent.matchAll(/<loc>([^<]+)<\/loc>/gi)].map((m) => m[1].trim()),
|
|
738
2047
|
sitemapMalformed: Boolean(sitemap && (!/<urlset\b/i.test(sitemapContent) || !/<loc>[^<]+<\/loc>/i.test(sitemapContent))),
|
|
739
2048
|
robotsFound: Boolean(robots),
|
|
740
|
-
robotsContent: robots ?
|
|
2049
|
+
robotsContent: robots ? fs4.readFileSync(robots, "utf8") : ""
|
|
741
2050
|
};
|
|
742
2051
|
}
|
|
743
2052
|
var FrameworkAdapter = class {
|
|
@@ -750,11 +2059,11 @@ var FrameworkAdapter = class {
|
|
|
750
2059
|
if (deps["@remix-run/react"] || deps["@remix-run/node"]) return "remix";
|
|
751
2060
|
if (deps.vite) return "vite";
|
|
752
2061
|
if (await NextJsAdapter.detect(root)) return "nextjs";
|
|
753
|
-
if (
|
|
754
|
-
if (
|
|
2062
|
+
if (fs4.existsSync(path4.join(root, "nuxt.config.ts")) || fs4.existsSync(path4.join(root, "nuxt.config.js"))) return "nuxt";
|
|
2063
|
+
if (fs4.existsSync(path4.join(root, "svelte.config.js")) && fs4.existsSync(path4.join(root, "src", "routes"))) {
|
|
755
2064
|
return "sveltekit";
|
|
756
2065
|
}
|
|
757
|
-
if (
|
|
2066
|
+
if (fs4.existsSync(path4.join(root, "index.html"))) return "static";
|
|
758
2067
|
return null;
|
|
759
2068
|
}
|
|
760
2069
|
static async scan(root) {
|
|
@@ -790,8 +2099,12 @@ var metadataTitleRule = {
|
|
|
790
2099
|
const findings = [];
|
|
791
2100
|
const titleMap = /* @__PURE__ */ new Map();
|
|
792
2101
|
for (const route of context.routes) {
|
|
2102
|
+
if (route.isRedirect) continue;
|
|
793
2103
|
const title = route.metadata.title;
|
|
2104
|
+
const dynamic = route.metadata.dynamicMetadata === true;
|
|
2105
|
+
const robots = (route.metadata.robots || "").toLowerCase();
|
|
794
2106
|
if (!title || title.trim() === "") {
|
|
2107
|
+
if (route.metadata.titleDeclared || dynamic) continue;
|
|
795
2108
|
findings.push({
|
|
796
2109
|
id: `title-missing-${route.route}`,
|
|
797
2110
|
rule: "metadata-title",
|
|
@@ -806,9 +2119,13 @@ var metadataTitleRule = {
|
|
|
806
2119
|
continue;
|
|
807
2120
|
}
|
|
808
2121
|
const trimmed = title.trim();
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
2122
|
+
const indexable = !robots.includes("noindex");
|
|
2123
|
+
if (route.metadata.titleDeclared && !dynamic && indexable) {
|
|
2124
|
+
const existing = titleMap.get(trimmed) || [];
|
|
2125
|
+
existing.push(route.route);
|
|
2126
|
+
titleMap.set(trimmed, existing);
|
|
2127
|
+
}
|
|
2128
|
+
if (dynamic) continue;
|
|
812
2129
|
if (trimmed.length < 10) {
|
|
813
2130
|
findings.push({
|
|
814
2131
|
id: `title-short-${route.route}`,
|
|
@@ -864,8 +2181,12 @@ var metadataDescriptionRule = {
|
|
|
864
2181
|
const findings = [];
|
|
865
2182
|
const descMap = /* @__PURE__ */ new Map();
|
|
866
2183
|
for (const route of context.routes) {
|
|
2184
|
+
if (route.isRedirect) continue;
|
|
867
2185
|
const desc = route.metadata.description;
|
|
2186
|
+
const dynamic = route.metadata.dynamicMetadata === true;
|
|
2187
|
+
const robots = (route.metadata.robots || "").toLowerCase();
|
|
868
2188
|
if (!desc || desc.trim() === "") {
|
|
2189
|
+
if (route.metadata.descriptionDeclared || dynamic) continue;
|
|
869
2190
|
findings.push({
|
|
870
2191
|
id: `desc-missing-${route.route}`,
|
|
871
2192
|
rule: "metadata-description",
|
|
@@ -880,9 +2201,13 @@ var metadataDescriptionRule = {
|
|
|
880
2201
|
continue;
|
|
881
2202
|
}
|
|
882
2203
|
const trimmed = desc.trim();
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
2204
|
+
const indexable = !robots.includes("noindex");
|
|
2205
|
+
if (route.metadata.descriptionDeclared && !dynamic && indexable) {
|
|
2206
|
+
const existing = descMap.get(trimmed) || [];
|
|
2207
|
+
existing.push(route.route);
|
|
2208
|
+
descMap.set(trimmed, existing);
|
|
2209
|
+
}
|
|
2210
|
+
if (dynamic) continue;
|
|
886
2211
|
if (trimmed.length < 50) {
|
|
887
2212
|
findings.push({
|
|
888
2213
|
id: `desc-short-${route.route}`,
|
|
@@ -956,8 +2281,12 @@ var canonicalRule = {
|
|
|
956
2281
|
}
|
|
957
2282
|
}
|
|
958
2283
|
for (const route of context.routes) {
|
|
2284
|
+
if (route.isRedirect) continue;
|
|
2285
|
+
if ((route.metadata.robots || "").toLowerCase().includes("noindex")) continue;
|
|
2286
|
+
if (route.metadata.dynamicMetadata) continue;
|
|
959
2287
|
const canonical = route.metadata.canonical;
|
|
960
2288
|
if (!canonical || canonical.trim() === "") {
|
|
2289
|
+
if (route.metadata.canonicalDeclared) continue;
|
|
961
2290
|
findings.push({
|
|
962
2291
|
id: `canonical-missing-${route.route}`,
|
|
963
2292
|
rule: "canonical",
|
|
@@ -1031,6 +2360,20 @@ var canonicalRule = {
|
|
|
1031
2360
|
}
|
|
1032
2361
|
}
|
|
1033
2362
|
} catch {
|
|
2363
|
+
const looksLikePath = canonical.startsWith("/");
|
|
2364
|
+
if (looksLikePath) {
|
|
2365
|
+
findings.push({
|
|
2366
|
+
id: `canonical-relative-no-base-${route.route}`,
|
|
2367
|
+
rule: "canonical",
|
|
2368
|
+
severity: "warning",
|
|
2369
|
+
category: "technical",
|
|
2370
|
+
message: `Canonical URL "${canonical}" on route "${route.route}" is relative and no siteUrl/metadataBase was found to resolve it.`,
|
|
2371
|
+
file: route.filePath,
|
|
2372
|
+
route: route.route,
|
|
2373
|
+
fixable: false
|
|
2374
|
+
});
|
|
2375
|
+
continue;
|
|
2376
|
+
}
|
|
1034
2377
|
findings.push({
|
|
1035
2378
|
id: `canonical-malformed-${route.route}`,
|
|
1036
2379
|
rule: "canonical",
|
|
@@ -1069,6 +2412,7 @@ var headingsRule = {
|
|
|
1069
2412
|
analyze(context) {
|
|
1070
2413
|
const findings = [];
|
|
1071
2414
|
for (const route of context.routes) {
|
|
2415
|
+
if (route.isRedirect) continue;
|
|
1072
2416
|
const headings = route.headings;
|
|
1073
2417
|
const h1s = headings.filter((h) => h.level === 1);
|
|
1074
2418
|
if (h1s.length === 0) {
|
|
@@ -1083,19 +2427,22 @@ var headingsRule = {
|
|
|
1083
2427
|
fixable: false,
|
|
1084
2428
|
explanation: "Every page should have exactly one <h1> element defining its primary topic for search engines and accessibility."
|
|
1085
2429
|
});
|
|
1086
|
-
} else
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
2430
|
+
} else {
|
|
2431
|
+
const effectiveCount = route.maxConcurrentH1 !== void 0 ? Math.max(route.maxConcurrentH1, h1s.length > 0 ? 1 : 0) : h1s.length;
|
|
2432
|
+
if (effectiveCount > 1) {
|
|
2433
|
+
findings.push({
|
|
2434
|
+
id: `heading-multiple-h1-${route.route}`,
|
|
2435
|
+
rule: "headings",
|
|
2436
|
+
severity: "warning",
|
|
2437
|
+
category: "content",
|
|
2438
|
+
message: `Found ${effectiveCount} <h1> tags on route "${route.route}". Best practice is a single prominent <h1>.`,
|
|
2439
|
+
file: route.filePath,
|
|
2440
|
+
line: h1s[1].line,
|
|
2441
|
+
route: route.route,
|
|
2442
|
+
fixable: false,
|
|
2443
|
+
explanation: "Multiple <h1> tags can dilute topical hierarchy and confuse screen readers."
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
1099
2446
|
}
|
|
1100
2447
|
let prevLevel = 1;
|
|
1101
2448
|
for (const h of headings) {
|
|
@@ -1157,7 +2504,9 @@ var imagesRule = {
|
|
|
1157
2504
|
// Never invent alt descriptions without AI
|
|
1158
2505
|
explanation: "Missing alt attributes harm accessibility and prevent images from ranking in Google Image Search."
|
|
1159
2506
|
});
|
|
1160
|
-
} else if (img.alt.trim() === "" && !img.src.includes("icon") && !img.src.includes("decorative"))
|
|
2507
|
+
} else if (img.alt.trim() === "" && !img.src.includes("icon") && !img.src.includes("decorative") && // A runtime/dynamic src (e.g. favicon from data) cannot be judged;
|
|
2508
|
+
// empty alt for it is commonly the intended decorative usage.
|
|
2509
|
+
!img.src.includes("unknown-image")) {
|
|
1161
2510
|
findings.push({
|
|
1162
2511
|
id: `img-empty-alt-${route.route}-${img.src}`,
|
|
1163
2512
|
rule: "images",
|
|
@@ -1177,6 +2526,44 @@ var imagesRule = {
|
|
|
1177
2526
|
}
|
|
1178
2527
|
};
|
|
1179
2528
|
|
|
2529
|
+
// ../core/src/route-match.ts
|
|
2530
|
+
function segmentsOf(route) {
|
|
2531
|
+
return route.split("/").filter((segment) => segment !== "");
|
|
2532
|
+
}
|
|
2533
|
+
function normalizeHref(href) {
|
|
2534
|
+
const clean = href.split("?")[0].split("#")[0];
|
|
2535
|
+
const withoutSlash = clean.length > 1 && clean.endsWith("/") ? clean.slice(0, -1) : clean;
|
|
2536
|
+
return withoutSlash === "" ? "/" : withoutSlash;
|
|
2537
|
+
}
|
|
2538
|
+
function isCatchAllSegment(segment) {
|
|
2539
|
+
return segment.startsWith("[...") || segment.startsWith("[[...");
|
|
2540
|
+
}
|
|
2541
|
+
function isDynamicSegment(segment) {
|
|
2542
|
+
return segment.startsWith("[") && segment.endsWith("]") || isCatchAllSegment(segment);
|
|
2543
|
+
}
|
|
2544
|
+
function routePatternMatches(pattern, href) {
|
|
2545
|
+
const patSegs = segmentsOf(pattern);
|
|
2546
|
+
const hrefSegs = segmentsOf(href);
|
|
2547
|
+
let hi = 0;
|
|
2548
|
+
for (let pi = 0; pi < patSegs.length; pi += 1) {
|
|
2549
|
+
const seg = patSegs[pi];
|
|
2550
|
+
if (seg.startsWith("[[...") && seg.endsWith("]")) {
|
|
2551
|
+
return true;
|
|
2552
|
+
}
|
|
2553
|
+
if (seg.startsWith("[...") && seg.endsWith("]")) {
|
|
2554
|
+
return hi < hrefSegs.length;
|
|
2555
|
+
}
|
|
2556
|
+
if (hi >= hrefSegs.length) return false;
|
|
2557
|
+
if (isDynamicSegment(seg)) {
|
|
2558
|
+
hi += 1;
|
|
2559
|
+
continue;
|
|
2560
|
+
}
|
|
2561
|
+
if (seg !== hrefSegs[hi]) return false;
|
|
2562
|
+
hi += 1;
|
|
2563
|
+
}
|
|
2564
|
+
return hi === hrefSegs.length;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
1180
2567
|
// ../core/src/rules/links.ts
|
|
1181
2568
|
var linksRule = {
|
|
1182
2569
|
id: "links",
|
|
@@ -1184,11 +2571,29 @@ var linksRule = {
|
|
|
1184
2571
|
category: "links",
|
|
1185
2572
|
analyze(context) {
|
|
1186
2573
|
const findings = [];
|
|
1187
|
-
const validRoutes = new Set(context.routes.map((r) => r.route));
|
|
1188
2574
|
const normalizeRoute2 = (r) => r.endsWith("/") && r.length > 1 ? r.slice(0, -1) : r;
|
|
1189
|
-
const
|
|
2575
|
+
const validRoutes = new Set(context.routes.map((r) => normalizeRoute2(r.route)));
|
|
1190
2576
|
const redirects = new Map((context.redirects || []).map((redirect) => [normalizeRoute2(redirect.source), redirect.destination]));
|
|
2577
|
+
const dynamicRoutes = context.routes.filter((r) => r.hasDynamicSegments);
|
|
2578
|
+
const dynamicByPattern = new Map(dynamicRoutes.map((r) => [normalizeRoute2(r.route), r]));
|
|
2579
|
+
const isDynamicReachable = (cleanHref) => {
|
|
2580
|
+
for (const [pattern, routeNode2] of dynamicByPattern) {
|
|
2581
|
+
if (routePatternMatches(pattern, cleanHref)) {
|
|
2582
|
+
if (routeNode2.dynamicParams !== false) return { reachable: true, route: routeNode2 };
|
|
2583
|
+
const candidates = new Set(routeNode2.generatedParams || []);
|
|
2584
|
+
const targetSegment = cleanHref.split("/").pop() || "";
|
|
2585
|
+
if (targetSegment !== "" && candidates.has(targetSegment)) return { reachable: true, route: routeNode2 };
|
|
2586
|
+
return {
|
|
2587
|
+
reachable: false,
|
|
2588
|
+
route: routeNode2,
|
|
2589
|
+
reason: `route "${pattern}" only renders its generateStaticParams output and "${targetSegment}" could not be verified`
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
return { reachable: false };
|
|
2594
|
+
};
|
|
1191
2595
|
for (const route of context.routes) {
|
|
2596
|
+
if (route.isRedirect) continue;
|
|
1192
2597
|
for (const link of route.links) {
|
|
1193
2598
|
if (!link.isInternal) continue;
|
|
1194
2599
|
const href = link.href.trim();
|
|
@@ -1207,7 +2612,7 @@ var linksRule = {
|
|
|
1207
2612
|
});
|
|
1208
2613
|
continue;
|
|
1209
2614
|
}
|
|
1210
|
-
const cleanHref =
|
|
2615
|
+
const cleanHref = normalizeHref(href);
|
|
1211
2616
|
if (cleanHref.startsWith("/")) {
|
|
1212
2617
|
const redirectTarget = redirects.get(cleanHref);
|
|
1213
2618
|
if (redirectTarget) {
|
|
@@ -1223,10 +2628,28 @@ var linksRule = {
|
|
|
1223
2628
|
fixable: false,
|
|
1224
2629
|
explanation: "Link directly to the final internal route to avoid unnecessary crawl hops."
|
|
1225
2630
|
});
|
|
2631
|
+
continue;
|
|
1226
2632
|
}
|
|
1227
|
-
if (!
|
|
2633
|
+
if (!validRoutes.has(cleanHref)) {
|
|
2634
|
+
const dynamic = isDynamicReachable(cleanHref);
|
|
2635
|
+
if (dynamic.reachable) continue;
|
|
2636
|
+
if (dynamic.route) {
|
|
2637
|
+
findings.push({
|
|
2638
|
+
id: `link-unverifiable-dynamic-${route.route}-${cleanHref}`,
|
|
2639
|
+
rule: "links",
|
|
2640
|
+
severity: "warning",
|
|
2641
|
+
category: "links",
|
|
2642
|
+
message: `Internal link "${href}" on "${route.route}" targets dynamic ${dynamic.route.route}, but ${dynamic.reason}.`,
|
|
2643
|
+
file: route.filePath,
|
|
2644
|
+
line: link.line,
|
|
2645
|
+
route: route.route,
|
|
2646
|
+
fixable: false,
|
|
2647
|
+
explanation: "The target route renders only its generateStaticParams output; this link cannot be statically verified as reachable."
|
|
2648
|
+
});
|
|
2649
|
+
continue;
|
|
2650
|
+
}
|
|
1228
2651
|
let isTypoFixable = false;
|
|
1229
|
-
for (const valid of
|
|
2652
|
+
for (const valid of validRoutes) {
|
|
1230
2653
|
if (valid.toLowerCase() === cleanHref.toLowerCase()) {
|
|
1231
2654
|
isTypoFixable = true;
|
|
1232
2655
|
break;
|
|
@@ -1324,11 +2747,13 @@ var crawlabilityRule = {
|
|
|
1324
2747
|
});
|
|
1325
2748
|
}
|
|
1326
2749
|
const validRoutes = new Set(context.routes.map((r) => r.route));
|
|
2750
|
+
const dynamicPatterns = context.routes.filter((r) => r.hasDynamicSegments).map((r) => r.route);
|
|
1327
2751
|
for (const url of context.sitemapUrls) {
|
|
1328
2752
|
try {
|
|
1329
2753
|
const pathname = url.startsWith("http") ? new URL(url).pathname : url;
|
|
1330
|
-
const clean =
|
|
1331
|
-
|
|
2754
|
+
const clean = normalizeHref(pathname);
|
|
2755
|
+
const isKnown = validRoutes.has(clean) || dynamicPatterns.some((pattern) => routePatternMatches(pattern, clean));
|
|
2756
|
+
if (!isKnown && clean !== "" && clean !== "/") {
|
|
1332
2757
|
findings.push({
|
|
1333
2758
|
id: `sitemap-orphan-url-${clean}`,
|
|
1334
2759
|
rule: "crawlability",
|
|
@@ -1637,7 +3062,7 @@ function buildLinkGraph(routes, redirects = []) {
|
|
|
1637
3062
|
outgoingCount[node] = 0;
|
|
1638
3063
|
adjacencyList[node] = [];
|
|
1639
3064
|
}
|
|
1640
|
-
const normalize = (
|
|
3065
|
+
const normalize = (path10) => path10.endsWith("/") && path10.length > 1 ? path10.slice(0, -1) : path10;
|
|
1641
3066
|
const redirectMap = new Map(redirects.map((redirect) => [normalize(redirect.source), normalize(redirect.destination)]));
|
|
1642
3067
|
for (const route of routes) {
|
|
1643
3068
|
const source = route.route;
|
|
@@ -1903,8 +3328,8 @@ function detectContentOpportunities(routes, providedKeywords = {}) {
|
|
|
1903
3328
|
}
|
|
1904
3329
|
|
|
1905
3330
|
// ../core/src/fixer.ts
|
|
1906
|
-
import
|
|
1907
|
-
import
|
|
3331
|
+
import path5 from "node:path";
|
|
3332
|
+
import fs5 from "node:fs";
|
|
1908
3333
|
function createUnifiedDiff(filename, oldText, newText) {
|
|
1909
3334
|
const oldLines = oldText ? oldText.split("\n") : [];
|
|
1910
3335
|
const newLines = newText ? newText.split("\n") : [];
|
|
@@ -1958,7 +3383,7 @@ function generateRevertSafeFix(options) {
|
|
|
1958
3383
|
filePath,
|
|
1959
3384
|
originalContent: headContent,
|
|
1960
3385
|
newContent: updated,
|
|
1961
|
-
diff: createUnifiedDiff(
|
|
3386
|
+
diff: createUnifiedDiff(path5.relative(projectRoot, filePath), headContent, updated),
|
|
1962
3387
|
description: `Restored canonical value "${canonicalVal}" from BASE revision.`
|
|
1963
3388
|
};
|
|
1964
3389
|
}
|
|
@@ -1973,7 +3398,7 @@ function generateRevertSafeFix(options) {
|
|
|
1973
3398
|
filePath,
|
|
1974
3399
|
originalContent: headContent,
|
|
1975
3400
|
newContent: updated,
|
|
1976
|
-
diff: createUnifiedDiff(
|
|
3401
|
+
diff: createUnifiedDiff(path5.relative(projectRoot, filePath), headContent, updated),
|
|
1977
3402
|
description: "Restored indexable robots directive present in BASE revision."
|
|
1978
3403
|
};
|
|
1979
3404
|
}
|
|
@@ -2072,19 +3497,19 @@ ${content}`;
|
|
|
2072
3497
|
}
|
|
2073
3498
|
function assetLayout(projectRoot, framework, isAppRouter) {
|
|
2074
3499
|
if (framework === "nextjs" && isAppRouter) {
|
|
2075
|
-
const appDirectory =
|
|
3500
|
+
const appDirectory = fs5.existsSync(path5.join(projectRoot, "app")) ? path5.join(projectRoot, "app") : path5.join(projectRoot, "src", "app");
|
|
2076
3501
|
return { kind: "app-router", directory: appDirectory };
|
|
2077
3502
|
}
|
|
2078
3503
|
let directory;
|
|
2079
3504
|
switch (framework) {
|
|
2080
3505
|
case "sveltekit":
|
|
2081
|
-
directory =
|
|
3506
|
+
directory = path5.join(projectRoot, "static");
|
|
2082
3507
|
break;
|
|
2083
3508
|
case "static":
|
|
2084
3509
|
directory = projectRoot;
|
|
2085
3510
|
break;
|
|
2086
3511
|
default:
|
|
2087
|
-
directory =
|
|
3512
|
+
directory = path5.join(projectRoot, "public");
|
|
2088
3513
|
}
|
|
2089
3514
|
return { kind: "file", directory };
|
|
2090
3515
|
}
|
|
@@ -2115,21 +3540,21 @@ async function applySafeFixes(options) {
|
|
|
2115
3540
|
const fixedFindingIds = /* @__PURE__ */ new Set();
|
|
2116
3541
|
const virtualFiles = /* @__PURE__ */ new Map();
|
|
2117
3542
|
const safeFile = (candidate) => {
|
|
2118
|
-
const root =
|
|
2119
|
-
const resolved =
|
|
2120
|
-
return resolved === root || resolved.startsWith(`${root}${
|
|
3543
|
+
const root = path5.resolve(projectRoot);
|
|
3544
|
+
const resolved = path5.resolve(candidate);
|
|
3545
|
+
return resolved === root || resolved.startsWith(`${root}${path5.sep}`) ? resolved : null;
|
|
2121
3546
|
};
|
|
2122
3547
|
const readCurrent = (candidate) => {
|
|
2123
3548
|
if (virtualFiles.has(candidate)) return virtualFiles.get(candidate);
|
|
2124
|
-
return
|
|
3549
|
+
return fs5.existsSync(candidate) ? fs5.readFileSync(candidate, "utf8") : "";
|
|
2125
3550
|
};
|
|
2126
3551
|
const recordChange = (change, findingId) => {
|
|
2127
3552
|
appliedChanges.push(change);
|
|
2128
3553
|
fixedFindingIds.add(findingId);
|
|
2129
3554
|
virtualFiles.set(change.filePath, change.newContent);
|
|
2130
3555
|
if (!dryRun) {
|
|
2131
|
-
|
|
2132
|
-
|
|
3556
|
+
fs5.mkdirSync(path5.dirname(change.filePath), { recursive: true });
|
|
3557
|
+
fs5.writeFileSync(change.filePath, change.newContent, "utf8");
|
|
2133
3558
|
}
|
|
2134
3559
|
};
|
|
2135
3560
|
const layout = assetLayout(projectRoot, framework, isAppRouter);
|
|
@@ -2140,7 +3565,7 @@ async function applySafeFixes(options) {
|
|
|
2140
3565
|
for (const f of revertSafeFindings) {
|
|
2141
3566
|
const targetFile = f.file ? safeFile(f.file) : null;
|
|
2142
3567
|
if (!targetFile) continue;
|
|
2143
|
-
const relPath =
|
|
3568
|
+
const relPath = path5.relative(projectRoot, targetFile).replace(/\\/g, "/");
|
|
2144
3569
|
const baseContent = options.baseFiles.get(relPath) || options.baseFiles.get(targetFile);
|
|
2145
3570
|
if (!baseContent) continue;
|
|
2146
3571
|
const headContent = readCurrent(targetFile);
|
|
@@ -2159,9 +3584,9 @@ async function applySafeFixes(options) {
|
|
|
2159
3584
|
const robotsFinding = findings.find((f) => f.rule === "crawlability" && f.id === "robots-missing");
|
|
2160
3585
|
if (robotsFinding) {
|
|
2161
3586
|
if (layout.kind === "app-router") {
|
|
2162
|
-
const robotsFilePath =
|
|
2163
|
-
const relativePath =
|
|
2164
|
-
const oldContent =
|
|
3587
|
+
const robotsFilePath = path5.join(layout.directory, "robots.ts");
|
|
3588
|
+
const relativePath = path5.relative(projectRoot, robotsFilePath);
|
|
3589
|
+
const oldContent = fs5.existsSync(robotsFilePath) ? fs5.readFileSync(robotsFilePath, "utf8") : "";
|
|
2165
3590
|
const sitemapLine = siteUrl ? `
|
|
2166
3591
|
sitemap: '${siteUrl}/sitemap.xml',` : "";
|
|
2167
3592
|
const newContent = `import { MetadataRoute } from 'next';
|
|
@@ -2185,9 +3610,9 @@ export default function robots(): MetadataRoute.Robots {
|
|
|
2185
3610
|
description: "Generated app/robots.ts with standard crawler rules and sitemap reference."
|
|
2186
3611
|
}, robotsFinding.id);
|
|
2187
3612
|
} else {
|
|
2188
|
-
const robotsFilePath =
|
|
2189
|
-
const relativePath =
|
|
2190
|
-
const oldContent =
|
|
3613
|
+
const robotsFilePath = path5.join(layout.directory, "robots.txt");
|
|
3614
|
+
const relativePath = path5.relative(projectRoot, robotsFilePath);
|
|
3615
|
+
const oldContent = fs5.existsSync(robotsFilePath) ? fs5.readFileSync(robotsFilePath, "utf8") : "";
|
|
2191
3616
|
const newContent = ROBOTS_TXT(siteUrl);
|
|
2192
3617
|
recordChange({
|
|
2193
3618
|
filePath: robotsFilePath,
|
|
@@ -2203,9 +3628,9 @@ export default function robots(): MetadataRoute.Robots {
|
|
|
2203
3628
|
);
|
|
2204
3629
|
if (sitemapFinding && siteUrl) {
|
|
2205
3630
|
if (layout.kind === "app-router") {
|
|
2206
|
-
const sitemapFilePath =
|
|
2207
|
-
const relativePath =
|
|
2208
|
-
const oldContent =
|
|
3631
|
+
const sitemapFilePath = path5.join(layout.directory, "sitemap.ts");
|
|
3632
|
+
const relativePath = path5.relative(projectRoot, sitemapFilePath);
|
|
3633
|
+
const oldContent = fs5.existsSync(sitemapFilePath) ? fs5.readFileSync(sitemapFilePath, "utf8") : "";
|
|
2209
3634
|
const routeEntries = validRoutes.map(
|
|
2210
3635
|
(r) => ` {
|
|
2211
3636
|
url: '${siteUrl}${r === "/" ? "" : r}',
|
|
@@ -2230,9 +3655,9 @@ ${routeEntries}
|
|
|
2230
3655
|
description: `Generated app/sitemap.ts containing ${validRoutes.length} discovered routes.`
|
|
2231
3656
|
}, sitemapFinding.id);
|
|
2232
3657
|
} else {
|
|
2233
|
-
const sitemapFilePath =
|
|
2234
|
-
const relativePath =
|
|
2235
|
-
const oldContent =
|
|
3658
|
+
const sitemapFilePath = path5.join(layout.directory, "sitemap.xml");
|
|
3659
|
+
const relativePath = path5.relative(projectRoot, sitemapFilePath);
|
|
3660
|
+
const oldContent = fs5.existsSync(sitemapFilePath) ? fs5.readFileSync(sitemapFilePath, "utf8") : "";
|
|
2236
3661
|
const xmlEntries = validRoutes.map(
|
|
2237
3662
|
(r) => ` <url>
|
|
2238
3663
|
<loc>${siteUrl}${r === "/" ? "" : r}</loc>
|
|
@@ -2258,7 +3683,7 @@ ${xmlEntries}
|
|
|
2258
3683
|
const canonicalFindings = findings.filter((f) => f.rule === "canonical" && f.fixable && f.file);
|
|
2259
3684
|
for (const finding of canonicalFindings) {
|
|
2260
3685
|
const findingFile = finding.file ? safeFile(finding.file) : null;
|
|
2261
|
-
if (!findingFile || !
|
|
3686
|
+
if (!findingFile || !fs5.existsSync(findingFile)) continue;
|
|
2262
3687
|
const fileContent = readCurrent(findingFile);
|
|
2263
3688
|
const route = finding.route || "/";
|
|
2264
3689
|
const canonicalUrl = `${siteUrl}${route === "/" ? "" : route}`;
|
|
@@ -2268,8 +3693,8 @@ ${xmlEntries}
|
|
|
2268
3693
|
filePath: findingFile,
|
|
2269
3694
|
originalContent: fileContent,
|
|
2270
3695
|
newContent: updatedContent,
|
|
2271
|
-
diff: createUnifiedDiff(
|
|
2272
|
-
description: `Added canonical "${canonicalUrl}" declaration to ${
|
|
3696
|
+
diff: createUnifiedDiff(path5.relative(projectRoot, findingFile), fileContent, updatedContent),
|
|
3697
|
+
description: `Added canonical "${canonicalUrl}" declaration to ${path5.basename(findingFile)}.`
|
|
2273
3698
|
}, finding.id);
|
|
2274
3699
|
}
|
|
2275
3700
|
}
|
|
@@ -2277,7 +3702,7 @@ ${xmlEntries}
|
|
|
2277
3702
|
const fixableLinkFindings = findings.filter((f) => f.rule === "links" && f.fixable && f.file);
|
|
2278
3703
|
for (const lf of fixableLinkFindings) {
|
|
2279
3704
|
const linkFile = lf.file ? safeFile(lf.file) : null;
|
|
2280
|
-
if (!linkFile || !
|
|
3705
|
+
if (!linkFile || !fs5.existsSync(linkFile)) continue;
|
|
2281
3706
|
const fileContent = readCurrent(linkFile);
|
|
2282
3707
|
const match = lf.message.match(/nonexistent route "([^"]+)"/);
|
|
2283
3708
|
if (match) {
|
|
@@ -2296,7 +3721,7 @@ ${xmlEntries}
|
|
|
2296
3721
|
filePath: linkFile,
|
|
2297
3722
|
originalContent: fileContent,
|
|
2298
3723
|
newContent: updatedContent,
|
|
2299
|
-
diff: createUnifiedDiff(
|
|
3724
|
+
diff: createUnifiedDiff(path5.relative(projectRoot, linkFile), fileContent, updatedContent),
|
|
2300
3725
|
description: `Fixed casing of internal link: "${brokenHref}" -> "${normalizedTarget}".`
|
|
2301
3726
|
}, lf.id);
|
|
2302
3727
|
}
|
|
@@ -2446,7 +3871,7 @@ function runSEOAudit(options) {
|
|
|
2446
3871
|
recommendations,
|
|
2447
3872
|
opportunities,
|
|
2448
3873
|
timestamp,
|
|
2449
|
-
engineVersion: "0.
|
|
3874
|
+
engineVersion: "0.3.1"
|
|
2450
3875
|
};
|
|
2451
3876
|
}
|
|
2452
3877
|
|
|
@@ -2524,7 +3949,7 @@ function evaluateQualityGate(input) {
|
|
|
2524
3949
|
}
|
|
2525
3950
|
|
|
2526
3951
|
// ../core/src/diff-engine.ts
|
|
2527
|
-
import
|
|
3952
|
+
import path6 from "node:path";
|
|
2528
3953
|
|
|
2529
3954
|
// ../core/src/contracts.ts
|
|
2530
3955
|
function matchesRoutePattern(route, pattern) {
|
|
@@ -3012,19 +4437,19 @@ function consolidateRegressionsByRootCause(rawNewFindings, baseAudit, headAudit,
|
|
|
3012
4437
|
const layoutPath = mut.nodeId.replace(/^layout:/, "");
|
|
3013
4438
|
const downstream = traceDownstreamRoutes(headSEOGraph, layoutPath);
|
|
3014
4439
|
if (downstream.length === 0) continue;
|
|
3015
|
-
const
|
|
4440
|
+
const prop2 = mut.property;
|
|
3016
4441
|
const matching = rawNewFindings.filter((f) => {
|
|
3017
4442
|
if (handledIds.has(f.id)) return false;
|
|
3018
4443
|
const routeMatch = f.route && downstream.includes(f.route);
|
|
3019
|
-
const propMatch =
|
|
4444
|
+
const propMatch = prop2 === "canonical" ? f.rule.includes("canonical") : f.rule.includes("robots") || f.message.includes("noindex");
|
|
3020
4445
|
return routeMatch && propMatch;
|
|
3021
4446
|
});
|
|
3022
4447
|
if (matching.length > 0) {
|
|
3023
4448
|
for (const m of matching) handledIds.add(m.id);
|
|
3024
|
-
const isCanonical =
|
|
3025
|
-
const baseName =
|
|
4449
|
+
const isCanonical = prop2 === "canonical";
|
|
4450
|
+
const baseName = path6.basename(layoutPath);
|
|
3026
4451
|
consolidated.push({
|
|
3027
|
-
id: `regression-${baseName}-${
|
|
4452
|
+
id: `regression-${baseName}-${prop2}`,
|
|
3028
4453
|
rule: isCanonical ? "metadata/canonical-regression" : "crawlability/robots-regression",
|
|
3029
4454
|
severity: "error",
|
|
3030
4455
|
category: isCanonical ? "metadata" : "technical",
|
|
@@ -3057,7 +4482,7 @@ function consolidateRegressionsByRootCause(rawNewFindings, baseAudit, headAudit,
|
|
|
3057
4482
|
sampleRoutes: downstream.slice(0, 3),
|
|
3058
4483
|
confidence: "STRUCTURAL",
|
|
3059
4484
|
revertSafeValue: mut.oldValue ? String(mut.oldValue) : void 0,
|
|
3060
|
-
fixRecommendation: `Restore previous ${
|
|
4485
|
+
fixRecommendation: `Restore previous ${prop2} declaration from BASE revision in ${baseName}.`
|
|
3061
4486
|
}
|
|
3062
4487
|
});
|
|
3063
4488
|
}
|
|
@@ -3087,7 +4512,7 @@ function consolidateRegressionsByRootCause(rawNewFindings, baseAudit, headAudit,
|
|
|
3087
4512
|
(n) => n.type === "layout" && (n.path?.includes(dir.replace("/", "")) || n.id.includes(dir.replace("/", "")))
|
|
3088
4513
|
);
|
|
3089
4514
|
const inferredLayoutFile = layoutNode?.path || `app${dir}/layout.tsx`;
|
|
3090
|
-
const baseName =
|
|
4515
|
+
const baseName = path6.basename(inferredLayoutFile);
|
|
3091
4516
|
const affected = dirFindings.map((f) => f.route).sort();
|
|
3092
4517
|
for (const df of dirFindings) handledIds.add(df.id);
|
|
3093
4518
|
consolidated.push({
|
|
@@ -3807,8 +5232,8 @@ var SessionKeyStore = class {
|
|
|
3807
5232
|
var sessionKeyStore = new SessionKeyStore();
|
|
3808
5233
|
|
|
3809
5234
|
// ../ai/src/semantic-resolver.ts
|
|
3810
|
-
import
|
|
3811
|
-
import
|
|
5235
|
+
import fs6 from "node:fs";
|
|
5236
|
+
import path7 from "node:path";
|
|
3812
5237
|
var METADATA_KEYS = ["canonical", "robots", "title", "description"];
|
|
3813
5238
|
function helperDefinesProperty(helperCode, property) {
|
|
3814
5239
|
const escaped = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -3819,8 +5244,8 @@ async function resolveSemanticHelpers(options) {
|
|
|
3819
5244
|
if (!aiProvider) return [];
|
|
3820
5245
|
const results = [];
|
|
3821
5246
|
for (const route of routes) {
|
|
3822
|
-
if (!route.filePath || !
|
|
3823
|
-
const content =
|
|
5247
|
+
if (!route.filePath || !fs6.existsSync(route.filePath)) continue;
|
|
5248
|
+
const content = fs6.readFileSync(route.filePath, "utf8");
|
|
3824
5249
|
const helperMatch = content.match(
|
|
3825
5250
|
/export\s+const\s+metadata\s*=\s*([A-Za-z0-9_]+)\s*\(([\s\S]*?)\)/
|
|
3826
5251
|
);
|
|
@@ -3835,16 +5260,16 @@ async function resolveSemanticHelpers(options) {
|
|
|
3835
5260
|
if (importMatch) {
|
|
3836
5261
|
const importPath = importMatch[1];
|
|
3837
5262
|
const candidates = [
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
5263
|
+
path7.resolve(path7.dirname(route.filePath), `${importPath}.ts`),
|
|
5264
|
+
path7.resolve(path7.dirname(route.filePath), `${importPath}.tsx`),
|
|
5265
|
+
path7.resolve(path7.dirname(route.filePath), `${importPath}/index.ts`),
|
|
5266
|
+
path7.resolve(projectRoot, `${importPath.replace(/^@\//, "src/").replace(/^~\//, "")}.ts`),
|
|
5267
|
+
path7.resolve(projectRoot, `${importPath.replace(/^@\//, "src/").replace(/^~\//, "")}.tsx`)
|
|
3843
5268
|
];
|
|
3844
5269
|
for (const cand of candidates) {
|
|
3845
|
-
if (
|
|
5270
|
+
if (fs6.existsSync(cand)) {
|
|
3846
5271
|
helperFilePath = cand;
|
|
3847
|
-
helperCode =
|
|
5272
|
+
helperCode = fs6.readFileSync(cand, "utf8");
|
|
3848
5273
|
break;
|
|
3849
5274
|
}
|
|
3850
5275
|
}
|
|
@@ -3869,7 +5294,7 @@ async function resolveSemanticHelpers(options) {
|
|
|
3869
5294
|
if (hasMeaningfulProps) {
|
|
3870
5295
|
const verifiedProperties = helperCode ? METADATA_KEYS.filter((property) => resolution.maps[property] && helperDefinesProperty(helperCode, property)) : [];
|
|
3871
5296
|
const confidence = verifiedProperties.length > 0 ? "AI_VERIFIED" : "AI_ASSISTED";
|
|
3872
|
-
const cleanHelperPath = helperFilePath ?
|
|
5297
|
+
const cleanHelperPath = helperFilePath ? path7.relative(projectRoot, helperFilePath).replace(/\\/g, "/") : helperName;
|
|
3873
5298
|
const resolvedMeta = {};
|
|
3874
5299
|
if (verifiedProperties.includes("canonical") && !route.metadata?.canonical) {
|
|
3875
5300
|
resolvedMeta.canonical = resolution.maps.canonical;
|
|
@@ -3877,7 +5302,7 @@ async function resolveSemanticHelpers(options) {
|
|
|
3877
5302
|
if (verifiedProperties.includes("robots") && !route.metadata?.robots) {
|
|
3878
5303
|
resolvedMeta.robots = resolution.maps.robots;
|
|
3879
5304
|
}
|
|
3880
|
-
const proofId = `proof-seo-${
|
|
5305
|
+
const proofId = `proof-seo-${path7.basename(route.filePath)}-${helperName}`;
|
|
3881
5306
|
const evidenceItems = [
|
|
3882
5307
|
{
|
|
3883
5308
|
id: `ev-callsite-${helperName}`,
|
|
@@ -3898,23 +5323,23 @@ async function resolveSemanticHelpers(options) {
|
|
|
3898
5323
|
}
|
|
3899
5324
|
const verifiedClaims = [];
|
|
3900
5325
|
const rejectedClaims = [];
|
|
3901
|
-
for (const [
|
|
5326
|
+
for (const [prop2, val] of Object.entries(resolution.maps)) {
|
|
3902
5327
|
if (val) {
|
|
3903
|
-
if (helperVerified && verifiedProperties.includes(
|
|
5328
|
+
if (helperVerified && verifiedProperties.includes(prop2)) {
|
|
3904
5329
|
verifiedClaims.push({
|
|
3905
|
-
id: `claim-${
|
|
5330
|
+
id: `claim-${prop2}-${helperName}`,
|
|
3906
5331
|
subject: route.route,
|
|
3907
|
-
predicate: `has${
|
|
5332
|
+
predicate: `has${prop2.charAt(0).toUpperCase() + prop2.slice(1)}`,
|
|
3908
5333
|
observedValue: val,
|
|
3909
5334
|
evidenceRefs: evidenceItems.map((e) => e.id),
|
|
3910
5335
|
source: { file: cleanHelperPath, route: route.route }
|
|
3911
5336
|
});
|
|
3912
5337
|
} else {
|
|
3913
5338
|
rejectedClaims.push({
|
|
3914
|
-
id: `claim-${
|
|
5339
|
+
id: `claim-${prop2}-${helperName}`,
|
|
3915
5340
|
subject: route.route,
|
|
3916
|
-
predicate: `has${
|
|
3917
|
-
reason: helperVerified ? `Helper source does not deterministically define metadata property "${
|
|
5341
|
+
predicate: `has${prop2.charAt(0).toUpperCase() + prop2.slice(1)}`,
|
|
5342
|
+
reason: helperVerified ? `Helper source does not deterministically define metadata property "${prop2}".` : "Helper source code could not be verified on filesystem."
|
|
3918
5343
|
});
|
|
3919
5344
|
}
|
|
3920
5345
|
}
|
|
@@ -3942,7 +5367,7 @@ async function resolveSemanticHelpers(options) {
|
|
|
3942
5367
|
discoveredBy: "AI",
|
|
3943
5368
|
verifiedBy: verifiedProperties.length > 0 ? "DETERMINISTIC_VERIFIER" : void 0,
|
|
3944
5369
|
evidence: [
|
|
3945
|
-
`Call site: ${
|
|
5370
|
+
`Call site: ${path7.basename(route.filePath)}`,
|
|
3946
5371
|
helperVerified ? `Helper definition: ${cleanHelperPath}` : `Inferred signature from ${helperName}`,
|
|
3947
5372
|
...resolution.evidenceChain || []
|
|
3948
5373
|
]
|
|
@@ -3965,7 +5390,7 @@ async function resolveSemanticHelpers(options) {
|
|
|
3965
5390
|
}
|
|
3966
5391
|
|
|
3967
5392
|
// src/formatter.ts
|
|
3968
|
-
import
|
|
5393
|
+
import path8 from "node:path";
|
|
3969
5394
|
var colors = {
|
|
3970
5395
|
reset: "\x1B[0m",
|
|
3971
5396
|
bold: "\x1B[1m",
|
|
@@ -4020,7 +5445,7 @@ ${colors.dim}${"\u2501".repeat(45)}${colors.reset}
|
|
|
4020
5445
|
console.log(`${colors.dim}${"\u2501".repeat(45)}${colors.reset}
|
|
4021
5446
|
`);
|
|
4022
5447
|
const rawFile = f.rootCause?.file || f.sourceFile || f.file || "";
|
|
4023
|
-
const relFile = rawFile ?
|
|
5448
|
+
const relFile = rawFile ? path8.relative(projectRoot, rawFile).replace(/\\/g, "/") : "";
|
|
4024
5449
|
const lineSuffix = f.rootCause?.line || f.sourceLine || f.line ? `:${f.rootCause?.line || f.sourceLine || f.line}` : "";
|
|
4025
5450
|
if (f.rootCause) {
|
|
4026
5451
|
console.log(`${colors.bold}Root cause${colors.reset}`);
|
|
@@ -4041,7 +5466,7 @@ ${colors.dim}${"\u2501".repeat(45)}${colors.reset}
|
|
|
4041
5466
|
}
|
|
4042
5467
|
if (f.evidenceChain?.steps && f.evidenceChain.steps.length > 0) {
|
|
4043
5468
|
console.log(`${colors.bold}Dependency${colors.reset}`);
|
|
4044
|
-
const sourceName =
|
|
5469
|
+
const sourceName = path8.basename(relFile || f.evidenceChain.sourceFile);
|
|
4045
5470
|
console.log(`${sourceName}`);
|
|
4046
5471
|
for (const step of f.evidenceChain.steps) {
|
|
4047
5472
|
const detail = step.detail ? ` ${step.detail}` : "";
|
|
@@ -4165,7 +5590,7 @@ function printAuditReport(result, projectRoot) {
|
|
|
4165
5590
|
if (f.severity === "warning") icon = colors.yellow + "\u26A0" + colors.reset;
|
|
4166
5591
|
const fixTag = f.fixable ? ` ${colors.green}[fixable]${colors.reset}` : "";
|
|
4167
5592
|
const routeTag = f.route ? ` ${colors.cyan}(${f.route})${colors.reset}` : "";
|
|
4168
|
-
const relativeFile = f.file ?
|
|
5593
|
+
const relativeFile = f.file ? path8.relative(projectRoot, f.file) : "";
|
|
4169
5594
|
const fileTag = relativeFile ? `
|
|
4170
5595
|
${colors.dim}${relativeFile}${f.line ? `:${f.line}` : ""}${colors.reset}` : "";
|
|
4171
5596
|
console.log(` ${icon} ${f.message}${fixTag}${routeTag}${fileTag}`);
|
|
@@ -4261,7 +5686,7 @@ function loadLocalEnv(projectRoot) {
|
|
|
4261
5686
|
const dirsToSearch = [projectRoot];
|
|
4262
5687
|
let curr = projectRoot;
|
|
4263
5688
|
for (let i = 0; i < 4; i++) {
|
|
4264
|
-
const parent =
|
|
5689
|
+
const parent = path9.dirname(curr);
|
|
4265
5690
|
if (parent && parent !== curr) {
|
|
4266
5691
|
dirsToSearch.push(parent);
|
|
4267
5692
|
curr = parent;
|
|
@@ -4271,10 +5696,10 @@ function loadLocalEnv(projectRoot) {
|
|
|
4271
5696
|
}
|
|
4272
5697
|
for (const dir of dirsToSearch) {
|
|
4273
5698
|
for (const envFile of [".env", ".env.local"]) {
|
|
4274
|
-
const fullPath =
|
|
4275
|
-
if (
|
|
5699
|
+
const fullPath = path9.join(dir, envFile);
|
|
5700
|
+
if (fs7.existsSync(fullPath)) {
|
|
4276
5701
|
try {
|
|
4277
|
-
const content =
|
|
5702
|
+
const content = fs7.readFileSync(fullPath, "utf8");
|
|
4278
5703
|
for (const line of content.split("\n")) {
|
|
4279
5704
|
const trimmed = line.trim();
|
|
4280
5705
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -4297,15 +5722,15 @@ function loadLocalEnv(projectRoot) {
|
|
|
4297
5722
|
}
|
|
4298
5723
|
}
|
|
4299
5724
|
function checkSecretRisk(projectRoot) {
|
|
4300
|
-
const envCandidates =
|
|
4301
|
-
const gitignorePath =
|
|
5725
|
+
const envCandidates = fs7.readdirSync(projectRoot).filter((name) => name !== ".env.example" && (name === ".env" || name.startsWith(".env.")));
|
|
5726
|
+
const gitignorePath = path9.join(projectRoot, ".gitignore");
|
|
4302
5727
|
let gitignoreContent = "";
|
|
4303
|
-
if (
|
|
4304
|
-
gitignoreContent =
|
|
5728
|
+
if (fs7.existsSync(gitignorePath)) {
|
|
5729
|
+
gitignoreContent = fs7.readFileSync(gitignorePath, "utf8");
|
|
4305
5730
|
}
|
|
4306
5731
|
for (const env of envCandidates) {
|
|
4307
|
-
const envPath =
|
|
4308
|
-
if (
|
|
5732
|
+
const envPath = path9.join(projectRoot, env);
|
|
5733
|
+
if (fs7.existsSync(envPath)) {
|
|
4309
5734
|
const tracked = spawnSync("git", ["ls-files", "--error-unmatch", "--", env], {
|
|
4310
5735
|
cwd: projectRoot,
|
|
4311
5736
|
stdio: "ignore"
|
|
@@ -4317,11 +5742,11 @@ function checkSecretRisk(projectRoot) {
|
|
|
4317
5742
|
}
|
|
4318
5743
|
}
|
|
4319
5744
|
async function auditGitRef(projectRoot, ref) {
|
|
4320
|
-
const tmpDir =
|
|
5745
|
+
const tmpDir = fs7.mkdtempSync(path9.join(os.tmpdir(), "crawlemon-base-"));
|
|
4321
5746
|
try {
|
|
4322
5747
|
const gitRootRes = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd: projectRoot, encoding: "utf8" });
|
|
4323
5748
|
const gitRoot = gitRootRes.status === 0 ? gitRootRes.stdout.trim() : projectRoot;
|
|
4324
|
-
const relToGitRoot =
|
|
5749
|
+
const relToGitRoot = path9.relative(gitRoot, projectRoot).replace(/\\/g, "/");
|
|
4325
5750
|
const archiveRef = relToGitRoot && relToGitRoot !== "." ? `${ref}:${relToGitRoot}` : ref;
|
|
4326
5751
|
const gitArchive = spawnSync("git", ["archive", archiveRef], {
|
|
4327
5752
|
cwd: gitRoot,
|
|
@@ -4348,7 +5773,7 @@ async function auditGitRef(projectRoot, ref) {
|
|
|
4348
5773
|
isAppRouter: scanData.isAppRouter
|
|
4349
5774
|
});
|
|
4350
5775
|
} finally {
|
|
4351
|
-
|
|
5776
|
+
fs7.rmSync(tmpDir, { recursive: true, force: true });
|
|
4352
5777
|
}
|
|
4353
5778
|
}
|
|
4354
5779
|
async function runCli(args) {
|
|
@@ -4364,7 +5789,7 @@ async function runCli(args) {
|
|
|
4364
5789
|
return;
|
|
4365
5790
|
}
|
|
4366
5791
|
if (command === "--version" || command === "-v") {
|
|
4367
|
-
console.log("0.
|
|
5792
|
+
console.log("0.3.1");
|
|
4368
5793
|
return;
|
|
4369
5794
|
}
|
|
4370
5795
|
checkSecretRisk(projectRoot);
|
|
@@ -4463,25 +5888,25 @@ async function runCli(args) {
|
|
|
4463
5888
|
try {
|
|
4464
5889
|
const gitRootRes = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd: projectRoot, encoding: "utf8" });
|
|
4465
5890
|
const gitRoot = gitRootRes.status === 0 ? gitRootRes.stdout.trim() : projectRoot;
|
|
4466
|
-
const relToGitRoot =
|
|
5891
|
+
const relToGitRoot = path9.relative(gitRoot, projectRoot).replace(/\\/g, "/");
|
|
4467
5892
|
const archiveRef = relToGitRoot && relToGitRoot !== "." ? `${baseRefArg}:${relToGitRoot}` : baseRefArg;
|
|
4468
5893
|
const gitArchive = spawnSync("git", ["archive", archiveRef], { cwd: gitRoot, maxBuffer: 64 * 1024 * 1024 });
|
|
4469
5894
|
if (gitArchive.status === 0 && gitArchive.stdout) {
|
|
4470
|
-
const tmpDir =
|
|
5895
|
+
const tmpDir = fs7.mkdtempSync(path9.join(os.tmpdir(), "crawlemon-base-fix-"));
|
|
4471
5896
|
spawnSync("tar", ["-x", "-C", tmpDir], { input: gitArchive.stdout });
|
|
4472
5897
|
baseFiles = /* @__PURE__ */ new Map();
|
|
4473
5898
|
const walk2 = (d) => {
|
|
4474
|
-
for (const ent of
|
|
4475
|
-
const full =
|
|
5899
|
+
for (const ent of fs7.readdirSync(d, { withFileTypes: true })) {
|
|
5900
|
+
const full = path9.join(d, ent.name);
|
|
4476
5901
|
if (ent.isDirectory()) walk2(full);
|
|
4477
5902
|
else if (ent.isFile()) {
|
|
4478
|
-
const rel =
|
|
4479
|
-
baseFiles.set(rel,
|
|
5903
|
+
const rel = path9.relative(tmpDir, full).replace(/\\/g, "/");
|
|
5904
|
+
baseFiles.set(rel, fs7.readFileSync(full, "utf8"));
|
|
4480
5905
|
}
|
|
4481
5906
|
}
|
|
4482
5907
|
};
|
|
4483
5908
|
walk2(tmpDir);
|
|
4484
|
-
|
|
5909
|
+
fs7.rmSync(tmpDir, { recursive: true, force: true });
|
|
4485
5910
|
}
|
|
4486
5911
|
} catch {
|
|
4487
5912
|
}
|