Skip to content
Open
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
11 changes: 11 additions & 0 deletions .changeset/docs-plugin-initial.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@everything-dev/docs-plugin": minor
---

Initial release of the docs plugin — docs-as-code over the repository's own markdown.
Velite compiles `docs/**` and `GLOSSARY.md` at build time through a rspack
`beforeCompile` hook (appended via every-plugin's `extraPlugins` build.config.ts
option); a read-only oRPC router (`listPublished`, `getBySlug`) serves the compiled
content, and a gate-free public UI (`/docs`, `/docs/<slug>`) renders it in the app
shell with client-side markdown rendering (react-markdown + shiki). No database, no
secrets.
9 changes: 9 additions & 0 deletions .changeset/every-plugin-extra-plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"every-plugin": minor
---

Plugin workspaces can append rspack plugins from `build.config.ts` via the new
`extraPlugins` option — instances are appended after the composed defaults
(manifest emission, build reporting, data-URI fix) and run in both build and
watch. This is the sanctioned escape hatch for build-time integrations such as
content pipelines.
10 changes: 8 additions & 2 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ node_modules
# Build artifacts
dist
**/dist
# velite content output (compiled fresh inside the container by the docs
# plugin's rspack hook — a local copy is stale context bytes)
**/.velite
.turbo
**/.turbo

Expand Down Expand Up @@ -78,8 +81,11 @@ packages/every-plugin/tests
!packages/every-plugin/tests/types.d.ts
packages/everything-dev/tests

# Docs
docs
# Docs — README/LLM/CONTRIBUTING are image hygiene only; the repository docs
# tree must stay in the build context: the docs plugin's velite config roots
# there (plugins/docs/velite.config.ts) and compiles content into the
# plugin's server bundle. The prod stage strips sources, so runtime images
# never carry the tree.
README.md
LLM.txt
CONTRIBUTING.md
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,9 @@ node_modules/.federation
# every-plugin synthesized build config
.every-plugin/

# velite content output (compiled by the docs plugin's rspack build)
plugins/docs/.velite/

# vendored Effect source (local clone restored by `bun run prepare`; see AGENTS.md)
repos/

Expand Down
4 changes: 4 additions & 0 deletions bos.app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,5 +121,9 @@ export default App({
secrets: ["AI_API_KEY"],
ui: { name: "ai-ui", path: "plugins/ai/ui" },
}),
docs: Plugin("docs").path("plugins/docs", {
name: "@everything-dev/docs-plugin",
ui: { name: "docs-ui", path: "plugins/docs/ui" },
}),
},
});
91 changes: 91 additions & 0 deletions docs/plugins/docs-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
title: Docs plugin
description: The repository's own documentation, productized — docs-as-code with build-time compilation and gate-free reads
tags:
- plugins
- docs
order: 20
---

# Docs plugin

The docs plugin treats this repository's markdown as the product. There is no content
database and no publishing pipeline: the markdown in `docs/` (plus `GLOSSARY.md`) is the
single source of truth, compiled at build time and served read-only.

## How content flows

1. Edit markdown in the repository (`docs/**` or `GLOSSARY.md`) — by hand or by an agent.
2. Merge to `main`. CI deploys; the deploy train rebuilds the plugin.
3. During the plugin's rspack build, a `beforeCompile` hook runs
[Velite](https://velite.js.org), which compiles every matching file into typed JSON.
4. The compiled JSON is bundled into the plugin, and the read API serves it.

Content changes are versioned, reviewed, and audited in git — nothing else stores them.

## Frontmatter

Every doc honors an optional frontmatter block:

| Field | Type | Default | Purpose |
| --- | --- | --- | --- |
| `title` | string | first `#` heading, then the filename | Human title |
| `description` | string | — | Card copy on the index |
| `tags` | string[] | `[]` | Filter facet |
| `order` | number | `100` | Sort weight (lower first) |

Slugs are path-derived: `docs/adr/0012-foo.md` → `adr/0012-foo`, `GLOSSARY.md` →
`glossary`.

## Reading docs

Public, gate-free routes under the `public` mount:

- `/docs` — the index: every compiled doc as a card (title, description, tag chips)
- `/docs/<slug>` — a rendered doc

The API supports query/tag filtering and cursor pagination (`listPublished`); the v1
index renders the full list and doesn't expose filter inputs yet.

Programmatically, the same content is available to agents and tools:

- REST: `GET /api/rpc/docs/listPublished?q=&tag=&limit=&cursor=` and
`GET /api/rpc/docs/getBySlug?slug=<slug>` (OpenAPI: `/api/spec.json`)
- MCP: the same operations appear as generated tools on the MCP surface

## Adding a doc

Create a markdown file anywhere under `docs/` (or edit an existing one). Frontmatter is
optional — a file with just a heading is a valid doc:

```md
---
title: My feature
description: What it does
tags:
- guides
order: 50
---

# My feature

...
```

Then merge to `main`; the doc goes live on the next deploy. To preview locally, run the
dev stack and open `/docs`.

## Adding the plugin to another runtime

Register it in the authored config beside the other plugins:

```ts
docs: Plugin("docs").path("plugins/docs", {
name: "@everything-dev/docs-plugin",
ui: { name: "docs-ui", path: "plugins/docs/ui" },
}),
```

The plugin declares no secrets and no database. Its velite config currently compiles the
root runtime's `docs/` tree (the pattern is authored in the plugin's `velite.config.ts`);
a child runtime can edit that file to point at its own content directory.
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,17 @@
"test:regression:browser:install": "pnpm exec playwright install --with-deps chromium",
"test:regression:browser:dev:ssr": "REGRESSION_MODE=dev:ssr pnpm exec playwright test -c tests/regression/browser/playwright.config.mjs --project=dev:ssr",
"test:regression:browser:dev:csr": "REGRESSION_MODE=dev:csr pnpm exec playwright test -c tests/regression/browser/playwright.config.mjs --project=dev:csr",
"test:regression:browser:start:ssr": "REGRESSION_MODE=start:ssr pnpm exec playwright test -c tests/regression/browser/playwright.config.mjs --project=start:ssr",
"test:regression:browser:start:csr": "REGRESSION_MODE=start:csr pnpm exec playwright test -c tests/regression/browser/playwright.config.mjs --project=start:csr",
"test:regression:browser:start:ssr": "node tests/regression/lib/regression-stack.mjs cleanup ssr && REGRESSION_MODE=start:ssr pnpm exec playwright test -c tests/regression/browser/playwright.config.mjs --project=start:ssr",
"test:regression:browser:start:csr": "node tests/regression/lib/regression-stack.mjs cleanup csr && REGRESSION_MODE=start:csr pnpm exec playwright test -c tests/regression/browser/playwright.config.mjs --project=start:csr",
"test:regression:browser:backcompat": "REGRESSION_MODE=backcompat pnpm exec playwright test -c tests/regression/browser/playwright.config.mjs --project=backcompat",
"test:regression:http:dev": "REGRESSION_MODE=dev:ssr sh -lc 'cd tests/regression/http && go test ./... -count=1 -v -timeout 10m'",
"test:regression:http:start": "REGRESSION_MODE=start:ssr sh -lc 'cd tests/regression/http && go test ./... -count=1 -v -timeout 10m'",
"test:regression:http:backcompat": "REGRESSION_MODE=backcompat sh -lc 'cd tests/regression/http && go test ./... -count=1 -v -timeout 10m'",
"test:regression:framework": "cd tests/regression/framework && go test ./... -count=1 -v -timeout 15m",
"regression:dev:ssr": "node tests/regression/lib/start-stack.mjs dev:ssr",
"regression:dev:csr": "node tests/regression/lib/start-stack.mjs dev:csr",
"regression:start:ssr": "node tests/regression/lib/start-container.mjs ssr",
"regression:start:csr": "node tests/regression/lib/start-container.mjs csr",
"regression:start:ssr": "node tests/regression/lib/regression-stack.mjs ensure ssr && node tests/regression/lib/regression-stack.mjs watch ssr",
"regression:start:csr": "node tests/regression/lib/regression-stack.mjs ensure csr && node tests/regression/lib/regression-stack.mjs watch csr",
"regression:backcompat": "node tests/regression/lib/start-stack.mjs backcompat",
"test:db:up": "docker compose up -d --wait postgres-api-test postgres-auth-test",
"test:db:reset": "docker compose rm -fsv postgres-api-test postgres-auth-test && docker compose up -d --wait postgres-api-test postgres-auth-test",
Expand Down
5 changes: 4 additions & 1 deletion packages/every-plugin/src/build/rspack/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ class EveryPluginComposedBuild implements RspackPluginInstance {
export interface PluginBaseConfigOptions {
drizzle?: boolean;
externals?: string[];
/** Rspack plugin instances appended after the composed defaults — the escape
* hatch for build-time integrations (content pipelines, codegen, ...). */
extraPlugins?: unknown[];
/** Concrete rspack fields to merge over the defaults (devtool, infrastructureLogging, ...). */
rspack?: Partial<PluginBaseConfig>;
}
Expand Down Expand Up @@ -80,7 +83,7 @@ export function createPluginBaseConfig(options: PluginBaseConfigOptions = {}): P
...options.rspack,
externals: options.externals ?? options.rspack?.externals ?? ["pg", "@electric-sql/pglite"],
devtool: options.rspack?.devtool ?? (shouldDeploy ? false : "source-map"),
plugins,
plugins: [...plugins, ...(options.extraPlugins ?? [])],
infrastructureLogging: options.rspack?.infrastructureLogging ?? { level: "error" },
stats: options.rspack?.stats ?? "errors-warnings",
};
Expand Down
42 changes: 42 additions & 0 deletions packages/every-plugin/tests/unit/plugin-base-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, expect, it } from "vitest";
import { createPluginBaseConfig } from "../../src/build/rspack/compose";

class StubPlugin {
name: string;
constructor(name: string) {
this.name = name;
}
}

const pluginNames = (config: { plugins: unknown[] }) =>
config.plugins.map((plugin) => (plugin as { name?: string }).name);

describe("createPluginBaseConfig", () => {
it("composes the defaults when no options are passed", () => {
const config = createPluginBaseConfig();
const names = pluginNames(config);
expect(names).toContain("EveryPluginComposedBuild");
});

it("appends extraPlugins after the composed defaults, order preserved", () => {
const first = new StubPlugin("first-extra");
const second = new StubPlugin("second-extra");
const config = createPluginBaseConfig({ extraPlugins: [first, second] });

const names = pluginNames(config);
expect(names.indexOf("EveryPluginComposedBuild")).toBeLessThan(names.indexOf("first-extra"));
expect(names).toEqual(
expect.arrayContaining(["EveryPluginComposedBuild", "first-extra", "second-extra"]),
);
expect(names.indexOf("first-extra")).toBeLessThan(names.indexOf("second-extra"));
expect(config.plugins.at(-1)).toBe(second);
expect(config.plugins.at(-2)).toBe(first);
});

it("keeps the composed plugin set identical when no extras are declared", () => {
const bare = createPluginBaseConfig();
const withOtherOptions = createPluginBaseConfig({ externals: ["pg"] });
expect(pluginNames(withOtherOptions)).toEqual(pluginNames(bare));
expect(withOtherOptions.externals).toEqual(["pg"]);
});
});
29 changes: 29 additions & 0 deletions plugins/docs/build.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Compiler, RspackPluginInstance } from "@rspack/core";
import type { PluginBaseConfigOptions } from "every-plugin/build/rspack";

/**
* Compiles the docs content collections through velite as part of the
* plugin's rspack build — no hand-run velite step in dev or build. Velite is
* started once per process (`watch` in watch mode) and its output under
* `.velite/` is imported by the plugin source.
*/
class VeliteContentPlugin implements RspackPluginInstance {
name = "VeliteContentPlugin";
private static started = false;

apply(compiler: Compiler) {
compiler.hooks.beforeCompile.tapPromise("VeliteContentPlugin", async () => {
if (VeliteContentPlugin.started) return;
VeliteContentPlugin.started = true;
const { build } = await import("velite");
await build({
watch: compiler.options.watch === true,
clean: compiler.options.watch !== true,
});
});
}
}

export default {
extraPlugins: [new VeliteContentPlugin()],
} satisfies PluginBaseConfigOptions;
72 changes: 72 additions & 0 deletions plugins/docs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"name": "@everything-dev/docs-plugin",
"version": "0.1.0-rc.0",
"description": "Docs plugin — docs-as-code over the repository's markdown, compiled at build time by velite",
"type": "module",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
},
"./contract": {
"types": "./src/contract.ts",
"default": "./src/contract.ts"
}
},
"peerDependencies": {
"typescript": "catalog:"
},
"dependencies": {
"@base-ui/react": "^1.8.0",
"@orpc/contract": "catalog:",
"@orpc/openapi": "catalog:",
"@orpc/server": "catalog:",
"@phosphor-icons/react": "^2.1.10",
"@tanstack/react-query": "catalog:",
"@tanstack/react-router": "catalog:",
"class-variance-authority": "^0.7.1",
"cn": "^0.4.0",
"effect": "catalog:",
"every-plugin": "workspace:^",
"everything-dev": "workspace:^",
"react": "catalog:",
"react-dom": "catalog:",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"shiki": "^3.20.0",
"zod": "catalog:"
},
"devDependencies": {
"@module-federation/enhanced": "catalog:",
"@module-federation/node": "catalog:",
"@module-federation/rsbuild-plugin": "catalog:",
"@module-federation/runtime-tools": "catalog:",
"@rsbuild/core": "catalog:",
"@rsbuild/plugin-react": "catalog:",
"@rspack/cli": "catalog:",
"@rspack/core": "catalog:",
"@tailwindcss/postcss": "^4.1.17",
"@tanstack/router-plugin": "catalog:",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"dotenv": "^17.2.3",
"everything-dev": "workspace:^",
"every-plugin": "workspace:^",
"shadcn": "^4.21.0",
"tailwindcss": "^4.1.17",
"tw-animate-css": "^1.4.0",
"typescript": "catalog:",
"velite": "^0.4.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "catalog:"
},
"scripts": {
"build": "every-plugin build",
"dev": "every-plugin dev",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "velite && tsc --noEmit",
"typecheck:ui": "tsc --noEmit -p ui/tsconfig.json"
}
}
13 changes: 13 additions & 0 deletions plugins/docs/plugin.dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import "dotenv/config";
import type { PluginConfigInput } from "every-plugin";
import packageJson from "./package.json" with { type: "json" };
import type Plugin from "./src/index";

export default {
pluginId: packageJson.name,
port: Number(process.env.PORT) || 3016,
config: {
variables: {},
secrets: {},
} satisfies PluginConfigInput<typeof Plugin>,
};
Loading
Loading