@writechoice/mint-cli 0.0.5 → 0.0.6
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 +44 -17
- package/bin/cli.js +4 -2
- package/package.json +1 -1
- package/src/commands/validate/links.js +196 -87
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ Check the installed version:
|
|
|
47
47
|
```bash
|
|
48
48
|
writechoice --version
|
|
49
49
|
# or
|
|
50
|
-
writechoice -
|
|
50
|
+
writechoice -v
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
### Update to Latest Version
|
|
@@ -74,6 +74,23 @@ You can also omit the `https://` prefix:
|
|
|
74
74
|
writechoice check links docs.example.com
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
**Using a Validation Base URL**
|
|
78
|
+
|
|
79
|
+
When validating anchor links online, the tool can use a different base URL (e.g., a local development server or staging environment) to click on headings and extract the generated anchors:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Use localhost:3000 for validation (default)
|
|
83
|
+
writechoice check links docs.example.com
|
|
84
|
+
|
|
85
|
+
# Use a custom validation URL
|
|
86
|
+
writechoice check links docs.example.com http://localhost:3000
|
|
87
|
+
|
|
88
|
+
# Use a staging environment
|
|
89
|
+
writechoice check links docs.example.com https://staging.example.com
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The validation base URL is only used for online checks. Local file validation remains unchanged for optimal performance.
|
|
93
|
+
|
|
77
94
|
### Common Options
|
|
78
95
|
|
|
79
96
|
```bash
|
|
@@ -110,19 +127,20 @@ writechoice check links docs.example.com --fix-from-report custom_report.json
|
|
|
110
127
|
|
|
111
128
|
### Complete Options
|
|
112
129
|
|
|
113
|
-
| Option | Alias | Description
|
|
114
|
-
| -------------------------- | ----- |
|
|
115
|
-
| `<baseUrl>` | - | Base URL for the documentation site (required, with or without https://)
|
|
116
|
-
|
|
|
117
|
-
| `--
|
|
118
|
-
| `--
|
|
119
|
-
| `--
|
|
120
|
-
| `--
|
|
121
|
-
| `--
|
|
122
|
-
| `--
|
|
123
|
-
| `--
|
|
124
|
-
| `--
|
|
125
|
-
| `--fix
|
|
130
|
+
| Option | Alias | Description | Default |
|
|
131
|
+
| -------------------------- | ----- | ------------------------------------------------------------------------- | ----------------------- |
|
|
132
|
+
| `<baseUrl>` | - | Base URL for the documentation site (required, with or without https://) | - |
|
|
133
|
+
| `[validationBaseUrl]` | - | Base URL for online validation (optional, clicks headings to get anchors) | `http://localhost:3000` |
|
|
134
|
+
| `--file <path>` | `-f` | Validate links in a single MDX file | - |
|
|
135
|
+
| `--dir <path>` | `-d` | Validate links in a specific directory | - |
|
|
136
|
+
| `--output <path>` | `-o` | Output path for JSON report | `links_report.json` |
|
|
137
|
+
| `--dry-run` | - | Extract and show links without validating | `false` |
|
|
138
|
+
| `--quiet` | - | Suppress terminal output (only generate report) | `false` |
|
|
139
|
+
| `--concurrency <number>` | `-c` | Number of concurrent browser tabs | `25` |
|
|
140
|
+
| `--headless` | - | Run browser in headless mode | `true` |
|
|
141
|
+
| `--no-headless` | - | Show browser window (for debugging) | - |
|
|
142
|
+
| `--fix` | - | Automatically fix anchor links in MDX files | `false` |
|
|
143
|
+
| `--fix-from-report [path]` | - | Fix anchor links from report file (optional path) | `links_report.json` |
|
|
126
144
|
|
|
127
145
|
**Note:** Detailed progress output is shown by default. Use `--quiet` to suppress terminal output.
|
|
128
146
|
|
|
@@ -138,15 +156,24 @@ The tool extracts internal links from MDX files in the following formats:
|
|
|
138
156
|
4. **JSX Button components**: `<Button href="/path/to/page#anchor">Button Text</Button>`
|
|
139
157
|
|
|
140
158
|
**Images are automatically ignored:**
|
|
159
|
+
|
|
141
160
|
- Markdown images: ``
|
|
142
161
|
- HTML images: `<img src="./image.png" />`
|
|
143
162
|
|
|
144
163
|
### Validation Process
|
|
145
164
|
|
|
146
165
|
1. **Local Validation**: First checks if the target MDX file exists locally
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
166
|
+
- For normal links: Verifies the file exists in the repository
|
|
167
|
+
- For anchor links: Checks if the heading exists in the MDX file with matching kebab-case format
|
|
168
|
+
2. **Online Validation**: If local check fails, uses Playwright to navigate to the validation base URL
|
|
169
|
+
- For normal links: Verifies the page loads successfully
|
|
170
|
+
- For anchor links:
|
|
171
|
+
- Finds the heading on the page by text matching
|
|
172
|
+
- Clicks on the heading to trigger anchor generation
|
|
173
|
+
- Extracts the generated anchor from the URL or href attribute
|
|
174
|
+
- Compares the generated anchor with the expected anchor
|
|
175
|
+
3. **Validation Base URL**: By default uses `http://localhost:3000` for online validation, or you can specify a custom URL (e.g., staging environment)
|
|
176
|
+
4. **Auto-Fix**: When issues are found, can automatically update MDX files with the correct anchors
|
|
150
177
|
|
|
151
178
|
### Report Format
|
|
152
179
|
|
package/bin/cli.js
CHANGED
|
@@ -24,7 +24,7 @@ const check = program.command("check").description("Validation commands for docu
|
|
|
24
24
|
|
|
25
25
|
// Validate links subcommand
|
|
26
26
|
check
|
|
27
|
-
.command("links <baseUrl>")
|
|
27
|
+
.command("links <baseUrl> [validationBaseUrl]")
|
|
28
28
|
.description("Validate internal links and anchors in MDX documentation files")
|
|
29
29
|
.option("-f, --file <path>", "Validate links in a single MDX file")
|
|
30
30
|
.option("-d, --dir <path>", "Validate links in a specific directory")
|
|
@@ -36,10 +36,12 @@ check
|
|
|
36
36
|
.option("--no-headless", "Show browser window (for debugging)")
|
|
37
37
|
.option("--fix", "Automatically fix anchor links in MDX files")
|
|
38
38
|
.option("--fix-from-report [path]", "Fix anchor links from report file (default: links_report.json)")
|
|
39
|
-
.action(async (baseUrl, options) => {
|
|
39
|
+
.action(async (baseUrl, validationBaseUrl, options) => {
|
|
40
40
|
const { validateLinks } = await import("../src/commands/validate/links.js");
|
|
41
41
|
// Verbose is now default (true unless --quiet is specified)
|
|
42
42
|
options.verbose = !options.quiet;
|
|
43
|
+
// Set validation base URL to localhost:3000 if not provided
|
|
44
|
+
options.validationBaseUrl = validationBaseUrl || "http://localhost:3000";
|
|
43
45
|
await validateLinks(baseUrl, options);
|
|
44
46
|
});
|
|
45
47
|
|
package/package.json
CHANGED
|
@@ -377,7 +377,7 @@ function findMdxFiles(repoRoot, directory = null, file = null) {
|
|
|
377
377
|
|
|
378
378
|
// Playwright Validation Functions
|
|
379
379
|
|
|
380
|
-
async function validateAnchor(page, link, baseUrl, repoRoot, verbose = false, progress = "") {
|
|
380
|
+
async function validateAnchor(page, link, baseUrl, validationBaseUrl, repoRoot, verbose = false, progress = "") {
|
|
381
381
|
const startTime = Date.now();
|
|
382
382
|
|
|
383
383
|
try {
|
|
@@ -385,7 +385,7 @@ async function validateAnchor(page, link, baseUrl, repoRoot, verbose = false, pr
|
|
|
385
385
|
console.log(`${progress} Validating anchor: ${link.anchor}`);
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
-
// OPTIMIZATION: Check if anchor exists in local MDX file first
|
|
388
|
+
// OPTIMIZATION: Check if anchor exists in local MDX file first (local validation)
|
|
389
389
|
const mdxFilePath = urlToFilePath(link.basePath, baseUrl, repoRoot);
|
|
390
390
|
if (mdxFilePath && existsSync(mdxFilePath)) {
|
|
391
391
|
const mdxHeadings = extractMdxHeadings(mdxFilePath);
|
|
@@ -394,7 +394,7 @@ async function validateAnchor(page, link, baseUrl, repoRoot, verbose = false, pr
|
|
|
394
394
|
if (mdxHeadingsKebab.includes(link.anchor)) {
|
|
395
395
|
const heading = mdxHeadings.find((h) => toKebabCase(h) === link.anchor);
|
|
396
396
|
if (verbose) {
|
|
397
|
-
console.log(
|
|
397
|
+
console.log(`${progress} ✓ Anchor validated locally in MDX file`);
|
|
398
398
|
}
|
|
399
399
|
return new ValidationResult(
|
|
400
400
|
link.source,
|
|
@@ -410,21 +410,47 @@ async function validateAnchor(page, link, baseUrl, repoRoot, verbose = false, pr
|
|
|
410
410
|
Date.now() - startTime,
|
|
411
411
|
);
|
|
412
412
|
} else if (verbose) {
|
|
413
|
-
console.log(
|
|
413
|
+
console.log(`${progress} Anchor not found in local MDX, checking online...`);
|
|
414
414
|
}
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
-
//
|
|
418
|
-
|
|
417
|
+
// ONLINE VALIDATION: Use validation base URL to navigate and click on headings
|
|
418
|
+
// Convert the base path to use the validation base URL
|
|
419
|
+
const validationUrl = link.basePath.replace(baseUrl, validationBaseUrl);
|
|
419
420
|
|
|
420
|
-
|
|
421
|
-
|
|
421
|
+
if (verbose) {
|
|
422
|
+
console.log(`${progress} Navigating to: ${validationUrl}`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Navigate to validation page
|
|
426
|
+
await page.goto(validationUrl, { waitUntil: "networkidle", timeout: DEFAULT_TIMEOUT });
|
|
427
|
+
|
|
428
|
+
// Extract headings from the MDX file to find the matching heading text
|
|
429
|
+
let targetHeadingText = null;
|
|
430
|
+
|
|
431
|
+
if (mdxFilePath && existsSync(mdxFilePath)) {
|
|
432
|
+
const mdxHeadings = extractMdxHeadings(mdxFilePath);
|
|
433
|
+
const mdxHeadingsKebab = mdxHeadings.map((h) => toKebabCase(h));
|
|
434
|
+
|
|
435
|
+
// Find the heading that matches our anchor
|
|
436
|
+
const headingIndex = mdxHeadingsKebab.indexOf(link.anchor);
|
|
437
|
+
if (headingIndex !== -1) {
|
|
438
|
+
targetHeadingText = mdxHeadings[headingIndex];
|
|
439
|
+
if (verbose) {
|
|
440
|
+
console.log(`${progress} Looking for heading: "${targetHeadingText}"`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
422
444
|
|
|
423
|
-
|
|
424
|
-
|
|
445
|
+
// If we couldn't find the heading text from MDX, try to find it by the link text
|
|
446
|
+
if (!targetHeadingText && link.source.linkText) {
|
|
447
|
+
targetHeadingText = link.source.linkText;
|
|
448
|
+
if (verbose) {
|
|
449
|
+
console.log(`${progress} Using link text as heading: "${targetHeadingText}"`);
|
|
450
|
+
}
|
|
425
451
|
}
|
|
426
452
|
|
|
427
|
-
if (!
|
|
453
|
+
if (!targetHeadingText) {
|
|
428
454
|
return new ValidationResult(
|
|
429
455
|
link.source,
|
|
430
456
|
link.targetUrl,
|
|
@@ -435,84 +461,147 @@ async function validateAnchor(page, link, baseUrl, repoRoot, verbose = false, pr
|
|
|
435
461
|
null,
|
|
436
462
|
null,
|
|
437
463
|
null,
|
|
438
|
-
`
|
|
464
|
+
`Could not determine heading text to search for anchor #${link.anchor}`,
|
|
439
465
|
Date.now() - startTime,
|
|
440
466
|
);
|
|
441
467
|
}
|
|
442
468
|
|
|
443
|
-
//
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
469
|
+
// Find all headings on the page
|
|
470
|
+
const headings = await page.$$("h1, h2, h3, h4, h5, h6");
|
|
471
|
+
let matchedHeading = null;
|
|
472
|
+
let matchedHeadingText = null;
|
|
447
473
|
|
|
448
|
-
//
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
474
|
+
// Try to find the heading by text matching
|
|
475
|
+
for (const heading of headings) {
|
|
476
|
+
const headingText = await heading.innerText();
|
|
477
|
+
const headingTextClean = cleanHeadingText(headingText);
|
|
452
478
|
|
|
453
|
-
|
|
479
|
+
// Try exact match first
|
|
480
|
+
if (headingTextClean.toLowerCase() === targetHeadingText.toLowerCase()) {
|
|
481
|
+
matchedHeading = heading;
|
|
482
|
+
matchedHeadingText = headingTextClean;
|
|
483
|
+
if (verbose) {
|
|
484
|
+
console.log(`${progress} Found exact match: "${headingTextClean}"`);
|
|
485
|
+
}
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
454
488
|
|
|
455
|
-
|
|
456
|
-
if (
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
null,
|
|
468
|
-
Date.now() - startTime,
|
|
469
|
-
);
|
|
470
|
-
} else {
|
|
471
|
-
return new ValidationResult(
|
|
472
|
-
link.source,
|
|
473
|
-
link.targetUrl,
|
|
474
|
-
link.basePath,
|
|
475
|
-
link.anchor,
|
|
476
|
-
link.expectedSlug,
|
|
477
|
-
"failure",
|
|
478
|
-
null,
|
|
479
|
-
actualTextClean,
|
|
480
|
-
actualKebab,
|
|
481
|
-
`Anchor "#${link.anchor}" matches page heading "${actualTextClean}" but this heading is not found in the MDX file`,
|
|
482
|
-
Date.now() - startTime,
|
|
483
|
-
);
|
|
489
|
+
// Try partial match
|
|
490
|
+
if (
|
|
491
|
+
headingTextClean.toLowerCase().includes(targetHeadingText.toLowerCase()) ||
|
|
492
|
+
targetHeadingText.toLowerCase().includes(headingTextClean.toLowerCase())
|
|
493
|
+
) {
|
|
494
|
+
if (!matchedHeading) {
|
|
495
|
+
matchedHeading = heading;
|
|
496
|
+
matchedHeadingText = headingTextClean;
|
|
497
|
+
if (verbose) {
|
|
498
|
+
console.log(`${progress} Found partial match: "${headingTextClean}"`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
484
501
|
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
)
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (!matchedHeading) {
|
|
505
|
+
return new ValidationResult(
|
|
506
|
+
link.source,
|
|
507
|
+
link.targetUrl,
|
|
508
|
+
link.basePath,
|
|
509
|
+
link.anchor,
|
|
510
|
+
link.expectedSlug,
|
|
511
|
+
"failure",
|
|
512
|
+
null,
|
|
513
|
+
null,
|
|
514
|
+
null,
|
|
515
|
+
`Heading "${targetHeadingText}" not found on page`,
|
|
516
|
+
Date.now() - startTime,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Click the heading to trigger anchor generation
|
|
521
|
+
if (verbose) {
|
|
522
|
+
console.log(`${progress} Clicking heading to get generated anchor...`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Try to click on the heading element or a link inside it
|
|
526
|
+
let clickTarget = matchedHeading;
|
|
527
|
+
const linkInHeading = await matchedHeading.$("a");
|
|
528
|
+
if (linkInHeading) {
|
|
529
|
+
clickTarget = linkInHeading;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
await clickTarget.click();
|
|
533
|
+
|
|
534
|
+
// Wait a bit for any JavaScript to update the URL
|
|
535
|
+
await page.waitForTimeout(500);
|
|
536
|
+
|
|
537
|
+
// Get the current URL to extract the anchor
|
|
538
|
+
const currentUrl = page.url();
|
|
539
|
+
let generatedAnchor = null;
|
|
540
|
+
|
|
541
|
+
if (currentUrl.includes("#")) {
|
|
542
|
+
generatedAnchor = currentUrl.split("#")[1];
|
|
543
|
+
if (verbose) {
|
|
544
|
+
console.log(`${progress} Generated anchor from URL: #${generatedAnchor}`);
|
|
514
545
|
}
|
|
515
546
|
}
|
|
547
|
+
|
|
548
|
+
// If no anchor in URL, try to get it from the href attribute of the link
|
|
549
|
+
if (!generatedAnchor && linkInHeading) {
|
|
550
|
+
const href = await linkInHeading.getAttribute("href");
|
|
551
|
+
if (href && href.includes("#")) {
|
|
552
|
+
generatedAnchor = href.split("#")[1];
|
|
553
|
+
if (verbose) {
|
|
554
|
+
console.log(`${progress} Generated anchor from href: #${generatedAnchor}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (!generatedAnchor) {
|
|
560
|
+
return new ValidationResult(
|
|
561
|
+
link.source,
|
|
562
|
+
link.targetUrl,
|
|
563
|
+
link.basePath,
|
|
564
|
+
link.anchor,
|
|
565
|
+
link.expectedSlug,
|
|
566
|
+
"failure",
|
|
567
|
+
null,
|
|
568
|
+
matchedHeadingText,
|
|
569
|
+
null,
|
|
570
|
+
`Could not extract generated anchor after clicking heading "${matchedHeadingText}"`,
|
|
571
|
+
Date.now() - startTime,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Compare the generated anchor with the expected anchor
|
|
576
|
+
if (generatedAnchor === link.anchor) {
|
|
577
|
+
return new ValidationResult(
|
|
578
|
+
link.source,
|
|
579
|
+
link.targetUrl,
|
|
580
|
+
link.basePath,
|
|
581
|
+
link.anchor,
|
|
582
|
+
link.expectedSlug,
|
|
583
|
+
"success",
|
|
584
|
+
link.basePath,
|
|
585
|
+
matchedHeadingText,
|
|
586
|
+
generatedAnchor,
|
|
587
|
+
null,
|
|
588
|
+
Date.now() - startTime,
|
|
589
|
+
);
|
|
590
|
+
} else {
|
|
591
|
+
return new ValidationResult(
|
|
592
|
+
link.source,
|
|
593
|
+
link.targetUrl,
|
|
594
|
+
link.basePath,
|
|
595
|
+
link.anchor,
|
|
596
|
+
link.expectedSlug,
|
|
597
|
+
"failure",
|
|
598
|
+
null,
|
|
599
|
+
matchedHeadingText,
|
|
600
|
+
generatedAnchor,
|
|
601
|
+
`Expected anchor "#${link.anchor}" but page generates "#${generatedAnchor}" for heading "${matchedHeadingText}"`,
|
|
602
|
+
Date.now() - startTime,
|
|
603
|
+
);
|
|
604
|
+
}
|
|
516
605
|
} catch (error) {
|
|
517
606
|
return new ValidationResult(
|
|
518
607
|
link.source,
|
|
@@ -530,7 +619,7 @@ async function validateAnchor(page, link, baseUrl, repoRoot, verbose = false, pr
|
|
|
530
619
|
}
|
|
531
620
|
}
|
|
532
621
|
|
|
533
|
-
async function validateNormalLink(page, link, baseUrl, repoRoot, verbose = false, progress = "") {
|
|
622
|
+
async function validateNormalLink(page, link, baseUrl, validationBaseUrl, repoRoot, verbose = false, progress = "") {
|
|
534
623
|
const startTime = Date.now();
|
|
535
624
|
|
|
536
625
|
try {
|
|
@@ -561,8 +650,15 @@ async function validateNormalLink(page, link, baseUrl, repoRoot, verbose = false
|
|
|
561
650
|
console.log(` File not found locally, checking online...`);
|
|
562
651
|
}
|
|
563
652
|
|
|
564
|
-
//
|
|
565
|
-
const
|
|
653
|
+
// Convert the target URL to use the validation base URL
|
|
654
|
+
const validationUrl = link.targetUrl.replace(baseUrl, validationBaseUrl);
|
|
655
|
+
|
|
656
|
+
if (verbose) {
|
|
657
|
+
console.log(`${progress} Navigating to: ${validationUrl}`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Navigate to the validation URL
|
|
661
|
+
const response = await page.goto(validationUrl, { waitUntil: "networkidle", timeout: DEFAULT_TIMEOUT });
|
|
566
662
|
|
|
567
663
|
if (!response) {
|
|
568
664
|
return new ValidationResult(
|
|
@@ -628,15 +724,15 @@ async function validateNormalLink(page, link, baseUrl, repoRoot, verbose = false
|
|
|
628
724
|
}
|
|
629
725
|
}
|
|
630
726
|
|
|
631
|
-
async function validateLink(page, link, baseUrl, repoRoot, verbose = false, progress = "") {
|
|
727
|
+
async function validateLink(page, link, baseUrl, validationBaseUrl, repoRoot, verbose = false, progress = "") {
|
|
632
728
|
if (link.anchor) {
|
|
633
|
-
return await validateAnchor(page, link, baseUrl, repoRoot, verbose, progress);
|
|
729
|
+
return await validateAnchor(page, link, baseUrl, validationBaseUrl, repoRoot, verbose, progress);
|
|
634
730
|
} else {
|
|
635
|
-
return await validateNormalLink(page, link, baseUrl, repoRoot, verbose, progress);
|
|
731
|
+
return await validateNormalLink(page, link, baseUrl, validationBaseUrl, repoRoot, verbose, progress);
|
|
636
732
|
}
|
|
637
733
|
}
|
|
638
734
|
|
|
639
|
-
async function validateLinksAsync(links, baseUrl, repoRoot, concurrency, headless, verbose) {
|
|
735
|
+
async function validateLinksAsync(links, baseUrl, validationBaseUrl, repoRoot, concurrency, headless, verbose) {
|
|
640
736
|
const results = [];
|
|
641
737
|
|
|
642
738
|
let browser;
|
|
@@ -670,7 +766,7 @@ async function validateLinksAsync(links, baseUrl, repoRoot, concurrency, headles
|
|
|
670
766
|
const page = await context.newPage();
|
|
671
767
|
|
|
672
768
|
try {
|
|
673
|
-
const result = await validateLink(page, link, baseUrl, repoRoot, verbose, progress);
|
|
769
|
+
const result = await validateLink(page, link, baseUrl, validationBaseUrl, repoRoot, verbose, progress);
|
|
674
770
|
return result;
|
|
675
771
|
} finally {
|
|
676
772
|
await context.close();
|
|
@@ -1081,9 +1177,22 @@ export async function validateLinks(baseUrl, options) {
|
|
|
1081
1177
|
console.log("\nValidating links...");
|
|
1082
1178
|
}
|
|
1083
1179
|
|
|
1180
|
+
// Normalize validation base URL
|
|
1181
|
+
let normalizedValidationBaseUrl = options.validationBaseUrl || "http://localhost:3000";
|
|
1182
|
+
if (!normalizedValidationBaseUrl.startsWith("http://") && !normalizedValidationBaseUrl.startsWith("https://")) {
|
|
1183
|
+
normalizedValidationBaseUrl = "https://" + normalizedValidationBaseUrl;
|
|
1184
|
+
}
|
|
1185
|
+
// Remove trailing slash
|
|
1186
|
+
normalizedValidationBaseUrl = normalizedValidationBaseUrl.replace(/\/+$/, "");
|
|
1187
|
+
|
|
1188
|
+
if (!options.quiet) {
|
|
1189
|
+
console.log(`\nUsing validation base URL: ${normalizedValidationBaseUrl}`);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1084
1192
|
const results = await validateLinksAsync(
|
|
1085
1193
|
allLinks,
|
|
1086
1194
|
normalizedBaseUrl,
|
|
1195
|
+
normalizedValidationBaseUrl,
|
|
1087
1196
|
repoRoot,
|
|
1088
1197
|
parseInt(options.concurrency) || DEFAULT_CONCURRENCY,
|
|
1089
1198
|
options.headless !== false,
|