Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/paths.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
},
{
"path": "/docs/pipeline/collections",
"hash": "c0385584dc467a04"
"hash": "8114273c620aa826"
},
{
"path": "/docs/pipeline/configure-sources",
Expand Down Expand Up @@ -119,15 +119,15 @@
},
{
"path": "/docs/reference/cli",
"hash": "c61c7a9a9dbb76bd"
"hash": "945fe8c9ae5db187"
},
{
"path": "/docs/reference/convert",
"hash": "47365b8cfe098cc0"
},
{
"path": "/docs/reference/doctor",
"hash": "f1ca98d7b80d093e"
"hash": "53e1cdcfaabcffe4"
},
{
"path": "/docs/reference/frontmatter-transformers",
Expand Down
3 changes: 2 additions & 1 deletion docs/reference/doctor.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Everything it reports comes from the same config loader and resolvers `generate`

**Collections and routes** — that each content directory exists and is readable, that include/exclude globs actually match files, and each collection's resolved public route prefix.

**Navigation** — whether the tree was authored here, **inherited** from a source repository, derived from `groups`, or inferred from the content tree; a summary of the resolved sections; and pages absent from a curated tree that fall back to the root of the sidebar rather than being placed deliberately.
**Navigation** — whether the tree was authored here, **inherited** from a source repository, derived from `groups`, or inferred from the content tree (`mixed` when collections disagree — each collection also reports its own origin); a summary of the resolved sections; and pages absent from a curated tree that fall back to the root of the sidebar rather than being placed deliberately.

**Outputs and integrations** — the output root, which expected artifacts are present or missing, which are older than the newest source file or the config, the detected framework adapter, and which agent surfaces are enabled.

Expand Down Expand Up @@ -140,6 +140,7 @@ leadtype doctor --json
| `source.inherit-failed` | warn | A source-owned config could not be read, so inheritance was skipped. |
| `source.dir-missing` | error | A collection's `dir` does not exist. |
| `collection.no-matches` | warn | A collection's globs match no files. |
| `nav.unresolvable` | error | The navigation tree did not resolve — a pin or page entry matches nothing. |
| `nav.unknown-group` | error | A page declares a `group:` that no config declares. |
| `nav.unrepresented-page` | warn | A page is absent from the curated tree and falls back to the root. |
| `output.not-generated` | warn | No artifacts under the output root. |
Expand Down
34 changes: 34 additions & 0 deletions packages/leadtype/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2446,6 +2446,40 @@ This page is valid, but the output path is not a directory.
expect(llmsTxt).toContain("# Collections Product");
});

it("applies a single default collection's exclude when generating", async () => {
const srcDir = await createTempDir();
const outDir = await createTempDir();
const capture = createCapture();

await writeMdxPage(srcDir, "index.mdx", 'title: "Home"');
await writeMdxPage(srcDir, "drafts/wip.mdx", 'title: "WIP"');
await writeFile(
path.join(srcDir, "leadtype.config.ts"),
`export default {
product: { name: "Filtered Product", tagline: "Single collection." },
collections: {
docs: { dir: "docs", routePrefix: "/docs", exclude: ["drafts/**"] },
},
};`
);

const code = await runCli(
["generate", "--src", srcDir, "--out", outDir, "--format", "json"],
capture.io
);

// A single default `docs` collection is the one shape that used to skip
// staging and serve the directory in place — which applied no filter, so
// this exact config's `exclude` did nothing and the draft shipped. The
// collection's filters must stage a filtered mirror here like they do in
// every other shape.
expect(code).toBe(0);
expect(existsSync(path.join(outDir, "docs", "index.md"))).toBe(true);
expect(existsSync(path.join(outDir, "docs", "drafts", "wip.md"))).toBe(
false
);
});

it("inherits source-owned navigation, groups, and flatteners after sync", async () => {
const sourceRepo = await createGitDocsSource({
"docs/docs.config.ts": `import { defineComponentFlattener } from ${JSON.stringify(markdownEntry)};
Expand Down
149 changes: 149 additions & 0 deletions packages/leadtype/src/cli/doctor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,155 @@ describe("findings", () => {
).toBe("warn");
});

it("reports an unresolvable navigation as a finding, not a crash", async () => {
const dir = await fixture({
"docs/docs.config.ts": `export default {
product: { name: "Acme", tagline: "Acme docs." },
navigation: [
{ title: "Guides", base: "guides", pages: [{ include: "**", pin: ["renamed"] }] },
],
};`,
"docs/guides/setup.mdx": page("Setup"),
});

// CI gates on doctor: a pin typo has to come back as a report with a
// stable id, not a raw error under `--json` with no report at all.
// `runJson` parses stdout, so this also asserts the JSON stayed valid.
const { code, report } = await runJson(dir);

expect(code).toBe(1);
expect(report.ok).toBe(false);
const finding = report.issues.find(
(entry) => entry.id === "nav.unresolvable"
);
expect(finding?.level).toBe("error");
expect(finding?.message).toContain('Nav pin "renamed"');
expect(finding?.owner).toBe("navigation");
expect(finding?.fix).toContain("leadtype doctor");
});

it("reports curated navigation naming an excluded page as a finding", async () => {
const dir = await fixture({
"leadtype.config.ts": `export default {
product: { name: "Acme", tagline: "Acme docs." },
collections: {
docs: { dir: "docs", routePrefix: "/docs", exclude: ["drafts/**"], navigation: ["index", "drafts/wip"] },
},
};`,
"docs/index.mdx": page("Home"),
"docs/drafts/wip.mdx": page("WIP"),
});

// `generate` stages the filtered mirror before resolving, so this
// reference fails the build as missing. Resolving against the raw
// directory let it succeed here, and the admitted-page check merely
// dropped the page from counts — `ok: true` for a build that exits 1.
const { code, report } = await runJson(dir);

expect(code).toBe(1);
Comment thread
pullfrog[bot] marked this conversation as resolved.
expect(report.ok).toBe(false);
const finding = report.issues.find(
(entry) => entry.id === "nav.unresolvable"
);
expect(finding?.level).toBe("error");
expect(finding?.message).toContain('Nav page "drafts/wip"');
expect(finding?.owner).toBe("collections.docs.navigation");
expect(finding?.fix).toContain("include");
});

it("reports a non-default locale's unknown group when the default locale is clean", async () => {
const dir = await fixture({
"docs/docs.config.ts": `export default {
product: { name: "Acme", tagline: "Acme docs." },
i18n: { defaultLocale: "en", locales: ["en", "zh"] },
groups: [{ slug: "guides", title: "Guides" }],
};`,
"docs/index.mdx": `---\ntitle: "Home"\ngroup: guides\n---\n\nBody.\n`,
"docs/zh/index.mdx": `---\ntitle: "Home (zh)"\ngroup: mystery\n---\n\nBody.\n`,
});

// `generate` resolves the tree once per configured locale and exits 1 on
// '/docs/zh declares unknown group "mystery"'. Resolving only the
// default locale missed the translation's finding entirely — doctor said
// ok for a build that fails.
const { code, report } = await runJson(dir);

expect(code).toBe(1);
expect(report.ok).toBe(false);
const finding = report.issues.find(
(entry) => entry.id === "nav.unknown-group"
);
expect(finding?.level).toBe("error");
expect(finding?.message).toBe('/docs/zh declares unknown group "mystery"');
});

it("keeps excluded pages out of the routed page count", async () => {
const dir = await fixture({
"leadtype.config.ts": `export default {
product: { name: "Acme", tagline: "Acme docs." },
collections: {
docs: { dir: "docs", routePrefix: "/docs", exclude: ["drafts/**"], navigation: ["index"] },
},
};`,
"docs/index.mdx": page("Home"),
"docs/drafts/wip.mdx": page("WIP"),
});

const { report } = await runJson(dir);

// `generate` stages a filtered mirror before resolving navigation, so an
// excluded page never routes — counting it here disagreed with the
// collection's own page count on the same report.
expect(report.collections[0]?.pageCount).toBe(1);
expect(report.navigation?.routedPages).toBe(1);
});

it("lists each declared group once, however many collections read it", async () => {
const dir = await fixture({
"leadtype.config.ts": `export default {
product: { name: "Acme", tagline: "Acme docs." },
collections: {
docs: { dir: "content/docs", routePrefix: "/docs", groups: [{ slug: "ref", title: "Reference" }] },
guides: { dir: "content/guides", routePrefix: "/guides", groups: [{ slug: "howto", title: "How-to" }] },
},
};`,
"content/docs/api.mdx": `---\ntitle: "API"\ndescription: "API."\ngroup: ref\n---\n\nBody.\n`,
"content/guides/deploy.mdx": `---\ntitle: "Deploy"\ndescription: "Deploy."\ngroup: howto\n---\n\nBody.\n`,
});

const { report } = await runJson(dir);

// Groups merge across collections (membership is pure slug matching), and
// each collection's manifest emits every declared group — so without a
// dedupe two collections sharing two groups read `sections: Reference,
// How-to, Reference, How-to`.
expect(report.navigation?.groups).toEqual(["Reference", "How-to"]);
});

it("reports mixed navigation origins instead of collapsing them to explicit", async () => {
const dir = await fixture({
"leadtype.config.ts": `export default {
product: { name: "Acme", tagline: "Acme docs." },
collections: {
docs: { dir: "content/docs", routePrefix: "/docs", navigation: ["index"] },
changelog: { dir: "content/changelog", routePrefix: "/changelog" },
},
};`,
"content/docs/index.mdx": page("Home"),
"content/changelog/1-0.mdx": page("1.0"),
});

const { report } = await runJson(dir);

expect(report.navigation?.origin).toBe("mixed");
expect(
report.collections.map((entry) => [entry.key, entry.navigationOrigin])
).toEqual([
["docs", "explicit"],
["changelog", "inferred"],
]);
});

it("warns when a collection's include globs match nothing", async () => {
const dir = await fixture({
"leadtype.config.ts": `export default {
Expand Down
Loading
Loading