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