dogsbay 0.2.0-beta.35 → 0.2.0-beta.36
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/lib/collect-assets.js +64 -0
- package/dist/audit/lib/validate-refs.js +132 -0
- package/dist/audit/rules/structure/asset-refs.js +41 -0
- package/dist/audit/rules/structure/index.js +4 -0
- package/dist/audit/rules/structure/internal-links.js +28 -0
- package/dist/audit/rules/structure/nav-target-exists.js +3 -1
- package/dist/audit/run.js +2 -0
- package/dist/commands/site-build.js +43 -1
- package/dist/commands/site-check.js +9 -0
- package/dist/import-content.js +6 -21
- package/dist/index.js +4 -0
- package/package.json +9 -9
|
@@ -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,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,41 @@
|
|
|
1
|
+
import { validateRefs } from "../../lib/validate-refs.js";
|
|
2
|
+
export const assetRefs = {
|
|
3
|
+
id: "structure/asset-refs",
|
|
4
|
+
category: "structure",
|
|
5
|
+
stage: "source-corpus",
|
|
6
|
+
severity: "error",
|
|
7
|
+
description: "Every absolute `<img src>` and asset-style `<a href>` resolves to a file under the content tree.",
|
|
8
|
+
run(rawCtx) {
|
|
9
|
+
const ctx = rawCtx;
|
|
10
|
+
if (!ctx.pages || ctx.pages.length === 0)
|
|
11
|
+
return [];
|
|
12
|
+
if (!ctx.knownAssets) {
|
|
13
|
+
// No asset set available — typically a unit-test ctx that
|
|
14
|
+
// didn't bother. No-op rather than false-positive.
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
const { assetMisses } = validateRefs(ctx.pages, {
|
|
18
|
+
basePath: ctx.basePath ?? "",
|
|
19
|
+
knownAssets: ctx.knownAssets,
|
|
20
|
+
});
|
|
21
|
+
return assetMisses.map((m) => {
|
|
22
|
+
// Where the user should drop the file to satisfy THIS ref —
|
|
23
|
+
// strip optional basePath, prepend `content` for the
|
|
24
|
+
// suggestion. (We can't know the exact content dir layout
|
|
25
|
+
// for multi-source sites without more plumbing; keep it
|
|
26
|
+
// intuitive.)
|
|
27
|
+
const cleanHref = m.href.split(/[?#]/)[0];
|
|
28
|
+
const expected = `content${cleanHref}`;
|
|
29
|
+
return {
|
|
30
|
+
ruleId: "structure/asset-refs",
|
|
31
|
+
severity: "error",
|
|
32
|
+
file: `${m.pageSlug}.md`,
|
|
33
|
+
message: `Asset reference ${m.href} doesn't match any file in the ` +
|
|
34
|
+
`content tree. To fix: correct the path, rename the ` +
|
|
35
|
+
`existing file to match, or drop the asset at ` +
|
|
36
|
+
`${expected} to satisfy this exact reference.`,
|
|
37
|
+
context: m.href,
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* - structure/duplicate-slugs (corpus; two sources produced the same URL)
|
|
12
12
|
*/
|
|
13
13
|
import { registerRule } from "../../registry.js";
|
|
14
|
+
import { assetRefs } from "./asset-refs.js";
|
|
15
|
+
import { internalLinks } from "./internal-links.js";
|
|
14
16
|
import { localeCoherence } from "./locale-coherence.js";
|
|
15
17
|
import { namespaceCoherence } from "./namespace-coherence.js";
|
|
16
18
|
import { navTargetExists } from "./nav-target-exists.js";
|
|
@@ -28,6 +30,8 @@ export function registerStructureRules() {
|
|
|
28
30
|
registerRule(versionCoherence);
|
|
29
31
|
registerRule(localeCoherence);
|
|
30
32
|
registerRule(navTargetExists);
|
|
33
|
+
registerRule(internalLinks);
|
|
34
|
+
registerRule(assetRefs);
|
|
31
35
|
}
|
|
32
36
|
/**
|
|
33
37
|
* Test-only: reset the "registered" flag so unit tests can
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { validateRefs } from "../../lib/validate-refs.js";
|
|
2
|
+
export const internalLinks = {
|
|
3
|
+
id: "structure/internal-links",
|
|
4
|
+
category: "structure",
|
|
5
|
+
stage: "source-corpus",
|
|
6
|
+
severity: "error",
|
|
7
|
+
description: "Every absolute internal `<a href>` resolves to a built page in the corpus.",
|
|
8
|
+
run(rawCtx) {
|
|
9
|
+
const ctx = rawCtx;
|
|
10
|
+
if (!ctx.pages || ctx.pages.length === 0)
|
|
11
|
+
return [];
|
|
12
|
+
const { linkMisses } = validateRefs(ctx.pages, {
|
|
13
|
+
basePath: ctx.basePath ?? "",
|
|
14
|
+
knownAssets: ctx.knownAssets ?? new Set(),
|
|
15
|
+
});
|
|
16
|
+
return linkMisses.map((m) => ({
|
|
17
|
+
ruleId: "structure/internal-links",
|
|
18
|
+
severity: "error",
|
|
19
|
+
file: `${m.pageSlug}.md`,
|
|
20
|
+
message: `Internal link to ${m.href} doesn't resolve to any built ` +
|
|
21
|
+
`page. To fix: correct the path, rename the target page ` +
|
|
22
|
+
`to match, or remove the link. (Common cause: page renamed ` +
|
|
23
|
+
`without updating the body text. ` +
|
|
24
|
+
`Use \`dogsbay site check\` to surface every miss.)`,
|
|
25
|
+
context: m.href,
|
|
26
|
+
}));
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -78,7 +78,9 @@ export const navTargetExists = {
|
|
|
78
78
|
severity: "error",
|
|
79
79
|
file: navPath,
|
|
80
80
|
message: `Nav references missing file: ${miss.file} ` +
|
|
81
|
-
`(resolved to ${miss.resolvedAbs}).
|
|
81
|
+
`(resolved to ${miss.resolvedAbs}). To fix: rename the ` +
|
|
82
|
+
`file to match, correct the path in the nav file, or ` +
|
|
83
|
+
`remove the entry.`,
|
|
82
84
|
context: miss.file,
|
|
83
85
|
});
|
|
84
86
|
}
|
package/dist/audit/run.js
CHANGED
|
@@ -30,6 +30,8 @@ export function runAudit(inputs) {
|
|
|
30
30
|
locales: inputs.locales,
|
|
31
31
|
auditSources: inputs.auditSources,
|
|
32
32
|
siteRoot: inputs.siteRoot,
|
|
33
|
+
basePath: inputs.basePath,
|
|
34
|
+
knownAssets: inputs.knownAssets,
|
|
33
35
|
};
|
|
34
36
|
for (const issue of rule.run(ctx)) {
|
|
35
37
|
issues.push(issue);
|
|
@@ -22,6 +22,8 @@ import { collectPassthroughEntries } from "../passthrough-astro.js";
|
|
|
22
22
|
import { configToAstroOptions, findConfig, loadConfig, resolveOutputDir, } from "../config/index.js";
|
|
23
23
|
import { filterSourcesForMode, importContent, primaryModeFiltersAnything, } from "../import-content.js";
|
|
24
24
|
import { resolveSource } from "../source-resolver.js";
|
|
25
|
+
import { collectAssets } from "../audit/lib/collect-assets.js";
|
|
26
|
+
import { validateRefs } from "../audit/lib/validate-refs.js";
|
|
25
27
|
import { loadPlugins, runExtendConfig, runOnContentImported, runTransformNav, runEmitFiles, collectClientModules, collectStyles, collectClientConfigs, collectComponentWrappers, } from "../plugins/index.js";
|
|
26
28
|
export async function siteBuild(cwd, options) {
|
|
27
29
|
const startDir = resolve(cwd || ".");
|
|
@@ -137,7 +139,15 @@ export async function siteBuild(cwd, options) {
|
|
|
137
139
|
console.log(` Output: ${outputDir}`);
|
|
138
140
|
}
|
|
139
141
|
// 7. Import content
|
|
140
|
-
|
|
142
|
+
//
|
|
143
|
+
// `--strict` promotes nav-target misses to fatal at build time
|
|
144
|
+
// (default: warn + label-only fallback, soft). Plumbed through
|
|
145
|
+
// importContent → plugin → buildNavFromDirectory → loadNavFile
|
|
146
|
+
// via the `missingNavTargets: "fail"` import option. See
|
|
147
|
+
// plans/site-build-strict.md.
|
|
148
|
+
const importResult = await importContent(resolvedSources, config, {
|
|
149
|
+
missingNavTargets: options.strict ? "fail" : undefined,
|
|
150
|
+
});
|
|
141
151
|
const { importer } = importResult;
|
|
142
152
|
// Filter status: draft pages from production builds. dogsbay site
|
|
143
153
|
// dev passes includeDrafts: true so drafts stay visible during
|
|
@@ -161,6 +171,38 @@ export async function siteBuild(cwd, options) {
|
|
|
161
171
|
: draftCount > 0
|
|
162
172
|
? ` (including ${draftCount} draft${draftCount === 1 ? "" : "s"})`
|
|
163
173
|
: ""));
|
|
174
|
+
// 7d. --strict ref validation. Phase 2 of plans/site-build-strict.md.
|
|
175
|
+
// Same logic as the structure/internal-links + structure/asset-refs
|
|
176
|
+
// audit rules — single source of truth in validateRefs. Under
|
|
177
|
+
// --strict, the first miss throws; without it, site check is the
|
|
178
|
+
// surface for these (build stays soft to preserve the dev loop).
|
|
179
|
+
if (options.strict === true) {
|
|
180
|
+
const knownAssets = collectAssets(resolvedSources);
|
|
181
|
+
const { linkMisses, assetMisses } = validateRefs(pages, {
|
|
182
|
+
basePath: config.site.basePath ?? "",
|
|
183
|
+
knownAssets,
|
|
184
|
+
});
|
|
185
|
+
if (linkMisses.length > 0) {
|
|
186
|
+
const m = linkMisses[0];
|
|
187
|
+
throw new Error(`Internal link to ${m.href} from ${m.pageSlug}.md doesn't ` +
|
|
188
|
+
`resolve to any built page. To fix: correct the path, rename ` +
|
|
189
|
+
`the target page to match, or remove the link.` +
|
|
190
|
+
(linkMisses.length > 1
|
|
191
|
+
? ` (${linkMisses.length - 1} more found — run \`dogsbay site check\` for the full list.)`
|
|
192
|
+
: ""));
|
|
193
|
+
}
|
|
194
|
+
if (assetMisses.length > 0) {
|
|
195
|
+
const m = assetMisses[0];
|
|
196
|
+
const cleanHref = m.href.split(/[?#]/)[0];
|
|
197
|
+
throw new Error(`Asset reference ${m.href} from ${m.pageSlug}.md doesn't ` +
|
|
198
|
+
`match any file in the content tree. To fix: correct the ` +
|
|
199
|
+
`path, rename the existing file to match, or drop the asset ` +
|
|
200
|
+
`at content${cleanHref} to satisfy this exact reference.` +
|
|
201
|
+
(assetMisses.length > 1
|
|
202
|
+
? ` (${assetMisses.length - 1} more found — run \`dogsbay site check\` for the full list.)`
|
|
203
|
+
: ""));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
164
206
|
// 7. Emit content / config-derived / agent-readiness tiers.
|
|
165
207
|
// sourceDir drives asset copying + relative extraCss resolution.
|
|
166
208
|
// PR 1 ships a single-source pipeline; the namespace PR will
|
|
@@ -23,6 +23,7 @@ import pc from "picocolors";
|
|
|
23
23
|
import { configToAstroOptions, findConfig, loadConfig, resolveOutputDir, } from "../config/index.js";
|
|
24
24
|
import { effectiveLocale, effectiveVersion, importContent, isLocaleAxisActive, isNamespaceAxisActive, isVersionAxisActive, } from "../import-content.js";
|
|
25
25
|
import { resolveSource } from "../source-resolver.js";
|
|
26
|
+
import { collectAssets } from "../audit/lib/collect-assets.js";
|
|
26
27
|
import { filterRules, registerRule } from "../audit/registry.js";
|
|
27
28
|
import { reportExitCode, runAudit } from "../audit/run.js";
|
|
28
29
|
import { loadPlugins, collectAuditRules, runOnContentImported, runTransformNav, } from "../plugins/index.js";
|
|
@@ -189,6 +190,12 @@ export async function siteCheck(cwd, options) {
|
|
|
189
190
|
const r = await resolveSource(src, siteRoot);
|
|
190
191
|
auditSources.push({ contentDir: r.path, navOverride: src.nav });
|
|
191
192
|
}
|
|
193
|
+
// Asset set for `structure/asset-refs` — walks every source's
|
|
194
|
+
// content tree for files matching `copyAssets`'s extension
|
|
195
|
+
// list. Keep `collectAssets` in sync with that helper or refs
|
|
196
|
+
// either false-positive (real assets flagged) or false-negative
|
|
197
|
+
// (real misses missed).
|
|
198
|
+
const knownAssets = collectAssets(auditSources.map((s) => s.contentDir));
|
|
192
199
|
const report = runAudit({
|
|
193
200
|
rules,
|
|
194
201
|
pages,
|
|
@@ -201,6 +208,8 @@ export async function siteCheck(cwd, options) {
|
|
|
201
208
|
locales,
|
|
202
209
|
auditSources,
|
|
203
210
|
siteRoot,
|
|
211
|
+
basePath: config.site.basePath,
|
|
212
|
+
knownAssets,
|
|
204
213
|
});
|
|
205
214
|
// ── Output ─────────────────────────────────────────────────────
|
|
206
215
|
if (!options.quiet) {
|
package/dist/import-content.js
CHANGED
|
@@ -180,25 +180,7 @@ export function primaryModeFiltersAnything(sources) {
|
|
|
180
180
|
primaryCount++;
|
|
181
181
|
return primaryCount > 0 && primaryCount < sources.length;
|
|
182
182
|
}
|
|
183
|
-
|
|
184
|
-
* Run the import pipeline for a Dogsbay site.
|
|
185
|
-
*
|
|
186
|
-
* Loops over `config.content.sources[]`, resolving each to a
|
|
187
|
-
* filesystem path, picking its importer, and calling its
|
|
188
|
-
* `import()`. Pages and nav are concatenated in declaration order.
|
|
189
|
-
*
|
|
190
|
-
* In PR 1 only the `path:` origin is supported and only single-
|
|
191
|
-
* element `sources[]` is exercised in practice; the loop shape sets
|
|
192
|
-
* up multi-source aggregation for the namespace / versioning /
|
|
193
|
-
* i18n PRs.
|
|
194
|
-
*
|
|
195
|
-
* @param resolvedSources - absolute paths to each source's content
|
|
196
|
-
* directory, in the same order as `config.content.sources[]`. The
|
|
197
|
-
* caller is responsible for resolving relative source paths
|
|
198
|
-
* against the config-file dir before invoking.
|
|
199
|
-
* @param config - resolved DogsbayConfig (defaults applied)
|
|
200
|
-
*/
|
|
201
|
-
export async function importContent(resolvedSources, config) {
|
|
183
|
+
export async function importContent(resolvedSources, config, options = {}) {
|
|
202
184
|
const sources = config.content.sources;
|
|
203
185
|
if (sources.length === 0) {
|
|
204
186
|
throw new Error("config.content.sources must contain at least one source");
|
|
@@ -228,7 +210,7 @@ export async function importContent(resolvedSources, config) {
|
|
|
228
210
|
}
|
|
229
211
|
if (!firstImporter)
|
|
230
212
|
firstImporter = importer;
|
|
231
|
-
const opts = buildImportOptions(config, sourceCfg);
|
|
213
|
+
const opts = buildImportOptions(config, sourceCfg, options);
|
|
232
214
|
const { pages, nav } = await importer.import(resolved, opts);
|
|
233
215
|
const anyAxisActive = axes.version || axes.namespace || axes.locale;
|
|
234
216
|
const prefixSegs = getPrefixSegments(sourceCfg, axes, defaults);
|
|
@@ -374,7 +356,7 @@ function resolveImporter(source, fromName) {
|
|
|
374
356
|
* top-level fields (`section`, `codeBlockTitle`) apply to all
|
|
375
357
|
* sources.
|
|
376
358
|
*/
|
|
377
|
-
function buildImportOptions(config, source) {
|
|
359
|
+
function buildImportOptions(config, source, extra = {}) {
|
|
378
360
|
const opts = {};
|
|
379
361
|
if (source.nav)
|
|
380
362
|
opts.nav = source.nav;
|
|
@@ -386,6 +368,9 @@ function buildImportOptions(config, source) {
|
|
|
386
368
|
if (config.content.diagrams !== undefined) {
|
|
387
369
|
opts.diagrams = config.content.diagrams;
|
|
388
370
|
}
|
|
371
|
+
if (extra.missingNavTargets !== undefined) {
|
|
372
|
+
opts.missingNavTargets = extra.missingNavTargets;
|
|
373
|
+
}
|
|
389
374
|
// MkDocs-specific
|
|
390
375
|
if (source.mkdocs?.partialsDir) {
|
|
391
376
|
opts.partialsDir = source.mkdocs.partialsDir;
|
package/dist/index.js
CHANGED
|
@@ -117,6 +117,10 @@ site
|
|
|
117
117
|
"Kept as a no-op for compatibility with older scripts.")
|
|
118
118
|
.option("--refresh", "Wipe the per-source git cache before resolving — forces re-clone of every git: source. " +
|
|
119
119
|
"Useful when a tracked branch's HEAD has moved.")
|
|
120
|
+
.option("--strict", "Fail the build on structural breakage — missing nav targets, " +
|
|
121
|
+
"malformed nav files. Recommended for CI. One flag, growing " +
|
|
122
|
+
"coverage; release notes call out additions. See " +
|
|
123
|
+
"plans/site-build-strict.md.")
|
|
120
124
|
.action((dir, options) => siteBuild(dir, options));
|
|
121
125
|
site
|
|
122
126
|
.command("dev")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dogsbay",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.36",
|
|
4
4
|
"description": "CLI for Dogsbay — scaffold, build, and serve documentation sites with markdown / MkDocs / Obsidian / OpenAPI sources",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"picocolors": "^1.1.0",
|
|
33
33
|
"prompts": "^2.4.2",
|
|
34
34
|
"yaml": "^2.8.3",
|
|
35
|
-
"@dogsbay/format-
|
|
36
|
-
"@dogsbay/format-
|
|
37
|
-
"@dogsbay/format-
|
|
38
|
-
"@dogsbay/format-mdx": "0.2.0-beta.
|
|
39
|
-
"@dogsbay/format-starlight": "0.2.0-beta.
|
|
40
|
-
"@dogsbay/format-dogsbay-md": "0.2.0-beta.
|
|
41
|
-
"@dogsbay/
|
|
42
|
-
"@dogsbay/
|
|
35
|
+
"@dogsbay/format-mkdocs": "0.2.0-beta.36",
|
|
36
|
+
"@dogsbay/format-obsidian": "0.2.0-beta.36",
|
|
37
|
+
"@dogsbay/format-astro": "0.2.0-beta.36",
|
|
38
|
+
"@dogsbay/format-mdx": "0.2.0-beta.36",
|
|
39
|
+
"@dogsbay/format-starlight": "0.2.0-beta.36",
|
|
40
|
+
"@dogsbay/format-dogsbay-md": "0.2.0-beta.36",
|
|
41
|
+
"@dogsbay/format-openapi": "0.2.0-beta.36",
|
|
42
|
+
"@dogsbay/types": "0.2.0-beta.36"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^22.0.0",
|