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
23 changes: 23 additions & 0 deletions .changeset/docs-project-runtime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"leadtype": minor
---

Add `createDocsProject()` — the resolved project config as a runtime source.

`createDocsSource()` describes one content directory, so an app restates what its config already says: the content root, navigation, mounts, the frontmatter schema, and for a multi-collection project all of that per collection plus route prefixes and source-owned inheritance. Two descriptions of one project drift, and when they do the rendered site and the generated agent artifacts disagree about what exists.

A project reads the same resolved config the artifact pipeline reads, and returns a superset of `DocsSource`, so every first-party adapter accepts it unchanged:

```ts
const source = await createDocsProject({
config: docsConfig,
configPath: "docs/docs.config.ts",
baseUrl: "https://example.com",
});
```

Multi-collection projects get one merged, route-aware page API — `listPages()` tags each page with its collection, `loadPage()` accepts a collection-local slug or the full route — plus `project.collections`, `project.sources`, and `project.getSource(key)` for custom integrations. Source-owned config inheritance now runs through one shared implementation, so human rendering and generated artifacts cannot resolve it differently.

Remote collections are cache-only: a missing, unverifiable, or wrong-revision cache fails with a diagnostic naming `leadtype sync` rather than cloning inside a request. Route collisions name both collections.

`createDocsSource()` stays fully supported and is what the project is built on. `leadtype init` now scaffolds the project primitive.
14 changes: 9 additions & 5 deletions apps/astro-example/src/lib/source.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import path from "node:path";
import { createDocsSource } from "leadtype";
import { createDocsProject } from "leadtype";
import docsConfig from "../../../../docs/docs.config";

const repoRoot = path.resolve(process.cwd(), "../..");

export const source = await createDocsSource({
contentDir: path.join(repoRoot, "docs"),
nav: docsConfig.navigation,
mounts: docsConfig.mounts,
// The project reads the same resolved config the artifact pipeline reads, so
// navigation, mounts, the frontmatter schema, and the OpenAPI overlay are
// stated once — in the config — rather than restated here and left to drift.
// `configPath` fixes both the content root and where the config's relative
// paths resolve, using the same rules the CLI uses.
export const source = await createDocsProject({
config: docsConfig,
configPath: path.join(repoRoot, "docs", "docs.config.ts"),
baseUrl: "http://localhost:4321",
typeTableBasePath: repoRoot,
});
27 changes: 14 additions & 13 deletions apps/fumadocs-example/lib/source.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
import { resolve } from "node:path";
import { loader } from "fumadocs-core/source";
import { createDocsProject } from "leadtype";
import { fumadocsSource } from "leadtype/fumadocs";
import docsConfig from "../../../docs/docs.config";

// process.cwd() is the app root when Next runs build/dev.
const repoRoot = resolve(process.cwd(), "..", "..");
const contentDir = resolve(repoRoot, "docs");

/**
* fumadocs source backed by leadtype/fumadocs. It reads the repo-root
* Leadtype docs, uses the same curated navigation as the other examples, and
* resolves `<include>` / `<ExtractedTypeTable>` relative to the repo root.
*
* Passing `openapi` stages generated API reference pages into a temp copy of
* the docs and appends their navigation — the authored docs are untouched.
* The project resolves the repo-root docs from the config: content root,
* curated navigation, mounts, and the OpenAPI overlay all come from
* `docs/docs.config.ts` rather than being restated here, so this app and the
* generated agent artifacts describe the same docs.
*/
const project = await createDocsProject({
config: docsConfig,
configPath: resolve(repoRoot, "docs", "docs.config.ts"),
typeTableBasePath: repoRoot,
});

// The adapter takes the project directly — a project satisfies `DocsSource`.
const fumadocsSourceResult = await fumadocsSource({
contentDir,
source: project,
includeMetaJson: false,
nav: docsConfig.navigation,
mounts: docsConfig.mounts,
openapi: docsConfig.openapi,
typeTableBasePath: repoRoot,
});

export const source = loader({
baseUrl: "/docs",
source: fumadocsSourceResult,
});

/** Underlying leadtype DocsSource — call loadPage/buildSearchIndex/resolveInclude on this. */
/** Underlying leadtype source — call loadPage/buildSearchIndex/resolveInclude on this. */
export const leadtypeSource = fumadocsSourceResult.leadtype;
19 changes: 19 additions & 0 deletions docs/integrations/integrate-with-fumadocs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,25 @@ export const source = loader({ baseUrl: "/docs", source: fumaSource });
export const leadtypeSource = fumaSource.leadtype;
```

If your docs are described by a Leadtype config, pass a project instead of a content directory. The adapter accepts any `DocsSource`, and a project is one — so the content root, navigation, mounts, and OpenAPI overlay come from the config rather than being restated here:

```ts title="lib/source.ts"
// @noErrors - `../docs/docs.config` is your project's own config file.
import { loader } from "fumadocs-core/source";
import { createDocsProject } from "leadtype";
import { fumadocsSource } from "leadtype/fumadocs";
import docsConfig from "../docs/docs.config";

const project = await createDocsProject({
config: docsConfig,
configPath: "docs/docs.config.ts",
});

const fumaSource = await fumadocsSource({ source: project });
export const source = loader({ baseUrl: "/docs", source: fumaSource });
export const leadtypeSource = fumaSource.leadtype;
```

```ts title="next.config.mjs"
import createMDX from "@next/mdx";
import { createMdxSourcePlugins } from "leadtype/mdx";
Expand Down
4 changes: 2 additions & 2 deletions docs/paths.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
},
{
"path": "/docs/integrations/integrate-with-fumadocs",
"hash": "d80047caddd99c81"
"hash": "23f6af7223091f5c"
},
{
"path": "/docs/package-docs/bundle",
Expand Down Expand Up @@ -107,7 +107,7 @@
},
{
"path": "/docs/pipeline/use-the-source-primitive",
"hash": "0749b497916da7c1"
"hash": "9d11498866b40b2d"
},
{
"path": "/docs/pipeline/validate-in-ci",
Expand Down
51 changes: 51 additions & 0 deletions docs/pipeline/use-the-source-primitive.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,57 @@ export const source = await createDocsSource({

Wire `createMdxSourcePlugins()` into your bundler's remark stack, then call `source.loadPage(slug)` from your framework's page renderer. The "Wire into your framework" section below has minimal setups for each host.

## Start from your config instead

`createDocsSource()` describes **one content directory**. That is the right primitive for a custom integration, but it means your app restates what your config already says — the content root, the navigation, the mounts, the frontmatter schema — and a multi-collection app restates it per collection, plus route prefixes and source-owned inheritance.

Two descriptions of one project drift. When they do, the rendered site and the generated agent artifacts disagree about what exists — which is the failure "one content graph" exists to prevent.

`createDocsProject()` reads the same resolved config the artifact pipeline reads:

```ts title="lib/source.ts"
// @noErrors - `../docs/docs.config` is your project's own config file.
import { createDocsProject } from "leadtype";
import docsConfig from "../docs/docs.config";

export const source = await createDocsProject({
config: docsConfig,
configPath: "docs/docs.config.ts",
baseUrl: "https://example.com",
});
```

It returns a **superset of `DocsSource`**, so every first-party adapter takes it unchanged — `createGenerateStaticParams({ source })`, `createLoadPageData({ source })`, and the rest keep working with no parallel code path.

`configPath` fixes both the content root and where the config's relative paths resolve, using the same rules the CLI uses: a `docs.config.*` sits inside the docs directory, a `leadtype.config.*` sits at the project root above it. Pass `configDir` and `contentDir` instead when you want to be explicit.

### What it adds

```ts
// @noErrors - illustrative; `project` comes from createDocsProject above.
project.collections; // resolved collections, in config order
project.sources; // the acquisition graph
project.getSource("changelog"); // one collection's DocsSource
const page = await project.loadPage("changelog/1-0");
page?.collection; // "changelog"
```

A multi-collection app gets one merged, route-aware page API: `listPages()` returns every collection's pages tagged with where they came from, and `loadPage()` accepts either a collection-local slug (`1-0`) or the full route (`changelog/1-0`), so one handler can serve everything or you can mount a route per collection.

### Remote collections are cache-only

A request handler must never clone a repository. A collection whose cache is missing, unverifiable, or holds a different revision than the config asks for fails with a diagnostic naming `leadtype sync` — it does not quietly fetch:

```text
createDocsProject: the cache for collection "docs" holds
https://github.com/acme/acme.git@v0.9, but the config asks for
https://github.com/acme/acme.git@main. Run `leadtype sync --refresh`.
```

### Keep using `createDocsSource()` when

You are wiring a single content directory that no Leadtype config describes, or you need to construct a source from paths computed at runtime. The lower-level primitive is fully supported and is what the project is built on.

## Install

```sh
Expand Down
Loading
Loading