create-zudo-doc 5.16.1 → 5.17.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/CHANGELOG.md +18 -0
- package/dist/scaffold.d.ts +1 -1
- package/dist/scaffold.js +11 -5
- package/package.json +1 -1
- package/templates/base/scripts/check-links.js +61 -11
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ All notable changes to `create-zudo-doc` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on Keep a Changelog, and release notes are generated from the changelog MDX pages.
|
|
6
6
|
|
|
7
|
+
## [5.17.0] - 2026-09-04
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- `check-links` now reports protocol-relative hrefs (`//host/path`) as informational notices rather than passing over them silently (`67afa352f`).
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
- `check-links` no longer treats protocol-relative URLs (`//host/path`) as broken local links (`319169bf3`).
|
|
16
|
+
|
|
17
|
+
### Other Changes
|
|
18
|
+
|
|
19
|
+
- Newly generated projects now use the zfb 2.15.0 package family (`d9f5b2f64`). `ts` and `typescript` code fences resolve to the TypeScript grammar and `tsx` to TypeScriptReact, where both previously fell back to JavaScript highlighting.
|
|
20
|
+
|
|
21
|
+
## [5.16.2] - 2026-09-03
|
|
22
|
+
|
|
23
|
+
- No package-specific changes.
|
|
24
|
+
|
|
7
25
|
## [5.16.1] - 2026-09-02
|
|
8
26
|
|
|
9
27
|
### Other Changes
|
package/dist/scaffold.d.ts
CHANGED
|
@@ -31,5 +31,5 @@ export declare function deriveDocSkillName(projectName: string): string;
|
|
|
31
31
|
*
|
|
32
32
|
* Bumped in lockstep by scripts/release-create-zudo-doc.sh.
|
|
33
33
|
*/
|
|
34
|
-
export declare const ZUDO_DOC_PIN = "^5.
|
|
34
|
+
export declare const ZUDO_DOC_PIN = "^5.17.0";
|
|
35
35
|
export declare function scaffold(choices: UserChoices): Promise<void>;
|
package/dist/scaffold.js
CHANGED
|
@@ -47,7 +47,7 @@ export function deriveDocSkillName(projectName) {
|
|
|
47
47
|
*
|
|
48
48
|
* Bumped in lockstep by scripts/release-create-zudo-doc.sh.
|
|
49
49
|
*/
|
|
50
|
-
export const ZUDO_DOC_PIN = "^5.
|
|
50
|
+
export const ZUDO_DOC_PIN = "^5.17.0";
|
|
51
51
|
/**
|
|
52
52
|
* Files in `templates/base/**` that must not be copied by the unconditional
|
|
53
53
|
* base mirror. Each entry is matched against the path relative to
|
|
@@ -748,9 +748,15 @@ function generatePackageJson(choices, localePlan) {
|
|
|
748
748
|
// 2.14.3: lockstep release metadata and rebuilt md-wasm artifacts only.
|
|
749
749
|
// Public APIs, exports, config defaults, engine requirements, and shipped
|
|
750
750
|
// wasm byte sizes stay unchanged, so a fresh scaffold needs no migration.
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
751
|
+
// 2.15.0: `ts`/`typescript` fences now resolve to the TypeScript grammar
|
|
752
|
+
// and `tsx` to TypeScriptReact, where both previously fell back to
|
|
753
|
+
// JavaScript. The content-pipeline fingerprint moves to v3, so the first
|
|
754
|
+
// build after upgrading re-renders cached content under the new grammar
|
|
755
|
+
// set. Token classes stay on the same semantic-role vocabulary, so themes
|
|
756
|
+
// need no new rules and a fresh scaffold needs no migration.
|
|
757
|
+
"@takazudo/zfb": "2.15.0",
|
|
758
|
+
"@takazudo/zfb-runtime": "2.15.0",
|
|
759
|
+
"@takazudo/zfb-md-wasm": "2.15.0",
|
|
754
760
|
// @takazudo/zudo-doc — published from this monorepo via
|
|
755
761
|
// .github/workflows/publish-zudo-doc.yml. The pin here is bumped in
|
|
756
762
|
// lockstep by scripts/release-create-zudo-doc.sh whenever zudo-doc's
|
|
@@ -878,7 +884,7 @@ function generatePackageJson(choices, localePlan) {
|
|
|
878
884
|
// `/exclude` at module scope from the always-bundled chrome graph; #3110
|
|
879
885
|
// moved compileExclude into @takazudo/zudo-doc, so projects with both
|
|
880
886
|
// docHistory and assetViewer off no longer need the package at all.
|
|
881
|
-
deps["@takazudo/zudo-doc-history-server"] = "^5.
|
|
887
|
+
deps["@takazudo/zudo-doc-history-server"] = "^5.17.0";
|
|
882
888
|
// tsx is no longer needed here: the relocated package plugin imports the
|
|
883
889
|
// runner directly (no `tsx -e` spawn) since the package ships compiled
|
|
884
890
|
// dist/ — package-first migration #2321 (#2337).
|
package/package.json
CHANGED
|
@@ -445,17 +445,39 @@ function decodeHtmlAttributeValue(value) {
|
|
|
445
445
|
);
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
-
|
|
449
|
-
|
|
448
|
+
// Single shared anchor scan. `extractHtmlLinks` and
|
|
449
|
+
// `extractProtocolRelativeHtmlLinks` classify the SAME set of `<a href>`
|
|
450
|
+
// matches into disjoint buckets, so the grammar and the incremental line
|
|
451
|
+
// counting live here once — a fix to the anchor regex must never reach only
|
|
452
|
+
// one of the two callers.
|
|
453
|
+
function* iterateHtmlAnchorHrefs(html) {
|
|
450
454
|
const regex = /<a(?=\s)[^>]*?\shref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`\\]+))[^>]*>/gi;
|
|
451
455
|
let match;
|
|
452
456
|
let lastIndex = 0;
|
|
453
457
|
let line = 1;
|
|
454
458
|
while ((match = regex.exec(html)) !== null) {
|
|
455
|
-
const href = decodeHtmlAttributeValue(match[1] ?? match[2] ?? match[3]);
|
|
456
|
-
if (/^(?:https?:|mailto:|javascript:|data:|tel:)/i.test(href)) continue;
|
|
457
459
|
for (let i = lastIndex; i < match.index; i += 1) if (html[i] === "\n") line += 1;
|
|
458
460
|
lastIndex = match.index;
|
|
461
|
+
yield { href: decodeHtmlAttributeValue(match[1] ?? match[2] ?? match[3]), line };
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export function extractHtmlLinks(html) {
|
|
466
|
+
const links = [];
|
|
467
|
+
for (const { href, line } of iterateHtmlAnchorHrefs(html)) {
|
|
468
|
+
if (/^(?:https?:|\/\/|mailto:|javascript:|data:|tel:)/i.test(href)) continue;
|
|
469
|
+
links.push({ href, line });
|
|
470
|
+
}
|
|
471
|
+
return links;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Informational counterpart to extractHtmlLinks: same scan, but keeps only the
|
|
475
|
+
// protocol-relative hrefs that extractHtmlLinks classifies as external and
|
|
476
|
+
// skips (see #3921/#3930).
|
|
477
|
+
export function extractProtocolRelativeHtmlLinks(html) {
|
|
478
|
+
const links = [];
|
|
479
|
+
for (const { href, line } of iterateHtmlAnchorHrefs(html)) {
|
|
480
|
+
if (!/^\/\//.test(href)) continue;
|
|
459
481
|
links.push({ href, line });
|
|
460
482
|
}
|
|
461
483
|
return links;
|
|
@@ -547,12 +569,12 @@ export function extractMdxFragmentLinks(content) {
|
|
|
547
569
|
let match;
|
|
548
570
|
const markdownLink = /\]\(\s*([^\s)#]*#[^\s)]*)(?:\s+[^)]*)?\)/g;
|
|
549
571
|
while ((match = markdownLink.exec(searchLine)) !== null) {
|
|
550
|
-
if (!/^(?:https
|
|
572
|
+
if (!/^(?:https?:|\/\/|mailto:|javascript:|data:|tel:)/i.test(match[1])) links.push({ href: match[1], line: i + 1 });
|
|
551
573
|
}
|
|
552
574
|
const jsxHref = /\bhref\s*=\s*(?:"([^"]*#[^"]*)"|'([^']*#[^']*)')/g;
|
|
553
575
|
while ((match = jsxHref.exec(searchLine)) !== null) {
|
|
554
576
|
const href = match[1] ?? match[2];
|
|
555
|
-
if (!/^(?:https
|
|
577
|
+
if (!/^(?:https?:|\/\/|mailto:|javascript:|data:|tel:)/i.test(href)) links.push({ href, line: i + 1 });
|
|
556
578
|
}
|
|
557
579
|
}
|
|
558
580
|
return links;
|
|
@@ -731,6 +753,7 @@ export async function checkHtmlLinksAndTrailing(
|
|
|
731
753
|
const broken = [];
|
|
732
754
|
const anchors = [];
|
|
733
755
|
const trailingSlash = [];
|
|
756
|
+
const protocolRelative = [];
|
|
734
757
|
const idCache = new Map();
|
|
735
758
|
const cache = new Map();
|
|
736
759
|
const pages = [];
|
|
@@ -743,6 +766,13 @@ export async function checkHtmlLinksAndTrailing(
|
|
|
743
766
|
scanned.ids += ids.length;
|
|
744
767
|
idCache.set(file, new Set(ids));
|
|
745
768
|
pages.push({ file, links });
|
|
769
|
+
|
|
770
|
+
// Informational-only: classified from the content already in memory — no
|
|
771
|
+
// second read of the file.
|
|
772
|
+
const relFile = relative(rootDir, file);
|
|
773
|
+
for (const { href, line } of extractProtocolRelativeHtmlLinks(content)) {
|
|
774
|
+
protocolRelative.push({ file: relFile, line, href });
|
|
775
|
+
}
|
|
746
776
|
}
|
|
747
777
|
for (const { file, links } of pages) {
|
|
748
778
|
for (const { href, line } of links) {
|
|
@@ -777,7 +807,7 @@ export async function checkHtmlLinksAndTrailing(
|
|
|
777
807
|
}
|
|
778
808
|
}
|
|
779
809
|
}
|
|
780
|
-
return { broken, anchors, trailingSlash, scanned };
|
|
810
|
+
return { broken, anchors, trailingSlash, protocolRelative, scanned };
|
|
781
811
|
}
|
|
782
812
|
|
|
783
813
|
export async function checkMdxLinks(
|
|
@@ -802,7 +832,21 @@ export async function checkMdxLinks(
|
|
|
802
832
|
return warnings;
|
|
803
833
|
}
|
|
804
834
|
|
|
805
|
-
|
|
835
|
+
// The authority segment is everything after "//" up to the first "/", "?",
|
|
836
|
+
// or "#". A dotless, colonless authority (no TLD-shaped or host:port-shaped
|
|
837
|
+
// piece) is flagged as a likely internal-path typo — see formatReport below.
|
|
838
|
+
function protocolRelativeAuthority(href) {
|
|
839
|
+
const rest = href.slice(2);
|
|
840
|
+
const end = rest.search(/[/?#]/);
|
|
841
|
+
return end === -1 ? rest : rest.slice(0, end);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function isLikelyInternalPathTypo(href) {
|
|
845
|
+
const authority = protocolRelativeAuthority(href);
|
|
846
|
+
return !authority.includes(".") && !authority.includes(":");
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
export function formatReport(brokenLinks, mdxWarnings, trailingSlashWarnings = [], anchorWarnings = [], protocolRelative = []) {
|
|
806
850
|
const lines = [];
|
|
807
851
|
const section = (title, entries, format) => {
|
|
808
852
|
if (entries.length === 0) return;
|
|
@@ -814,6 +858,11 @@ export function formatReport(brokenLinks, mdxWarnings, trailingSlashWarnings = [
|
|
|
814
858
|
section("=== Absolute Links Bypassing Base Path (MDX Source) ===", mdxWarnings, (e) => `${e.file}:${e.line} ${e.href}`);
|
|
815
859
|
section("=== Links Missing Trailing Slash ===", trailingSlashWarnings, (e) => `${e.file}:${e.line} ${e.href}`);
|
|
816
860
|
section("=== Invalid Anchors ===", anchorWarnings, (e) => `${e.file}:${e.line} ${e.href} (fragment: #${e.fragment}; ${e.reason})`);
|
|
861
|
+
// Informational only — excluded from `total` / the ✓/✗ line / the
|
|
862
|
+
// non-strict "Issues found" note below, deliberately (see #3934).
|
|
863
|
+
section("=== Protocol-Relative Links (informational) ===", protocolRelative, (e) =>
|
|
864
|
+
`${e.file}:${e.line} ${e.href}${isLikelyInternalPathTypo(e.href) ? ` ← authority has no dot or colon; may be an internal-path typo (e.g. ${e.href} → ${e.href.slice(1)})` : ""}`,
|
|
865
|
+
);
|
|
817
866
|
const total = brokenLinks.length + mdxWarnings.length + trailingSlashWarnings.length + anchorWarnings.length;
|
|
818
867
|
if (total === 0) lines.push("✓ No broken links, invalid anchors, or absolute path issues found");
|
|
819
868
|
else {
|
|
@@ -852,8 +901,8 @@ async function main() {
|
|
|
852
901
|
console.log(`Checking links (base: ${config.basePath}, trailingSlash: ${config.trailingSlash})...`);
|
|
853
902
|
console.log(`Source scan: ${contentDirs.map((dir) => relative(rootDir, dir) || ".").join(", ")}${hasDist ? "; dist/ pass enabled" : "; dist/ absent (source-only)"}\n`);
|
|
854
903
|
|
|
855
|
-
const [{ broken, anchors: htmlAnchors, trailingSlash, scanned }, mdxWarnings, mdxAnchors] = await Promise.all([
|
|
856
|
-
hasDist ? checkHtmlLinksAndTrailing(distDir, rootDir, config.basePath, excludePatterns, config.trailingSlash) : Promise.resolve({ broken: [], anchors: [], trailingSlash: [], scanned: { links: 0, ids: 0 } }),
|
|
904
|
+
const [{ broken, anchors: htmlAnchors, trailingSlash, protocolRelative, scanned }, mdxWarnings, mdxAnchors] = await Promise.all([
|
|
905
|
+
hasDist ? checkHtmlLinksAndTrailing(distDir, rootDir, config.basePath, excludePatterns, config.trailingSlash) : Promise.resolve({ broken: [], anchors: [], trailingSlash: [], protocolRelative: [], scanned: { links: 0, ids: 0 } }),
|
|
857
906
|
checkMdxLinks(contentDirs, rootDir, hasDist ? distDir : null, config.basePath, config.localeKeys),
|
|
858
907
|
checkMdxAnchors(contentDirs, rootDir, config.basePath, config.localeKeys, excludePatterns),
|
|
859
908
|
]);
|
|
@@ -865,8 +914,9 @@ async function main() {
|
|
|
865
914
|
const realAbsolute = filter(mdxWarnings);
|
|
866
915
|
const realAnchors = filter(anchorWarnings);
|
|
867
916
|
const realTrailing = filter(trailingSlash);
|
|
868
|
-
console.log(formatReport(broken, mdxWarnings, trailingSlash, anchorWarnings));
|
|
917
|
+
console.log(formatReport(broken, mdxWarnings, trailingSlash, anchorWarnings, protocolRelative));
|
|
869
918
|
if (hasDist) console.log(`\nBuilt HTML scan: ${scanned.links} internal link${scanned.links === 1 ? "" : "s"} and ${scanned.ids} ID attribute${scanned.ids === 1 ? "" : "s"} inspected.`);
|
|
919
|
+
if (protocolRelative.length > 0) console.log(`Protocol-relative links: ${protocolRelative.length} found (informational only — see "Protocol-Relative Links" section above; not counted as issues).`);
|
|
870
920
|
const skipped = broken.length - realBroken.length + mdxWarnings.length - realAbsolute.length + anchorWarnings.length - realAnchors.length + trailingSlash.length - realTrailing.length;
|
|
871
921
|
if (skipped > 0) console.log(`\nAllowlist: ${skipped} known exception${skipped === 1 ? "" : "s"} excluded from strict-mode counts (${allowlistPath}).`);
|
|
872
922
|
let failed = false;
|