dogsbay 0.2.0-beta.11 → 0.2.0-beta.111
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/dist/audit/deliverable-vars.d.ts +18 -0
- package/dist/audit/deliverable-vars.js +143 -0
- package/dist/audit/dist/crawler.d.ts +23 -0
- package/dist/audit/lib/collect-assets.d.ts +7 -0
- package/dist/audit/lib/collect-assets.js +64 -0
- package/dist/audit/lib/validate-refs.d.ts +37 -0
- package/dist/audit/lib/validate-refs.js +132 -0
- package/dist/audit/output/json.d.ts +7 -0
- package/dist/audit/output/text.d.ts +6 -0
- package/dist/audit/registry.d.ts +55 -0
- package/dist/audit/rules/seo/description-length.d.ts +16 -0
- package/dist/audit/rules/seo/description-missing.d.ts +15 -0
- package/dist/audit/rules/seo/dist-meta.d.ts +22 -0
- package/dist/audit/rules/seo/h1-brand-keyword.d.ts +2 -0
- package/dist/audit/rules/seo/index.d.ts +11 -0
- package/dist/audit/rules/seo/json-ld-parse.d.ts +13 -0
- package/dist/audit/rules/seo/json-ld-required-fields.d.ts +21 -0
- package/dist/audit/rules/seo/keyword-placement.d.ts +2 -0
- package/dist/audit/rules/seo/sitemap.d.ts +5 -0
- package/dist/audit/rules/seo/sitemap.js +125 -8
- package/dist/audit/rules/seo/title-missing.d.ts +20 -0
- package/dist/audit/rules/structure/asset-refs.d.ts +23 -0
- package/dist/audit/rules/structure/asset-refs.js +41 -0
- package/dist/audit/rules/structure/index.d.ts +11 -0
- package/dist/audit/rules/structure/index.js +10 -0
- package/dist/audit/rules/structure/internal-links.d.ts +22 -0
- package/dist/audit/rules/structure/internal-links.js +28 -0
- package/dist/audit/rules/structure/locale-coherence.d.ts +17 -0
- package/dist/audit/rules/structure/namespace-coherence.d.ts +17 -0
- package/dist/audit/rules/structure/nav-target-exists.d.ts +2 -0
- package/dist/audit/rules/structure/nav-target-exists.js +107 -0
- package/dist/audit/rules/structure/topicmap-valid.d.ts +12 -0
- package/dist/audit/rules/structure/topicmap-valid.js +25 -0
- package/dist/audit/rules/structure/unresolved-directives.d.ts +2 -0
- package/dist/audit/rules/structure/unresolved-directives.js +108 -0
- package/dist/audit/rules/structure/version-coherence.d.ts +25 -0
- package/dist/audit/run.d.ts +85 -0
- package/dist/audit/run.js +4 -0
- package/dist/audit/types.d.ts +203 -0
- package/dist/commands/add.d.ts +8 -0
- package/dist/commands/add.js +1 -1
- package/dist/commands/agent-run.js +48 -0
- package/dist/commands/agent.d.ts +6 -0
- package/dist/commands/chunk.js +121 -0
- package/dist/commands/convert.d.ts +64 -0
- package/dist/commands/convert.js +656 -52
- package/dist/commands/corpus.js +63 -0
- package/dist/commands/deliverables.d.ts +7 -0
- package/dist/commands/deliverables.js +41 -0
- package/dist/commands/embed.js +256 -0
- package/dist/commands/enrich.js +192 -0
- package/dist/commands/export-techdocs.d.ts +13 -0
- package/dist/commands/import-dita.d.ts +23 -0
- package/dist/commands/import-dita.js +129 -0
- package/dist/commands/import-ditamap.d.ts +37 -0
- package/dist/commands/import-ditamap.js +284 -0
- package/dist/commands/import-mkdocs.d.ts +9 -0
- package/dist/commands/import-mkdocs.js +112 -69
- package/dist/commands/index-build.js +59 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/lighthouse.d.ts +13 -0
- package/dist/commands/migrate-asciidoc.d.ts +72 -0
- package/dist/commands/migrate-asciidoc.js +1078 -0
- package/dist/commands/migrate-docusaurus.d.ts +44 -0
- package/dist/commands/migrate-docusaurus.js +464 -0
- package/dist/commands/migrate-fluidtopics-rest.d.ts +21 -0
- package/dist/commands/migrate-fluidtopics-rest.js +149 -0
- package/dist/commands/migrate-fluidtopics.d.ts +13 -0
- package/dist/commands/migrate-fluidtopics.js +158 -0
- package/dist/commands/migrate-gitbook.d.ts +11 -0
- package/dist/commands/migrate-gitbook.js +296 -0
- package/dist/commands/migrate-hugo.d.ts +13 -0
- package/dist/commands/migrate-hugo.js +297 -0
- package/dist/commands/migrate-mkdocs.d.ts +87 -0
- package/dist/commands/migrate-mkdocs.js +1005 -0
- package/dist/commands/migrate-myst.d.ts +9 -0
- package/dist/commands/migrate-myst.js +239 -0
- package/dist/commands/migrate-rst.d.ts +11 -0
- package/dist/commands/migrate-rst.js +240 -0
- package/dist/commands/preprocess-variants.d.ts +3 -0
- package/dist/commands/pull.d.ts +69 -0
- package/dist/commands/pull.js +154 -33
- package/dist/commands/search.js +144 -0
- package/dist/commands/site-build.d.ts +174 -0
- package/dist/commands/site-build.js +510 -12
- package/dist/commands/site-check.d.ts +35 -0
- package/dist/commands/site-check.js +95 -6
- package/dist/commands/site-dev.d.ts +30 -0
- package/dist/commands/site-dev.js +39 -11
- package/dist/commands/site-init.d.ts +44 -0
- package/dist/commands/site-init.js +62 -7
- package/dist/commands/source.js +777 -0
- package/dist/commands/topicmap.d.ts +16 -0
- package/dist/commands/topicmap.js +123 -0
- package/dist/config/defaults.d.ts +14 -0
- package/dist/config/defaults.js +40 -22
- package/dist/config/deliverables.d.ts +44 -0
- package/dist/config/deliverables.js +68 -0
- package/dist/config/find.d.ts +6 -0
- package/dist/config/index.d.ts +13 -0
- package/dist/config/index.js +2 -1
- package/dist/config/load.d.ts +25 -0
- package/dist/config/load.js +385 -40
- package/dist/config/output.d.ts +2 -0
- package/dist/config/schema.d.ts +1082 -0
- package/dist/config/schema.js +6 -0
- package/dist/config/serialize.d.ts +3 -0
- package/dist/config/to-astro-options.d.ts +18 -0
- package/dist/config/to-astro-options.js +52 -1
- package/dist/corpus/agent-config.js +212 -0
- package/dist/corpus/bench.js +694 -0
- package/dist/corpus/classify-structured.js +1132 -0
- package/dist/corpus/classify.js +331 -0
- package/dist/corpus/comparison.js +999 -0
- package/dist/corpus/compile-check.js +307 -0
- package/dist/corpus/curate.js +808 -0
- package/dist/corpus/delta.js +314 -0
- package/dist/corpus/endpoints.js +102 -0
- package/dist/corpus/exchange.js +189 -0
- package/dist/corpus/feed.js +176 -0
- package/dist/corpus/git.js +34 -0
- package/dist/corpus/init.js +135 -0
- package/dist/corpus/judgment-cache.js +109 -0
- package/dist/corpus/near-duplicates.js +192 -0
- package/dist/corpus/ordering.js +182 -0
- package/dist/corpus/pack.js +83 -0
- package/dist/corpus/partition.js +0 -0
- package/dist/corpus/propose.js +365 -0
- package/dist/corpus/pull.js +96 -0
- package/dist/corpus/record-validation.js +167 -0
- package/dist/corpus/record.js +194 -0
- package/dist/corpus/review.js +116 -0
- package/dist/corpus/skill.js +444 -0
- package/dist/corpus/sources.js +199 -0
- package/dist/corpus/store.js +203 -0
- package/dist/corpus/update.js +577 -0
- package/dist/corpus/versions.js +54 -0
- package/dist/engine-cli.d.ts +19 -0
- package/dist/engine-cli.js +142 -0
- package/dist/formats.d.ts +122 -0
- package/dist/formats.js +179 -0
- package/dist/import-content.d.ts +185 -0
- package/dist/import-content.js +253 -71
- package/dist/index.d.ts +2 -0
- package/dist/index.js +390 -25
- package/dist/lib.d.ts +17 -0
- package/dist/lib.js +17 -0
- package/dist/passthrough-astro.d.ts +35 -0
- package/dist/passthrough-astro.js +152 -0
- package/dist/plugins/context.d.ts +36 -0
- package/dist/plugins/index.d.ts +11 -0
- package/dist/plugins/lifecycle.d.ts +76 -0
- package/dist/plugins/loader.d.ts +38 -0
- package/dist/registry.d.ts +9 -0
- package/dist/registry.js +8 -0
- package/dist/resolve-autodoc.d.ts +24 -0
- package/dist/resolve-autodoc.js +236 -0
- package/dist/site-build/preprocess.d.ts +140 -0
- package/dist/site-build/preprocess.js +654 -0
- package/dist/source-resolver.d.ts +60 -0
- package/dist/topicmap-nav.d.ts +45 -0
- package/dist/topicmap-nav.js +112 -0
- package/dist/utils/git.d.ts +8 -0
- package/dist/utils/git.js +34 -0
- package/dist/utils/gitignore.d.ts +8 -0
- package/dist/utils/gitignore.js +54 -0
- package/dist/utils/link-labels.d.ts +25 -0
- package/dist/utils/link-labels.js +67 -0
- package/dist/utils/project.d.ts +3 -0
- package/dist/utils/remote-includes.d.ts +35 -0
- package/dist/utils/remote-includes.js +317 -0
- package/dist/versioning/docusaurus.d.ts +53 -0
- package/dist/versioning/docusaurus.js +136 -0
- package/dist/versioning/lineage.d.ts +107 -0
- package/dist/versioning/lineage.js +212 -0
- package/package.json +50 -18
- package/skills/platform/config-yml/SKILL.md +27 -0
- package/skills/platform/frontmatter-fields/SKILL.md +19 -0
- package/skills/platform/markdown-directives/SKILL.md +85 -0
- package/skills/platform/migration-shape/SKILL.md +314 -0
- package/skills/platform/nav-file/SKILL.md +25 -0
- package/skills/platform/taxonomy-config/SKILL.md +89 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Issue } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Root identifiers referenced by an expression: quoted strings and
|
|
4
|
+
* dotted property tails stripped, reserved words and numbers excluded.
|
|
5
|
+
* `platform == "win" and product.name` → {platform, product}.
|
|
6
|
+
*/
|
|
7
|
+
export declare function expressionIdentifiers(expr: string): Set<string>;
|
|
8
|
+
export interface DeliverableVarsOptions {
|
|
9
|
+
/** Resolved source content directories to scan. */
|
|
10
|
+
contentDirs: string[];
|
|
11
|
+
/** The deliverable's merged attribute context. */
|
|
12
|
+
attributes: Record<string, unknown>;
|
|
13
|
+
/** Deliverable name, for the message. */
|
|
14
|
+
deliverable: string;
|
|
15
|
+
/** Paths in issues are made relative to this root. */
|
|
16
|
+
siteRoot: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function auditDeliverableVariables(options: DeliverableVarsOptions): Promise<Issue[]>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Undefined-variable audit for deliverables (plans/deliverables.md
|
|
3
|
+
* Phase 1): preserve-parse every source markdown file, collect the
|
|
4
|
+
* variables referenced by `{{ … }}` / `{% if … %}` / inline spans, and
|
|
5
|
+
* report any not defined by the deliverable's merged attribute context.
|
|
6
|
+
* A `{% if distro == "rosa" %}` whose variable NO deliverable defines is
|
|
7
|
+
* almost certainly a typo — and invisible at build time, because the
|
|
8
|
+
* terminal build treats undefined conditions as falsy and silently drops
|
|
9
|
+
* the block.
|
|
10
|
+
*
|
|
11
|
+
* Command-side helper (does I/O); the result is surfaced through a
|
|
12
|
+
* registered corpus rule closure so it joins the normal audit report and
|
|
13
|
+
* exit-code accounting.
|
|
14
|
+
*/
|
|
15
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
16
|
+
import { join, relative } from "node:path";
|
|
17
|
+
import { dogsbayMdToTree } from "@dogsbay/format-dogsbay-md";
|
|
18
|
+
/** Words that appear in expressions but are never variables. */
|
|
19
|
+
const EXPR_RESERVED = new Set([
|
|
20
|
+
"true",
|
|
21
|
+
"false",
|
|
22
|
+
"null",
|
|
23
|
+
"none",
|
|
24
|
+
"and",
|
|
25
|
+
"or",
|
|
26
|
+
"not",
|
|
27
|
+
"in",
|
|
28
|
+
"is",
|
|
29
|
+
"defined",
|
|
30
|
+
"undefined",
|
|
31
|
+
]);
|
|
32
|
+
/**
|
|
33
|
+
* Root identifiers referenced by an expression: quoted strings and
|
|
34
|
+
* dotted property tails stripped, reserved words and numbers excluded.
|
|
35
|
+
* `platform == "win" and product.name` → {platform, product}.
|
|
36
|
+
*/
|
|
37
|
+
export function expressionIdentifiers(expr) {
|
|
38
|
+
const out = new Set();
|
|
39
|
+
const stripped = expr
|
|
40
|
+
.replace(/"[^"]*"|'[^']*'/g, " ")
|
|
41
|
+
.replace(/\.[A-Za-z_][A-Za-z0-9_-]*/g, "");
|
|
42
|
+
for (const m of stripped.matchAll(/[A-Za-z_][A-Za-z0-9_-]*/g)) {
|
|
43
|
+
const id = m[0];
|
|
44
|
+
if (!EXPR_RESERVED.has(id.toLowerCase()))
|
|
45
|
+
out.add(id);
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
function collectInline(nodes, acc) {
|
|
50
|
+
for (const n of nodes ?? []) {
|
|
51
|
+
const t = n.type;
|
|
52
|
+
if (t === "variable") {
|
|
53
|
+
for (const id of expressionIdentifiers(n.expr)) {
|
|
54
|
+
addRef(acc, id, "{{ … }}");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else if (t === "conditional-span") {
|
|
58
|
+
const span = n;
|
|
59
|
+
for (const id of expressionIdentifiers(span.test))
|
|
60
|
+
addRef(acc, id, "{% if %}");
|
|
61
|
+
collectInline(span.children, acc);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const children = n.children;
|
|
65
|
+
if (children)
|
|
66
|
+
collectInline(children, acc);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function addRef(acc, id, kind) {
|
|
71
|
+
const kinds = acc.refs.get(id) ?? new Set();
|
|
72
|
+
kinds.add(kind);
|
|
73
|
+
acc.refs.set(id, kinds);
|
|
74
|
+
}
|
|
75
|
+
function collectBlocks(nodes, acc) {
|
|
76
|
+
for (const node of nodes) {
|
|
77
|
+
if (node.type === "conditional") {
|
|
78
|
+
for (const id of expressionIdentifiers(String(node.props?.test ?? ""))) {
|
|
79
|
+
addRef(acc, id, "{% if %}");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (node.type === "var-def") {
|
|
83
|
+
acc.defined.add(String(node.props?.name ?? ""));
|
|
84
|
+
for (const id of expressionIdentifiers(String(node.props?.value ?? ""))) {
|
|
85
|
+
addRef(acc, id, "{% set %}");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (node.inline)
|
|
89
|
+
collectInline(node.inline, acc);
|
|
90
|
+
if (node.children)
|
|
91
|
+
collectBlocks(node.children, acc);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function mdFiles(dir) {
|
|
95
|
+
const out = [];
|
|
96
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
99
|
+
continue;
|
|
100
|
+
const full = join(dir, entry.name);
|
|
101
|
+
if (entry.isDirectory())
|
|
102
|
+
out.push(...(await mdFiles(full)));
|
|
103
|
+
else if (entry.name.endsWith(".md"))
|
|
104
|
+
out.push(full);
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
/** hyphen/underscore-insensitive (the preprocessor runs hyphenToUnderscore). */
|
|
109
|
+
const canon = (s) => s.replace(/-/g, "_");
|
|
110
|
+
export async function auditDeliverableVariables(options) {
|
|
111
|
+
const known = new Set(Object.keys(options.attributes).map(canon));
|
|
112
|
+
const issues = [];
|
|
113
|
+
for (const dir of options.contentDirs) {
|
|
114
|
+
for (const file of await mdFiles(dir)) {
|
|
115
|
+
const source = await readFile(file, "utf8").catch(() => null);
|
|
116
|
+
if (source === null)
|
|
117
|
+
continue;
|
|
118
|
+
const acc = { refs: new Map(), defined: new Set() };
|
|
119
|
+
try {
|
|
120
|
+
const { tree } = dogsbayMdToTree(source, { directives: "preserve" });
|
|
121
|
+
collectBlocks(tree, acc);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
continue; // unparseable file — other rules own that problem
|
|
125
|
+
}
|
|
126
|
+
for (const [id, kinds] of acc.refs) {
|
|
127
|
+
if (known.has(canon(id)))
|
|
128
|
+
continue;
|
|
129
|
+
if ([...acc.defined].some((d) => canon(d) === canon(id)))
|
|
130
|
+
continue;
|
|
131
|
+
issues.push({
|
|
132
|
+
ruleId: "directives/undefined-variable",
|
|
133
|
+
severity: "warning",
|
|
134
|
+
file: relative(options.siteRoot, file),
|
|
135
|
+
message: `"${id}" (used in ${[...kinds].join(", ")}) is not defined by ` +
|
|
136
|
+
`deliverable "${options.deliverable}" — the terminal build treats ` +
|
|
137
|
+
`undefined conditions as falsy, so guarded content is silently dropped.`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return issues;
|
|
143
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ParsedHtmlFile } from "../types.js";
|
|
2
|
+
export interface CrawlOptions {
|
|
3
|
+
/**
|
|
4
|
+
* File extension to include. Default: `["html", "htm"]`.
|
|
5
|
+
* Excluded files are silently skipped — Pagefind's prebuilt
|
|
6
|
+
* JS bundles, sitemap.xml, robots.txt, OG images, etc. don't
|
|
7
|
+
* need to be parsed for HTML rules.
|
|
8
|
+
*/
|
|
9
|
+
extensions?: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Sub-paths to skip (relative to the dist root). Default
|
|
12
|
+
* skips Pagefind's runtime + the `_astro/` build-asset bucket
|
|
13
|
+
* Astro emits — they're not pages, parsing them is wasted
|
|
14
|
+
* work and produces noisy issues.
|
|
15
|
+
*/
|
|
16
|
+
excludeDirs?: string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Walk `distRoot` recursively, parse every HTML file with
|
|
20
|
+
* cheerio, return the list. The list is sorted by path for
|
|
21
|
+
* deterministic test output.
|
|
22
|
+
*/
|
|
23
|
+
export declare function crawlDist(distRoot: string, options?: CrawlOptions): ParsedHtmlFile[];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return the set of absolute asset paths (leading slash) that
|
|
3
|
+
* `copyAssets` would emit for the given content directories.
|
|
4
|
+
*
|
|
5
|
+
* @param contentDirs Absolute paths, one per source.
|
|
6
|
+
*/
|
|
7
|
+
export declare function collectAssets(contentDirs: readonly string[]): Set<string>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Walk one or more content directories collecting absolute-path
|
|
3
|
+
* asset references that the build's `copyAssets` would emit to
|
|
4
|
+
* `public/`. The audit rule `structure/asset-refs` cross-checks
|
|
5
|
+
* `<img src>` / `<a href>` references against this set so a
|
|
6
|
+
* misnamed file shows up as a structured Issue.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors the extension set used by `copyAssets` in
|
|
9
|
+
* `@dogsbay/format-astro/src/project.ts`. Keep in sync — drift
|
|
10
|
+
* would either flag valid refs as broken or miss real misses.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
13
|
+
import { join, relative } from "node:path";
|
|
14
|
+
// Same sets as format-astro's copyAssets. Keep in sync.
|
|
15
|
+
const OPTIMIZABLE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
|
|
16
|
+
const PASSTHROUGH_EXTS = new Set([".svg", ".ico", ".pdf"]);
|
|
17
|
+
/**
|
|
18
|
+
* Return the set of absolute asset paths (leading slash) that
|
|
19
|
+
* `copyAssets` would emit for the given content directories.
|
|
20
|
+
*
|
|
21
|
+
* @param contentDirs Absolute paths, one per source.
|
|
22
|
+
*/
|
|
23
|
+
export function collectAssets(contentDirs) {
|
|
24
|
+
const out = new Set();
|
|
25
|
+
for (const dir of contentDirs) {
|
|
26
|
+
if (!existsSync(dir))
|
|
27
|
+
continue;
|
|
28
|
+
walk(dir, dir, out);
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
function walk(rootDir, dir, out) {
|
|
33
|
+
let entries;
|
|
34
|
+
try {
|
|
35
|
+
entries = readdirSync(dir);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const full = join(dir, entry);
|
|
42
|
+
let isDir = false;
|
|
43
|
+
try {
|
|
44
|
+
isDir = statSync(full).isDirectory();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (isDir) {
|
|
50
|
+
walk(rootDir, full, out);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const dot = entry.lastIndexOf(".");
|
|
54
|
+
if (dot < 0)
|
|
55
|
+
continue;
|
|
56
|
+
const ext = entry.slice(dot).toLowerCase();
|
|
57
|
+
if (!OPTIMIZABLE_EXTS.has(ext) && !PASSTHROUGH_EXTS.has(ext))
|
|
58
|
+
continue;
|
|
59
|
+
const rel = relative(rootDir, full);
|
|
60
|
+
// Public URL is "/<rel>" with forward slashes — copyAssets
|
|
61
|
+
// copies rel-to-source verbatim into public/.
|
|
62
|
+
out.add("/" + rel.replace(/\\/g, "/"));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure validator for internal page links + asset references in
|
|
3
|
+
* imported content. Walks every `ExportPage`'s tree, collects
|
|
4
|
+
* every absolute `<a href>` and `<img src>`, set-checks against
|
|
5
|
+
* the page slug set + the asset file set.
|
|
6
|
+
*
|
|
7
|
+
* Single source of truth shared between two surfaces:
|
|
8
|
+
* - `structure/internal-links` + `structure/asset-refs` audit
|
|
9
|
+
* rules (warning-or-error issues from `dogsbay site check`)
|
|
10
|
+
* - `dogsbay site build --strict` (throws on first miss)
|
|
11
|
+
*
|
|
12
|
+
* See plans/audit-internal-refs.md.
|
|
13
|
+
*/
|
|
14
|
+
import type { ExportPage } from "@dogsbay/types";
|
|
15
|
+
/** One miss found by the validator. */
|
|
16
|
+
export interface RefIssue {
|
|
17
|
+
/** Slug of the page that contains the broken reference. */
|
|
18
|
+
pageSlug: string;
|
|
19
|
+
/** Raw href / src as written by the author. */
|
|
20
|
+
href: string;
|
|
21
|
+
/** Whether the validator was looking for a page or an asset. */
|
|
22
|
+
resolvedAgainst: "page" | "asset";
|
|
23
|
+
}
|
|
24
|
+
export interface ValidateRefsOptions {
|
|
25
|
+
/** Site basePath (e.g. "/docs", "/" for host-root, "" for root). */
|
|
26
|
+
basePath: string;
|
|
27
|
+
/**
|
|
28
|
+
* Absolute paths under the public asset tree, leading slash,
|
|
29
|
+
* matching how authors write them. e.g. `"/_assets/foo.png"`.
|
|
30
|
+
*/
|
|
31
|
+
knownAssets: Set<string>;
|
|
32
|
+
}
|
|
33
|
+
export interface ValidateRefsResult {
|
|
34
|
+
linkMisses: RefIssue[];
|
|
35
|
+
assetMisses: RefIssue[];
|
|
36
|
+
}
|
|
37
|
+
export declare function validateRefs(pages: ExportPage[], options: ValidateRefsOptions): ValidateRefsResult;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const EXTERNAL_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
2
|
+
const ANCHOR_ONLY_RE = /^#/;
|
|
3
|
+
const DATA_URI_RE = /^data:/i;
|
|
4
|
+
const ASSET_EXT_RE = /\.(png|jpe?g|gif|webp|svg|avif|pdf|zip|csv|tsv|json|yml|yaml|toml|mp4|webm|mp3|ogg|woff2?|ttf|otf)$/i;
|
|
5
|
+
/**
|
|
6
|
+
* Heuristic for "looks like an asset reference," used to decide
|
|
7
|
+
* whether an `<a href>` should be validated against the asset
|
|
8
|
+
* set or the page set. Image src is always asset; href can be
|
|
9
|
+
* either. Anything ending in a known asset extension is treated
|
|
10
|
+
* as asset.
|
|
11
|
+
*/
|
|
12
|
+
function looksLikeAsset(href) {
|
|
13
|
+
// Strip query + fragment before checking extension
|
|
14
|
+
const noFragment = href.split(/[?#]/)[0];
|
|
15
|
+
return ASSET_EXT_RE.test(noFragment);
|
|
16
|
+
}
|
|
17
|
+
export function validateRefs(pages, options) {
|
|
18
|
+
const basePath = options.basePath.replace(/\/$/, "");
|
|
19
|
+
const knownAssets = options.knownAssets;
|
|
20
|
+
// Page slug set — multiple match forms so the validator
|
|
21
|
+
// accepts the common ways authors write the same slug.
|
|
22
|
+
// For slug "tutorials/quickstart" any of these match:
|
|
23
|
+
// /tutorials/quickstart
|
|
24
|
+
// /tutorials/quickstart/
|
|
25
|
+
// /tutorials/quickstart.md
|
|
26
|
+
// /<basePath>/tutorials/quickstart (and the above three)
|
|
27
|
+
const pageSlugs = new Set();
|
|
28
|
+
for (const p of pages) {
|
|
29
|
+
pageSlugs.add(p.slug);
|
|
30
|
+
// Index pages can be referenced as their parent dir
|
|
31
|
+
if (p.slug === "index")
|
|
32
|
+
pageSlugs.add("");
|
|
33
|
+
if (p.slug.endsWith("/index")) {
|
|
34
|
+
pageSlugs.add(p.slug.slice(0, -"/index".length));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const result = { linkMisses: [], assetMisses: [] };
|
|
38
|
+
for (const page of pages) {
|
|
39
|
+
walkTree(page.tree, (href, kind) => {
|
|
40
|
+
check(href, kind, page.slug, basePath, pageSlugs, knownAssets, result);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
/** Internal walk — invokes `visit` for every href/src in the tree. */
|
|
46
|
+
function walkTree(nodes, visit) {
|
|
47
|
+
for (const node of nodes) {
|
|
48
|
+
if (node.inline)
|
|
49
|
+
walkInline(node.inline, visit);
|
|
50
|
+
if (node.html)
|
|
51
|
+
walkHtml(node.html, visit);
|
|
52
|
+
if (node.props?.href && typeof node.props.href === "string") {
|
|
53
|
+
visit(node.props.href, "link");
|
|
54
|
+
}
|
|
55
|
+
if (node.props?.src && typeof node.props.src === "string") {
|
|
56
|
+
visit(node.props.src, "image");
|
|
57
|
+
}
|
|
58
|
+
if (node.children)
|
|
59
|
+
walkTree(node.children, visit);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function walkInline(nodes, visit) {
|
|
63
|
+
for (const node of nodes) {
|
|
64
|
+
if (node.type === "link" && typeof node.href === "string") {
|
|
65
|
+
visit(node.href, "link");
|
|
66
|
+
if (node.children)
|
|
67
|
+
walkInline(node.children, visit);
|
|
68
|
+
}
|
|
69
|
+
else if (node.type === "image" && typeof node.src === "string") {
|
|
70
|
+
visit(node.src, "image");
|
|
71
|
+
}
|
|
72
|
+
else if (node.type === "highlight" && node.children) {
|
|
73
|
+
walkInline(node.children, visit);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function walkHtml(html, visit) {
|
|
78
|
+
// Pre-rendered HTML in `node.html` (Starlight importer output,
|
|
79
|
+
// MkDocs raw HTML, etc.) — scan with regexes. Cheap; the audit
|
|
80
|
+
// doesn't need a full HTML parser for this.
|
|
81
|
+
const aRe = /<a\b[^>]*\shref="([^"]+)"/gi;
|
|
82
|
+
let m;
|
|
83
|
+
while ((m = aRe.exec(html)) !== null)
|
|
84
|
+
visit(m[1], "link");
|
|
85
|
+
const imgRe = /<img\b[^>]*\ssrc="([^"]+)"/gi;
|
|
86
|
+
while ((m = imgRe.exec(html)) !== null)
|
|
87
|
+
visit(m[1], "image");
|
|
88
|
+
}
|
|
89
|
+
function check(href, kind, pageSlug, basePath, pageSlugs, knownAssets, result) {
|
|
90
|
+
if (!href || EXTERNAL_RE.test(href))
|
|
91
|
+
return;
|
|
92
|
+
if (ANCHOR_ONLY_RE.test(href))
|
|
93
|
+
return;
|
|
94
|
+
if (DATA_URI_RE.test(href))
|
|
95
|
+
return;
|
|
96
|
+
if (!href.startsWith("/"))
|
|
97
|
+
return; // relative — out of scope for v1
|
|
98
|
+
// Strip query and fragment for the comparison.
|
|
99
|
+
const cleanHref = href.split(/[?#]/)[0];
|
|
100
|
+
// Strip basePath if the author prefixed it. We compare against
|
|
101
|
+
// bare slugs, so `/docs/intro` and `/intro` should both match
|
|
102
|
+
// page slug "intro" (basePath="/docs").
|
|
103
|
+
let path = cleanHref;
|
|
104
|
+
if (basePath && (path === basePath || path.startsWith(`${basePath}/`))) {
|
|
105
|
+
path = path.slice(basePath.length);
|
|
106
|
+
}
|
|
107
|
+
// Strip leading / and trailing /, plus the `.md` extension.
|
|
108
|
+
let trimmed = path.replace(/^\//, "").replace(/\/$/, "");
|
|
109
|
+
trimmed = trimmed.replace(/\.md$/i, "");
|
|
110
|
+
// Image src — always asset.
|
|
111
|
+
// Anchor href — asset if it has an asset extension, else page.
|
|
112
|
+
const wantsAsset = kind === "image" || looksLikeAsset(cleanHref);
|
|
113
|
+
if (wantsAsset) {
|
|
114
|
+
// Asset paths are absolute under the public root.
|
|
115
|
+
// knownAssets stores entries like "/_assets/foo.png".
|
|
116
|
+
const lookup = cleanHref.startsWith("/") && basePath && cleanHref.startsWith(`${basePath}/`)
|
|
117
|
+
? cleanHref.slice(basePath.length)
|
|
118
|
+
: cleanHref;
|
|
119
|
+
if (!knownAssets.has(lookup)) {
|
|
120
|
+
result.assetMisses.push({
|
|
121
|
+
pageSlug,
|
|
122
|
+
href,
|
|
123
|
+
resolvedAgainst: "asset",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Page lookup
|
|
129
|
+
if (!pageSlugs.has(trimmed)) {
|
|
130
|
+
result.linkMisses.push({ pageSlug, href, resolvedAgainst: "page" });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine-readable formatter — emits the full report as JSON.
|
|
3
|
+
* Stable shape for CI consumers; mirrors the `AuditReport` type
|
|
4
|
+
* exactly with no transformations.
|
|
5
|
+
*/
|
|
6
|
+
import type { AuditReport } from "../types.js";
|
|
7
|
+
export declare function formatJson(report: AuditReport): string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AuditReport } from "../types.js";
|
|
2
|
+
export interface FormatTextOptions {
|
|
3
|
+
/** Force colour off (e.g. for tests). Defaults to TTY detection. */
|
|
4
|
+
color?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function formatText(report: AuditReport, options?: FormatTextOptions): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule registry — the global list of audit rules + helpers for
|
|
3
|
+
* filtering by category, id pattern, or skip list.
|
|
4
|
+
*
|
|
5
|
+
* Categories register their rules at module-load time via
|
|
6
|
+
* `registerRule` (typically inside a category's `index.ts`,
|
|
7
|
+
* imported by the audit runner). The registry is keyed by
|
|
8
|
+
* `Rule.id` so duplicate registrations are caught early.
|
|
9
|
+
*
|
|
10
|
+
* Filtering supports category aliases (`seo`, `links`, …) and
|
|
11
|
+
* glob-style ids (`seo/<name>`, `<cat>/description-<name>`) so
|
|
12
|
+
* the CLI surface can be expressive without each rule needing
|
|
13
|
+
* its own flag. Glob is wildcard-only; see `compileGlob` for
|
|
14
|
+
* the supported pattern grammar.
|
|
15
|
+
*/
|
|
16
|
+
import type { Rule, Stage } from "./types.js";
|
|
17
|
+
/**
|
|
18
|
+
* Register a rule. Throws on duplicate id — rule files import
|
|
19
|
+
* each other only via the index, and double-registration
|
|
20
|
+
* indicates a copy-paste bug.
|
|
21
|
+
*/
|
|
22
|
+
export declare function registerRule(rule: Rule): void;
|
|
23
|
+
/** All registered rules, in registration order. */
|
|
24
|
+
export declare function listRules(): Rule[];
|
|
25
|
+
/** Lookup by exact id, or `undefined` if unknown. */
|
|
26
|
+
export declare function getRule(id: string): Rule | undefined;
|
|
27
|
+
/** Test-only: clear the registry between tests. Internal use. */
|
|
28
|
+
export declare function _clearRegistry(): void;
|
|
29
|
+
export interface FilterOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Subset of categories to include. When non-empty, rules whose
|
|
32
|
+
* `category` is not in this list are dropped. Empty / undefined
|
|
33
|
+
* means "all categories."
|
|
34
|
+
*/
|
|
35
|
+
categories?: string[];
|
|
36
|
+
/**
|
|
37
|
+
* Subset of stages to include. Same semantics as `categories`.
|
|
38
|
+
*/
|
|
39
|
+
stages?: Stage[];
|
|
40
|
+
/**
|
|
41
|
+
* Explicit rule list — id strings, optionally with `*` glob.
|
|
42
|
+
* When set, ONLY rules matching one of these patterns run.
|
|
43
|
+
* Takes precedence over `categories` / `stages`.
|
|
44
|
+
*/
|
|
45
|
+
rules?: string[];
|
|
46
|
+
/** Patterns of rule ids to skip. Applied AFTER inclusion filters. */
|
|
47
|
+
skip?: string[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Apply category / stage / explicit / skip filters. Glob patterns
|
|
51
|
+
* use `*` to match any run of characters within a single segment
|
|
52
|
+
* (no recursive descent — pattern grammar is intentionally
|
|
53
|
+
* simple).
|
|
54
|
+
*/
|
|
55
|
+
export declare function filterRules(options?: FilterOptions, rules?: Rule[]): Rule[];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: `seo/description-length`
|
|
3
|
+
*
|
|
4
|
+
* Flag pages whose `frontmatter.description` exceeds 160
|
|
5
|
+
* characters. Google truncates SERP snippets at ~155-160 chars
|
|
6
|
+
* (the "safe zone" called out in the 100daysofdocs SEO audit);
|
|
7
|
+
* over-length descriptions get cut mid-sentence, leaving readers
|
|
8
|
+
* with a worse first impression than a shorter, polished version.
|
|
9
|
+
*
|
|
10
|
+
* Severity: warning. The page still functions; it just loses
|
|
11
|
+
* some snippet quality. Writers fix it by shortening or by
|
|
12
|
+
* setting `description: ""` to suppress.
|
|
13
|
+
*/
|
|
14
|
+
import type { Rule } from "../../types.js";
|
|
15
|
+
export declare const DEFAULT_MAX_DESCRIPTION_LENGTH = 160;
|
|
16
|
+
export declare const descriptionLength: Rule;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: `seo/description-missing`
|
|
3
|
+
*
|
|
4
|
+
* Flag pages without a frontmatter description. Without one,
|
|
5
|
+
* Google falls back to whatever it can extract from body prose,
|
|
6
|
+
* which is often misleading (a navigation chip, a callout title,
|
|
7
|
+
* the first line of a code block). Authoring a description per
|
|
8
|
+
* page is the cheapest fix.
|
|
9
|
+
*
|
|
10
|
+
* Severity: warning. Doesn't break anything — degrades discovery.
|
|
11
|
+
*
|
|
12
|
+
* Skipped for redirect pages: they don't render normal content.
|
|
13
|
+
*/
|
|
14
|
+
import type { Rule } from "../../types.js";
|
|
15
|
+
export declare const descriptionMissing: Rule;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: `seo/dist-meta`
|
|
3
|
+
*
|
|
4
|
+
* Walk the rendered HTML and assert the basic SEO meta surface
|
|
5
|
+
* is populated:
|
|
6
|
+
* - `<title>` present and non-empty (error)
|
|
7
|
+
* - `<meta name="description">` present and non-empty (warning)
|
|
8
|
+
* - `<meta property="og:title">` and `og:description` present (warning)
|
|
9
|
+
* - `<meta property="og:type">` set (warning)
|
|
10
|
+
*
|
|
11
|
+
* Twitter Card meta is NOT enforced — Twitter has been steadily
|
|
12
|
+
* deprecating its preview card spec, and og:* covers most cases
|
|
13
|
+
* since Twitter falls back to OG when its own tags are missing.
|
|
14
|
+
*
|
|
15
|
+
* Skipped for redirect pages (we detect them by checking for a
|
|
16
|
+
* <meta http-equiv="refresh"> or a `<link rel="canonical">` to
|
|
17
|
+
* a different URL — but for now, we just exclude `index.html`
|
|
18
|
+
* pages whose body is suspiciously empty). Implementation-wise,
|
|
19
|
+
* a future `seo/redirect-detection` helper could centralize this.
|
|
20
|
+
*/
|
|
21
|
+
import type { Rule } from "../../types.js";
|
|
22
|
+
export declare const distMeta: Rule;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotent — calling `registerSeoRules` more than once is a
|
|
3
|
+
* no-op. Lets callers (CLI, tests) register without coordinating.
|
|
4
|
+
*/
|
|
5
|
+
export declare function registerSeoRules(): void;
|
|
6
|
+
/**
|
|
7
|
+
* Test-only: reset the "registered" flag so unit tests can
|
|
8
|
+
* register fresh after `_clearRegistry()`. Production code never
|
|
9
|
+
* calls this.
|
|
10
|
+
*/
|
|
11
|
+
export declare function _resetSeoRegistration(): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: `seo/json-ld-parse`
|
|
3
|
+
*
|
|
4
|
+
* Every `<script type="application/ld+json">` block on the page
|
|
5
|
+
* must parse as valid JSON. Malformed JSON-LD is invisible to
|
|
6
|
+
* Google Search and accidentally introduces invalid output that
|
|
7
|
+
* the Rich Results validator rejects.
|
|
8
|
+
*
|
|
9
|
+
* Severity: error. Broken JSON-LD = wasted machine-readable
|
|
10
|
+
* markup; this is a real bug worth blocking a build on.
|
|
11
|
+
*/
|
|
12
|
+
import type { Rule } from "../../types.js";
|
|
13
|
+
export declare const jsonLdParse: Rule;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: `seo/json-ld-required-fields`
|
|
3
|
+
*
|
|
4
|
+
* For each well-formed JSON-LD block on the page, validate that
|
|
5
|
+
* the declared `@type` carries the fields Google expects for
|
|
6
|
+
* Rich Results eligibility. Catches the common case where a
|
|
7
|
+
* writer extends `customJsonLd` with a Course block but omits
|
|
8
|
+
* `provider`, or where DocsLayout's auto-emit drops a required
|
|
9
|
+
* field on a particular page.
|
|
10
|
+
*
|
|
11
|
+
* We don't bundle the full Schema.org vocabulary — just the
|
|
12
|
+
* @type values DocsLayout emits (Article, HowTo, TechArticle,
|
|
13
|
+
* Course) plus a few common extension types (Person,
|
|
14
|
+
* Organization, BreadcrumbList).
|
|
15
|
+
*
|
|
16
|
+
* Companion to `seo/json-ld-parse`: parse fires on malformed
|
|
17
|
+
* JSON; this fires on well-formed JSON that's missing
|
|
18
|
+
* Rich-Results-required fields.
|
|
19
|
+
*/
|
|
20
|
+
import type { Rule } from "../../types.js";
|
|
21
|
+
export declare const jsonLdRequiredFields: Rule;
|