create-thally-docs 0.10.122 → 0.10.123
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
CHANGED
|
@@ -75,7 +75,7 @@ runs retain subprocess output for scripts and CI. These options also work with
|
|
|
75
75
|
| --- | --- |
|
|
76
76
|
| `create-thally-docs <dir>` | Scaffold a new project |
|
|
77
77
|
| `create-thally-docs migrate <github-or-docs-url-or-directory> [dir]` | Import a docs repository or public docs site through the shared migration engine |
|
|
78
|
-
| `create-thally-docs check [dir] [--fix] [--ci] [--drift] [--external]` | Lint content for broken links, orphan pages, and missing frontmatter; `--external` also checks links to public hosts (bounded, warnings only) |
|
|
78
|
+
| `create-thally-docs check [dir] [--fix] [--ci] [--drift] [--external]` | Lint content for MDX syntax errors (pages and snippets that cannot be parsed), broken links, orphan pages, and missing frontmatter; `--external` also checks links to public hosts (bounded, warnings only) |
|
|
79
79
|
| `create-thally-docs translate --locale <code>` | Translate content into another locale |
|
|
80
80
|
|
|
81
81
|
Public URL imports default to 1,000 pages. The web migration flow supports up to
|
|
@@ -492,7 +492,7 @@ async function discoverMigration(options) {
|
|
|
492
492
|
const cloneDir = join5(temporaryRoot, "repository");
|
|
493
493
|
try {
|
|
494
494
|
const cloneWarnings = [];
|
|
495
|
-
await terminal.step(`Cloning ${source.owner}/${source.repo}`, () => cloneGitHubRepository(source, cloneDir, cloneWarnings), "Source repository cloned");
|
|
495
|
+
await terminal.step(`Cloning ${source.owner}/${source.repo}`, () => cloneGitHubRepository(source, cloneDir, cloneWarnings, { docsDir: options.docsDir }), "Source repository cloned");
|
|
496
496
|
const sourceRefs = options.sourceRefs?.length ? await terminal.step("Importing sourceRef repositories", () => fetchSourceRefs(options.sourceRefs ?? [], cloneWarnings), "sourceRef repositories imported") : [];
|
|
497
497
|
const bundle = await terminal.step("Converting repository documentation", () => convertRepository({
|
|
498
498
|
sourceRefs,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
14
14
|
import { join, extname, relative } from "path";
|
|
15
15
|
import { execFileSync } from "child_process";
|
|
16
|
+
import { compile, pathToRegexp, regexpToFunction } from "path-to-regexp";
|
|
16
17
|
|
|
17
18
|
// src/frontmatter.ts
|
|
18
19
|
import { parse as parseYaml } from "yaml";
|
|
@@ -39,6 +40,7 @@ function parseFrontmatter(raw) {
|
|
|
39
40
|
|
|
40
41
|
// src/check.ts
|
|
41
42
|
import { parse as parseYaml2 } from "yaml";
|
|
43
|
+
import remarkGfm from "remark-gfm";
|
|
42
44
|
import remarkMdx2 from "remark-mdx";
|
|
43
45
|
import remarkParse2 from "remark-parse";
|
|
44
46
|
import { unified as unified2 } from "unified";
|
|
@@ -303,8 +305,9 @@ function manualApiProblems(file, data, hasOpenApiReference, mdx, docsApi) {
|
|
|
303
305
|
}
|
|
304
306
|
|
|
305
307
|
// src/check.ts
|
|
308
|
+
import { parseMdxTree } from "@thallylabs/core/markdown";
|
|
306
309
|
import { projectNavigationContract } from "@thallylabs/core/navigation";
|
|
307
|
-
import { plainInlineText, slugify } from "@thallylabs/core/slugify";
|
|
310
|
+
import { componentIds, constantString, fieldAnchorBase, fieldAnchorName, numberFieldIds, numberHeadingIds, plainInlineText, slugify } from "@thallylabs/core/slugify";
|
|
308
311
|
import { validateIntegrations } from "@thallylabs/migrate";
|
|
309
312
|
|
|
310
313
|
// src/external-links.ts
|
|
@@ -499,6 +502,19 @@ function scanMdx(dir, results) {
|
|
|
499
502
|
}
|
|
500
503
|
}
|
|
501
504
|
}
|
|
505
|
+
function compiledMdx(directory, results) {
|
|
506
|
+
let entries;
|
|
507
|
+
try {
|
|
508
|
+
entries = readdirSync(directory, { withFileTypes: true });
|
|
509
|
+
} catch {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
for (const entry of entries) {
|
|
513
|
+
const fullPath = join(directory, entry.name);
|
|
514
|
+
if (entry.isDirectory()) compiledMdx(fullPath, results);
|
|
515
|
+
else if (entry.isFile() && entry.name.endsWith(".mdx")) results.add(fullPath);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
502
518
|
function addOrphanToNav(projectDir, pageId) {
|
|
503
519
|
const config = readDocsJson(projectDir);
|
|
504
520
|
const tab = config.tabs.find((candidate) => !candidate.href && !candidate.api && (candidate.pages?.length || candidate.groups?.length));
|
|
@@ -532,70 +548,132 @@ function forEachNonFencedLine(content, visit) {
|
|
|
532
548
|
if (!fence) visit(line, index + 1);
|
|
533
549
|
}
|
|
534
550
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
551
|
+
var mdxParser = unified2().use(remarkParse2).use(remarkMdx2).use(remarkGfm);
|
|
552
|
+
var SNIPPET_IMPORT = /^\s*import\s+\{([^}]+)\}\s+from\s+['"]\/snippets\/[^'"]+['"];?\s*$/gm;
|
|
553
|
+
var LINE_BREAK = /\r\n|\r|\n/g;
|
|
554
|
+
function countAtMost(ascending, value) {
|
|
555
|
+
let low = 0;
|
|
556
|
+
let high = ascending.length;
|
|
557
|
+
while (low < high) {
|
|
558
|
+
const middle = low + high >> 1;
|
|
559
|
+
if (ascending[middle] <= value) low = middle + 1;
|
|
560
|
+
else high = middle;
|
|
561
|
+
}
|
|
562
|
+
return low;
|
|
563
|
+
}
|
|
564
|
+
function parseMdxPage(content) {
|
|
565
|
+
const removedAt = [];
|
|
566
|
+
const removedTotal = [];
|
|
567
|
+
let removed = 0;
|
|
568
|
+
const source = content.replace(SNIPPET_IMPORT, (match, _names, start) => {
|
|
569
|
+
removedAt.push(start - removed);
|
|
570
|
+
removed += match.length;
|
|
571
|
+
removedTotal.push(removed);
|
|
572
|
+
return "";
|
|
573
|
+
});
|
|
574
|
+
let newlines;
|
|
575
|
+
const lineAt = (offset) => {
|
|
576
|
+
const before = countAtMost(removedAt, offset);
|
|
577
|
+
const original = before === 0 ? offset : offset + removedTotal[before - 1];
|
|
578
|
+
if (!newlines) {
|
|
579
|
+
newlines = [];
|
|
580
|
+
for (const lineBreak of content.matchAll(LINE_BREAK)) newlines.push(lineBreak.index + lineBreak[0].length - 1);
|
|
552
581
|
}
|
|
582
|
+
return 1 + countAtMost(newlines, original - 1);
|
|
583
|
+
};
|
|
584
|
+
try {
|
|
585
|
+
return { tree: mdxParser.parse(source), lineAt };
|
|
586
|
+
} catch (error) {
|
|
587
|
+
const { reason, message, line, place } = error;
|
|
588
|
+
const offset = place?.offset ?? place?.start?.offset;
|
|
589
|
+
return {
|
|
590
|
+
error: {
|
|
591
|
+
reason: (reason ?? message ?? "The page could not be parsed").split("\n")[0].replace(/\.$/, ""),
|
|
592
|
+
line: offset === void 0 ? line ?? 1 : lineAt(offset)
|
|
593
|
+
},
|
|
594
|
+
lineAt
|
|
595
|
+
};
|
|
553
596
|
}
|
|
554
|
-
return visible;
|
|
555
597
|
}
|
|
556
|
-
var mdxParser = unified2().use(remarkParse2).use(remarkMdx2);
|
|
557
598
|
function stringAttribute(node, name) {
|
|
558
599
|
const attribute = node.attributes?.find((item) => item.type === "mdxJsxAttribute" && item.name === name);
|
|
559
600
|
if (!attribute) return void 0;
|
|
560
601
|
return typeof attribute.value === "string" ? attribute.value : null;
|
|
561
602
|
}
|
|
562
|
-
function
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
603
|
+
function constantAttribute(node, name) {
|
|
604
|
+
const attribute = node.attributes?.find((item) => item.type === "mdxJsxAttribute" && item.name === name);
|
|
605
|
+
if (!attribute) return void 0;
|
|
606
|
+
return typeof attribute.value === "string" ? attribute.value : constantString(attribute.value);
|
|
607
|
+
}
|
|
608
|
+
var HEADING_ID_MARKER = /^\s*\/\*\s*#(\S+?)\s*\*\/\s*$/;
|
|
609
|
+
function mdxSyntaxIssue(page, file, bodyLineOffset2, consequence) {
|
|
610
|
+
if (!page.error) return null;
|
|
611
|
+
return {
|
|
612
|
+
severity: "error",
|
|
613
|
+
message: `MDX syntax error: ${page.error.reason}. ${consequence}`,
|
|
614
|
+
file,
|
|
615
|
+
line: page.error.line + bodyLineOffset2
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function bodyLineOffset(raw, content) {
|
|
619
|
+
return raw.slice(0, raw.length - content.length).split(LINE_BREAK).length - 1;
|
|
620
|
+
}
|
|
621
|
+
function headingText(node, components, footnoteNumber) {
|
|
622
|
+
if (node.type === "text" || node.type === "inlineCode") return node.value ?? "";
|
|
623
|
+
if (node.type === "break") return "\n";
|
|
624
|
+
if (node.type === "footnoteReference") return String(footnoteNumber(node.identifier ?? ""));
|
|
625
|
+
if (node.type === "mdxTextExpression") return components ? constantString(node) ?? "" : "";
|
|
626
|
+
if (node.type === "mdxJsxTextElement" && !components) return "";
|
|
627
|
+
return (node.children ?? []).map((child) => headingText(child, components, footnoteNumber)).join("");
|
|
628
|
+
}
|
|
629
|
+
var STEP_HEADING_SIZES = ["h2", "h3", "h4"];
|
|
630
|
+
function readPageIds(content, parsed) {
|
|
631
|
+
const tree = parsed.tree ?? parseMdxTree(content);
|
|
632
|
+
const footnotes = /* @__PURE__ */ new Map();
|
|
633
|
+
const footnoteNumber = (identifier) => {
|
|
634
|
+
if (!footnotes.has(identifier)) footnotes.set(identifier, footnotes.size + 1);
|
|
635
|
+
return footnotes.get(identifier);
|
|
636
|
+
};
|
|
637
|
+
const page = { headings: [], fieldBases: [], reserved: /* @__PURE__ */ new Set() };
|
|
638
|
+
const visit = (node, stepSize, inNote) => {
|
|
572
639
|
let childSize = stepSize;
|
|
573
|
-
if (node.type === "
|
|
640
|
+
if (node.type === "footnoteReference" && !inNote) footnoteNumber(node.identifier ?? "");
|
|
641
|
+
if (node.type === "heading" && (node.depth ?? 0) >= 2) {
|
|
642
|
+
const children = node.children ?? [];
|
|
643
|
+
const marker = children.findIndex((child) => child.type === "mdxTextExpression" && HEADING_ID_MARKER.test(child.value ?? ""));
|
|
644
|
+
const given = marker < 0 ? void 0 : HEADING_ID_MARKER.exec(children[marker].value ?? "")[1];
|
|
645
|
+
const heading = marker < 0 ? node : { ...node, children: children.filter((_, index) => index !== marker) };
|
|
646
|
+
const base = given ?? (slugify(headingText(heading, false, footnoteNumber)) || slugify(headingText(heading, true, footnoteNumber)));
|
|
647
|
+
page.headings.push({ base, explicit: given !== void 0 });
|
|
648
|
+
} else if (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") {
|
|
649
|
+
const authored = constantAttribute(node, "id")?.trim();
|
|
650
|
+
if (authored) page.reserved.add(authored);
|
|
651
|
+
for (const id of componentIds(node.name ?? "", (attribute) => constantAttribute(node, attribute))) page.reserved.add(id);
|
|
574
652
|
if (node.name === "Steps") childSize = stringAttribute(node, "titleSize") ?? void 0;
|
|
575
653
|
if (node.name === "Step") {
|
|
576
654
|
const own = stringAttribute(node, "titleSize");
|
|
577
655
|
const size = own === void 0 ? stepSize : own;
|
|
578
656
|
const title = stringAttribute(node, "title")?.trim();
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
657
|
+
const stepId = constantAttribute(node, "id");
|
|
658
|
+
if (title && size && STEP_HEADING_SIZES.includes(size) && stepId !== null) {
|
|
659
|
+
const given = stepId?.trim();
|
|
660
|
+
page.headings.push({ base: given || slugify(plainInlineText(title)), explicit: !!given });
|
|
582
661
|
}
|
|
583
662
|
}
|
|
663
|
+
if (node.name === "ParamField" || node.name === "ResponseField") {
|
|
664
|
+
const name = fieldAnchorName(node.name, (attribute) => constantAttribute(node, attribute));
|
|
665
|
+
const base = name === void 0 ? void 0 : fieldAnchorBase(name);
|
|
666
|
+
if (base && constantAttribute(node, "id") === void 0) page.fieldBases.push(base);
|
|
667
|
+
}
|
|
584
668
|
}
|
|
585
|
-
for (const child of node.children ?? []) visit(child, childSize);
|
|
669
|
+
for (const child of node.children ?? []) visit(child, childSize, inNote || node.type === "footnoteDefinition");
|
|
586
670
|
};
|
|
587
|
-
visit(tree, void 0);
|
|
588
|
-
return
|
|
671
|
+
visit(tree, void 0, false);
|
|
672
|
+
return page;
|
|
589
673
|
}
|
|
590
|
-
function extractHeadingAnchors(content) {
|
|
674
|
+
function extractHeadingAnchors(content, parsed = parseMdxPage(content)) {
|
|
591
675
|
const anchors = /* @__PURE__ */ new Set();
|
|
592
|
-
|
|
593
|
-
forEachNonFencedLine(content, (line, lineNumber) => {
|
|
594
|
-
const heading = /^ {0,3}#{1,6}\s+(.+?)\s*#*\s*$/.exec(line);
|
|
595
|
-
if (heading) {
|
|
596
|
-
const explicit = /\{\/\*\s*#(\S+?)\s*\*\/\}$/.exec(heading[1]);
|
|
597
|
-
headings.push({ base: explicit?.[1] ?? slugify(renderedHeadingText(heading[1])), explicit: !!explicit, line: lineNumber });
|
|
598
|
-
}
|
|
676
|
+
forEachNonFencedLine(content, (line) => {
|
|
599
677
|
for (const idMatch of line.matchAll(EXPLICIT_ID_ATTRIBUTE)) {
|
|
600
678
|
const id = idMatch[1] ?? idMatch[2] ?? idMatch[3];
|
|
601
679
|
if (id) anchors.add(id);
|
|
@@ -605,26 +683,11 @@ function extractHeadingAnchors(content) {
|
|
|
605
683
|
if (name) anchors.add(name);
|
|
606
684
|
}
|
|
607
685
|
});
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const
|
|
611
|
-
const
|
|
612
|
-
const
|
|
613
|
-
for (const { base, explicit } of headings) {
|
|
614
|
-
let id = base;
|
|
615
|
-
let n = 1;
|
|
616
|
-
if (explicit) {
|
|
617
|
-
while (used.has(id)) id = `${base}-${++n}`;
|
|
618
|
-
} else {
|
|
619
|
-
n = occurrences.get(base) ?? 0;
|
|
620
|
-
do
|
|
621
|
-
id = ++n === 1 ? base : `${base}-${n}`;
|
|
622
|
-
while (used.has(id) || reserved.has(id));
|
|
623
|
-
occurrences.set(base, n);
|
|
624
|
-
}
|
|
625
|
-
used.add(id);
|
|
626
|
-
anchors.add(id);
|
|
627
|
-
}
|
|
686
|
+
const page = readPageIds(content, parsed);
|
|
687
|
+
const { ids, used } = numberHeadingIds(page.headings);
|
|
688
|
+
for (const id of ids) if (id) anchors.add(id);
|
|
689
|
+
for (const id of numberFieldIds(page.fieldBases, used, page.reserved)) anchors.add(id);
|
|
690
|
+
for (const id of page.reserved) anchors.add(id);
|
|
628
691
|
return anchors;
|
|
629
692
|
}
|
|
630
693
|
function decodedFragment(fragment) {
|
|
@@ -672,34 +735,45 @@ function pageIdToPath(pageId, secondaryLocales) {
|
|
|
672
735
|
const basePath = navPageId === "introduction" ? "/" : `/${navPageId}`;
|
|
673
736
|
return locale ? `/${locale}${basePath === "/" ? "" : basePath}` : basePath;
|
|
674
737
|
}
|
|
675
|
-
function
|
|
676
|
-
|
|
677
|
-
const
|
|
678
|
-
let
|
|
679
|
-
|
|
680
|
-
const
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
return;
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
});
|
|
692
|
-
return { regex: new RegExp(`^${pattern}$`), paramNames, destination };
|
|
738
|
+
function compileRedirectSource(source, destination) {
|
|
739
|
+
const bare = source.replace(/\/$/, "") || "/";
|
|
740
|
+
const keys = [];
|
|
741
|
+
let match = null;
|
|
742
|
+
try {
|
|
743
|
+
const regexp = pathToRegexp(bare, keys, { delimiter: "/", sensitive: false, strict: true });
|
|
744
|
+
const matcher = regexpToFunction(new RegExp(regexp.source.replace(/\^/, "^(?!\\/_next)").replace(/\$$/, "(?:\\/)?$"), regexp.flags), keys);
|
|
745
|
+
match = (path) => {
|
|
746
|
+
const found = matcher(path);
|
|
747
|
+
if (!found) return false;
|
|
748
|
+
for (const key of keys) if (typeof key.name === "number") delete found.params[key.name];
|
|
749
|
+
return found;
|
|
750
|
+
};
|
|
751
|
+
} catch {
|
|
752
|
+
}
|
|
753
|
+
return { match, destination, literal: !/[:*+?(){}\\]/.test(bare) };
|
|
693
754
|
}
|
|
694
|
-
function
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
755
|
+
function applyRedirect(rule, path) {
|
|
756
|
+
const found = rule.match?.(path);
|
|
757
|
+
if (!found) return void 0;
|
|
758
|
+
if (/^(?:https?:)?\/\//i.test(rule.destination)) return rule.destination;
|
|
759
|
+
try {
|
|
760
|
+
return compile(rule.destination.replace(/\?[^#]*/, ""), { validate: false })(found.params);
|
|
761
|
+
} catch {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
function matchRedirect(rules, path) {
|
|
766
|
+
for (const rule of rules) {
|
|
767
|
+
if (!rule.match) return "unsure";
|
|
768
|
+
const to = applyRedirect(rule, path);
|
|
769
|
+
if (to === null) return "unsure";
|
|
770
|
+
if (to !== void 0) return { to };
|
|
771
|
+
}
|
|
772
|
+
return void 0;
|
|
699
773
|
}
|
|
700
774
|
var MAX_REDIRECT_HOPS = 20;
|
|
701
775
|
var MISSING_DESCRIPTION = 'Missing "description" in frontmatter';
|
|
702
|
-
function resolveLink(target, redirects
|
|
776
|
+
function resolveLink(target, redirects) {
|
|
703
777
|
let current = target;
|
|
704
778
|
let anchor;
|
|
705
779
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -711,14 +785,10 @@ function resolveLink(target, redirects, dynamicRedirects = []) {
|
|
|
711
785
|
const path = beforeHash.split("?", 1)[0].replace(/\/$/, "") || "/";
|
|
712
786
|
if (seen.has(path) || seen.size >= MAX_REDIRECT_HOPS) return { path, anchor, cycle: true };
|
|
713
787
|
seen.add(path);
|
|
714
|
-
const
|
|
715
|
-
if (
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
}
|
|
719
|
-
const dynamicMatch = dynamicRedirects.map((redirect) => ({ redirect, match: path.match(redirect.regex) })).find((candidate) => candidate.match !== null);
|
|
720
|
-
if (!dynamicMatch) return { path, anchor };
|
|
721
|
-
current = resolveDynamicRedirectDestination(dynamicMatch.redirect, dynamicMatch.match);
|
|
788
|
+
const found = matchRedirect(redirects, path);
|
|
789
|
+
if (found === "unsure") return { path, anchor, unsure: true };
|
|
790
|
+
if (!found) return { path, anchor };
|
|
791
|
+
current = found.to;
|
|
722
792
|
}
|
|
723
793
|
}
|
|
724
794
|
function checkPublicSpecs(projectDir, tabs, issues) {
|
|
@@ -898,16 +968,7 @@ async function runCheck(projectDir, options) {
|
|
|
898
968
|
"/api",
|
|
899
969
|
...Array.from(secondaryLocales, (locale) => `/${locale}/api`)
|
|
900
970
|
]);
|
|
901
|
-
const
|
|
902
|
-
(config.redirects ?? []).filter((redirect) => !redirect.source.includes(":")).map((redirect) => [
|
|
903
|
-
redirect.source.replace(/\/$/, "") || "/",
|
|
904
|
-
redirect.destination
|
|
905
|
-
])
|
|
906
|
-
);
|
|
907
|
-
const dynamicRedirects = (config.redirects ?? []).flatMap((redirect) => {
|
|
908
|
-
const compiled = compileDynamicRedirectSource(redirect.source, redirect.destination);
|
|
909
|
-
return compiled ? [compiled] : [];
|
|
910
|
-
});
|
|
971
|
+
const redirectRules = (config.redirects ?? []).map(({ source, destination }) => compileRedirectSource(source, destination));
|
|
911
972
|
for (const message of validateIntegrations(config.integrations)) {
|
|
912
973
|
issues.push({ severity: "warning", message, file: "docs.json" });
|
|
913
974
|
}
|
|
@@ -927,11 +988,16 @@ async function runCheck(projectDir, options) {
|
|
|
927
988
|
}
|
|
928
989
|
const allFiles = [];
|
|
929
990
|
if (existsSync(contentDir)) scanMdx(contentDir, allFiles);
|
|
991
|
+
const compiledPages = /* @__PURE__ */ new Set();
|
|
992
|
+
compiledMdx(contentDir, compiledPages);
|
|
993
|
+
const compiledSnippets = /* @__PURE__ */ new Set();
|
|
994
|
+
compiledMdx(join(projectDir, "snippets"), compiledSnippets);
|
|
930
995
|
const fixedOrphans = [];
|
|
931
996
|
const operationPages = [];
|
|
932
997
|
const validPaths = /* @__PURE__ */ new Set(["/"]);
|
|
933
998
|
const anchorsByPath = /* @__PURE__ */ new Map();
|
|
934
999
|
const linksByFile = [];
|
|
1000
|
+
const linkStubs = [];
|
|
935
1001
|
for (const filePath of allFiles) {
|
|
936
1002
|
const rel = filePath.slice(contentDir.length + 1).replace(/\.mdx$/, "").replace(/\\/g, "/");
|
|
937
1003
|
const pageId = rel.endsWith("/index") ? rel.slice(0, -6) : rel;
|
|
@@ -943,7 +1009,7 @@ async function runCheck(projectDir, options) {
|
|
|
943
1009
|
const parsed = parseFrontmatter(raw);
|
|
944
1010
|
data = parsed.data;
|
|
945
1011
|
content = parsed.content;
|
|
946
|
-
lineOffset = raw
|
|
1012
|
+
lineOffset = bodyLineOffset(raw, content);
|
|
947
1013
|
} catch {
|
|
948
1014
|
issues.push({ severity: "error", message: `Could not parse frontmatter`, file: relative(projectDir, filePath) });
|
|
949
1015
|
continue;
|
|
@@ -960,23 +1026,38 @@ async function runCheck(projectDir, options) {
|
|
|
960
1026
|
const rel2 = relative(projectDir, filePath);
|
|
961
1027
|
if (!data.title) issues.push({ severity: "warning", message: `Missing "title" in frontmatter`, file: rel2 });
|
|
962
1028
|
const reference = parseDocReference(data.openapi);
|
|
963
|
-
|
|
1029
|
+
const path = pageIdToPath(pageId, secondaryLocales);
|
|
1030
|
+
const isLinkStub = data.noindex === true && redirectRules.some((rule) => rule.literal && rule.match?.(path));
|
|
1031
|
+
const notices = isLinkStub ? [] : issues;
|
|
1032
|
+
if (!data.description && !reference) notices.push({ severity: "warning", message: MISSING_DESCRIPTION, file: rel2 });
|
|
964
1033
|
if (typeof data.openapi !== "string" && content.trim().length < 50) {
|
|
965
|
-
|
|
1034
|
+
notices.push({ severity: "warning", message: `Very short body (${content.trim().length} chars) \u2014 page may be empty`, file: rel2 });
|
|
966
1035
|
}
|
|
1036
|
+
if (isLinkStub) linkStubs.push({ file: rel2, path, issues: notices });
|
|
967
1037
|
if (reference) operationPages.push({ file: rel2, reference, hasDescription: Boolean(data.description) });
|
|
968
1038
|
for (const message of manualApiProblems(rel2, data, Boolean(reference), content, config.api)) {
|
|
969
1039
|
issues.push({ severity: "warning", file: rel2, message });
|
|
970
1040
|
}
|
|
971
1041
|
if (options.drift) checkDrift(projectDir, rel2, data, issues);
|
|
972
|
-
const
|
|
973
|
-
const
|
|
1042
|
+
const page = parseMdxPage(content);
|
|
1043
|
+
const syntaxIssue = compiledPages.has(filePath) ? mdxSyntaxIssue(page, rel2, lineOffset, "The page cannot render until this is fixed.") : null;
|
|
1044
|
+
if (syntaxIssue) issues.push(syntaxIssue);
|
|
1045
|
+
const anchors = extractHeadingAnchors(content, page);
|
|
974
1046
|
validPaths.add(path);
|
|
975
1047
|
validPaths.add(`/${pageId}`);
|
|
976
1048
|
anchorsByPath.set(path, anchors);
|
|
977
1049
|
anchorsByPath.set(`/${pageId}`, anchors);
|
|
978
1050
|
linksByFile.push({ file: rel2, path, anchors, links: extractLinks(content), images: extractImageRefs(content), offset: lineOffset });
|
|
979
1051
|
}
|
|
1052
|
+
for (const filePath of compiledSnippets) {
|
|
1053
|
+
try {
|
|
1054
|
+
const raw = readFileSync(filePath, "utf8");
|
|
1055
|
+
const { content } = parseFrontmatter(raw);
|
|
1056
|
+
const issue = mdxSyntaxIssue(parseMdxPage(content), relative(projectDir, filePath), bodyLineOffset(raw, content), "Pages that use this snippet cannot render until this is fixed.");
|
|
1057
|
+
if (issue) issues.push(issue);
|
|
1058
|
+
} catch {
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
980
1061
|
for (const [path, anchors] of [...anchorsByPath]) {
|
|
981
1062
|
if (secondaryLocales.has(path.split("/")[1])) continue;
|
|
982
1063
|
for (const locale of secondaryLocales) {
|
|
@@ -986,6 +1067,16 @@ async function runCheck(projectDir, options) {
|
|
|
986
1067
|
anchorsByPath.set(localized, anchors);
|
|
987
1068
|
}
|
|
988
1069
|
}
|
|
1070
|
+
const isGeneratedPath = (path) => Array.from(generatedApiPaths).some((prefix) => path === prefix || path.startsWith(`${prefix}/`)) || path.startsWith("/_next") || /\.[a-z0-9]+$/i.test(path);
|
|
1071
|
+
for (const { file, path, issues: notices } of linkStubs) {
|
|
1072
|
+
const { path: destination, cycle, external, unsure } = resolveLink(path, redirectRules);
|
|
1073
|
+
if (external || unsure || !cycle && (isGeneratedPath(destination) || validPaths.has(destination))) continue;
|
|
1074
|
+
issues.push(...notices, {
|
|
1075
|
+
severity: "error",
|
|
1076
|
+
message: cycle ? `Broken redirect: "${path}" redirects in a loop, so the page it stands in for cannot be reached` : `Broken redirect: "${path}" redirects to "${destination}", where there is no page`,
|
|
1077
|
+
file
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
989
1080
|
const externalLinks = [];
|
|
990
1081
|
for (const { file, anchors, links, offset } of linksByFile) {
|
|
991
1082
|
for (const { target, line: contentLine } of links) {
|
|
@@ -1002,12 +1093,9 @@ async function runCheck(projectDir, options) {
|
|
|
1002
1093
|
continue;
|
|
1003
1094
|
}
|
|
1004
1095
|
if (!target.startsWith("/")) continue;
|
|
1005
|
-
const { path, anchor, cycle, external } = resolveLink(target,
|
|
1006
|
-
if (external) continue;
|
|
1007
|
-
|
|
1008
|
-
(prefix) => path === prefix || path.startsWith(`${prefix}/`)
|
|
1009
|
-
);
|
|
1010
|
-
if (!cycle && (isGeneratedApiPath || path.startsWith("/_next") || /\.[a-z0-9]+$/i.test(path))) continue;
|
|
1096
|
+
const { path, anchor, cycle, external, unsure } = resolveLink(target, redirectRules);
|
|
1097
|
+
if (external || unsure) continue;
|
|
1098
|
+
if (!cycle && isGeneratedPath(path)) continue;
|
|
1011
1099
|
if (cycle || !validPaths.has(path)) {
|
|
1012
1100
|
issues.push({ severity: "error", message: `Broken link: "${target}" \u2014 no page at "${path}"`, file, line });
|
|
1013
1101
|
} else if (anchor && !anchorsByPath.get(path)?.has(decodedFragment(anchor))) {
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
migrateDocs
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-NITY7H3R.js";
|
|
5
5
|
import {
|
|
6
6
|
parseFrontmatter,
|
|
7
7
|
runCheck
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-WKJN3RIC.js";
|
|
9
9
|
import "./chunk-KDLP27DS.js";
|
|
10
10
|
import {
|
|
11
11
|
logo,
|
package/dist/migrate/index.js
CHANGED
package/dist/migration-worker.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-thally-docs",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.123",
|
|
4
4
|
"description": "Scaffold an open-source Thally docs site for people, search engines, and AI tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -53,9 +53,11 @@
|
|
|
53
53
|
"@anthropic-ai/sdk": "^0.36.0",
|
|
54
54
|
"@clack/prompts": "^0.11.0",
|
|
55
55
|
"@inquirer/prompts": "^7.0.0",
|
|
56
|
-
"@thallylabs/core": "0.2.
|
|
57
|
-
"@thallylabs/migrate": "0.2.
|
|
56
|
+
"@thallylabs/core": "0.2.18",
|
|
57
|
+
"@thallylabs/migrate": "0.2.44",
|
|
58
58
|
"p-limit": "^6.1.0",
|
|
59
|
+
"path-to-regexp": "6.3.0",
|
|
60
|
+
"remark-gfm": "^4.0.1",
|
|
59
61
|
"remark-mdx": "^3.1.1",
|
|
60
62
|
"remark-parse": "^11.0.0",
|
|
61
63
|
"tar": "^7.5.22",
|