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 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
@@ -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.16.1";
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.16.1";
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
- "@takazudo/zfb": "2.14.3",
752
- "@takazudo/zfb-runtime": "2.14.3",
753
- "@takazudo/zfb-md-wasm": "2.14.3",
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.16.1";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-zudo-doc",
3
- "version": "5.16.1",
3
+ "version": "5.17.0",
4
4
  "description": "Create a new zudo-doc documentation site",
5
5
  "license": "MIT",
6
6
  "author": "Takeshi Takatsudo",
@@ -445,17 +445,39 @@ function decodeHtmlAttributeValue(value) {
445
445
  );
446
446
  }
447
447
 
448
- export function extractHtmlLinks(html) {
449
- const links = [];
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?:|mailto:|javascript:|data:|tel:)/i.test(match[1])) links.push({ href: match[1], line: i + 1 });
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?:|mailto:|javascript:|data:|tel:)/i.test(href)) links.push({ href, line: i + 1 });
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
- export function formatReport(brokenLinks, mdxWarnings, trailingSlashWarnings = [], anchorWarnings = []) {
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;