diff --git a/.async-pipeline/tasks.lock.json b/.async-pipeline/tasks.lock.json index 47619d2..3a4633a 100644 --- a/.async-pipeline/tasks.lock.json +++ b/.async-pipeline/tasks.lock.json @@ -54,8 +54,8 @@ "value": "async-pipeline run-task api-surface-generate" }, { - "name": "pipeline:task:docs", - "value": "async-pipeline run-task docs" + "name": "pipeline:task:docs.site", + "value": "async-pipeline run-task docs.site" }, { "name": "pipeline:task:sync-check", @@ -71,7 +71,7 @@ }, { "name": "pipeline:docs", - "value": "async-pipeline run-task docs" + "value": "async-pipeline run-task docs.site" }, { "name": "pipeline:github:check", @@ -158,8 +158,8 @@ "value": "async-pipeline run-task api-surface-generate" }, { - "name": "pipeline:task:docs", - "value": "async-pipeline run-task docs" + "name": "pipeline:task:docs.site", + "value": "async-pipeline run-task docs.site" }, { "name": "pipeline:task:sync-check", @@ -175,7 +175,7 @@ }, { "name": "pipeline:docs", - "value": "async-pipeline run-task docs" + "value": "async-pipeline run-task docs.site" }, { "name": "pipeline:github:check", @@ -222,6 +222,6 @@ "value": "async-pipeline run verify --force" } ], - "hash": "sha256:1c159852638b60608aa39ebb8f1a5841bde1ef8010cabfe7e8c81eac3635f7f3", - "generatedAt": "2026-06-14T11:16:50.730Z" + "hash": "sha256:5f179042f0de7a5e090c0f234980b53f348f793f702a72c0b7f9fe287f87236e", + "generatedAt": "2026-06-14T19:47:53.026Z" } diff --git a/.fallowrc.jsonc b/.fallowrc.jsonc index b5a0e12..ca1cd93 100644 --- a/.fallowrc.jsonc +++ b/.fallowrc.jsonc @@ -1,10 +1,15 @@ { "$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json", + "ignoreDependencies": [ + "@async/api-contract" + ], "ignorePatterns": [ - "examples/**" + "examples/**", + "scripts/build-pages.js" ], "entry": [ "db.config.example.mjs", + "pipeline.ts", "scripts/**/*.js", "scripts/**/*.ts", "src/**/*.test.ts", diff --git a/.github/async-pipeline.lock.json b/.github/async-pipeline.lock.json index 7d0a08e..77ac0b1 100644 --- a/.github/async-pipeline.lock.json +++ b/.github/async-pipeline.lock.json @@ -3,8 +3,8 @@ "generator": "@async/pipeline", "config": "pipeline.ts", "workflow": ".github/workflows/async-pipeline.yml", - "hash": "sha256:4aef2ae561d80b5f616aa169c3d277a90c7123166116ff2170d8524b411837d0", - "generatedAt": "2026-06-14T11:16:50.683Z", + "hash": "sha256:205c87f8bfd6e77a11e27cc598263a16e75f2f1abb08c33384fc06802b1928d6", + "generatedAt": "2026-06-14T19:47:52.985Z", "triggers": { "pull_request": {}, "push": { @@ -19,7 +19,7 @@ { "id": "pages", "target": [ - "docs" + "docs.site" ], "trigger": [ "pr", @@ -31,7 +31,7 @@ "pages": { "build": { "kind": "static", - "path": "./website/dist" + "path": ".async/pages" } } }, diff --git a/.github/workflows/async-pipeline.yml b/.github/workflows/async-pipeline.yml index b2c0b3e..6a38315 100644 --- a/.github/workflows/async-pipeline.yml +++ b/.github/workflows/async-pipeline.yml @@ -70,7 +70,7 @@ jobs: - name: Upload Pages artifact uses: actions/upload-pages-artifact@v4 with: - path: "./website/dist" + path: ".async/pages" pages-deploy: name: pages-deploy diff --git a/AGENTS.md b/AGENTS.md index 8b29a47..3ebff64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -187,6 +187,6 @@ and runs on Node.js 24: - `npm pack --dry-run` Docs site deployment is generated from the `pages` job in `pipeline.ts` and -publishes `website/dist/` to GitHub Pages on pushes to `main`. +publishes `.async/pages/` to GitHub Pages on pushes to `main`. Dependabot is configured in `.github/dependabot.yml` for GitHub Actions updates. diff --git a/package.json b/package.json index 39e957c..193b8b3 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "examples": "pnpm run build && pnpm run build:test && node ./.tmp/test-build/scripts/serve-examples.js", "pipeline:api-surface": "async-pipeline run-task api-surface", "pipeline:api-surface:generate": "async-pipeline run-task api-surface-generate", - "pipeline:docs": "async-pipeline run-task docs", + "pipeline:docs": "async-pipeline run-task docs.site", "pipeline:github:check": "async-pipeline github check", "pipeline:github:generate": "async-pipeline github generate", "pipeline:pages": "async-pipeline run pages", @@ -37,6 +37,7 @@ "pipeline:task:api-surface": "async-pipeline run-task api-surface", "pipeline:task:api-surface-generate": "async-pipeline run-task api-surface-generate", "pipeline:task:docs": "async-pipeline run-task docs", + "pipeline:task:docs.site": "async-pipeline run-task docs.site", "pipeline:task:sync-check": "async-pipeline run-task sync-check", "pipeline:verify": "async-pipeline run verify", "pipeline:verify:force": "async-pipeline run verify --force", diff --git a/pipeline.ts b/pipeline.ts index 7d67090..a976671 100644 --- a/pipeline.ts +++ b/pipeline.ts @@ -19,11 +19,11 @@ export default definePipeline({ runners: ["package"], targets: [{ package: "@async/db" }], jobs: ["pages", "preview", "publish", "publish-github", "release-doctor", "snapshot", "verify"], - tasks: ["api-surface", "api-surface-generate", "docs", "sync-check"], + tasks: ["api-surface", "api-surface-generate", "docs.site", "sync-check"], scripts: { "api-surface": "run-task api-surface", "api-surface:generate": "run-task api-surface-generate", - "docs": "run-task docs", + "docs": "run-task docs.site", "github:check": "github check", "sync:check": "sync check", "verify:force": "run verify --force", @@ -102,12 +102,12 @@ export default definePipeline({ cache: true, run: sh`pnpm run test` }), - docs: task({ - description: "Build the static docs site that GitHub Pages uploads from website/dist.", - inputs: ["docs/**/*.md", "website/**/*", "scripts/tasks/docs-build.js", "package.json", "pnpm-lock.yaml"], - outputs: ["website/dist/**"], + "docs.site": task({ + description: "Build the standardized GitHub Pages documentation site.", + inputs: ["README.md", "docs/**/*.md", "scripts/build-pages.js"], + outputs: [".async/pages/**"], cache: false, - run: sh`pnpm run docs:build` + run: sh`node scripts/build-pages.js` }), "release-doctor": task({ description: "Reconcile release state across npm, GitHub Packages, and GitHub Releases after package verification.", @@ -117,7 +117,7 @@ export default definePipeline({ }), pack: task({ description: "Verify the publishable package contents.", - dependsOn: ["check", "test", "docs", "api-surface", "sync-check"], + dependsOn: ["check", "test", "docs.site", "api-surface", "sync-check"], inputs: ["production"], cache: false, run: sh`npm pack --dry-run` @@ -167,11 +167,11 @@ export default definePipeline({ trigger: ["pr", "main", "release"] }), pages: job({ - target: "docs", + target: "docs.site", trigger: ["pr", "main", "manual"], github: { pages: { - build: { kind: "static", path: "./website/dist" } + build: { kind: "static", path: ".async/pages" } } } }), diff --git a/scripts/build-pages.js b/scripts/build-pages.js new file mode 100644 index 0000000..10f02fc --- /dev/null +++ b/scripts/build-pages.js @@ -0,0 +1,201 @@ +#!/usr/bin/env node +import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises"; +import { basename, dirname, join, relative } from "node:path"; + +const site = { + "title": "@async/db", + "repo": "db", + "stage": "Alpha", + "description": "Gradual data workflow from JSON fixtures to generated types, local APIs, writable stores, and real persistence.", + "lead": "Move from local prototype data to typed APIs and durable state in small steps.", + "quickstart": "pnpm add @async/db\npnpm run pipeline:verify", + "docsRoots": [ + "docs" + ] +}; +const outDir = ".async/pages"; +const asyncProjects = [ + ["@async/db", "https://async.github.io/db/", "Data workflow"], + ["@async/web", "https://async.github.io/web/", "Web runtime"], + ["@async/pipeline", "https://async.github.io/pipeline/", "Pipeline workflows"], + ["@async/dispatch", "https://async.github.io/dispatch/", "Goal-first coordination"], + ["@async/auto-git", "https://async.github.io/auto-git/", "Git handoffs"], + ["@async/api-contract", "https://async.github.io/api-contract/", "API ledgers"], + ["@async/claims", "https://async.github.io/claims/", "Doc claim checks"] +]; + +await rm(outDir, { recursive: true, force: true }); +await mkdir(outDir, { recursive: true }); + +const readme = await readFile("README.md", "utf8"); +const docs = await collectDocs(site.docsRoots); +for (const doc of docs) { + const markdown = await readFile(doc.path, "utf8"); + const htmlPath = join(outDir, doc.href); + await mkdir(dirname(htmlPath), { recursive: true }); + await writeFile(htmlPath, layout({ + title: doc.title, + body: renderMarkdown(markdown), + rootPrefix: "../".repeat(doc.href.split("/").length - 1) + })); +} + +await writeFile(join(outDir, "index.html"), layout({ + title: site.title, + body: home(readme, docs), + rootPrefix: "" +})); + +function home(readme, docs) { + const guideLinks = docs.length ? docs.map((doc) => + `${escapeHtml(doc.title)}${escapeHtml(doc.source)}` + ).join("\n") : "

No guide pages are published for this repo yet.

"; + const related = asyncProjects + .filter(([name]) => name !== site.title) + .map(([name, url, label]) => `${name}${label}`) + .join("\n"); + return ` +
+

${escapeHtml(site.stage)} / Async

+

${escapeHtml(site.title)}

+

${renderInline(site.description)}

+

${renderInline(site.lead)}

+
+ GitHub + npm +
+
+
+

Start

+
${escapeHtml(site.quickstart)}
+
+
+

Guides

+
${guideLinks}
+
+
+

Related Async Projects

+ +
+
+

README

+
${renderMarkdown(readme)}
+
+ `; +} + +function layout({ title, body, rootPrefix }) { + const nav = asyncProjects.map(([name, url]) => + `${name.replace("@async/", "")}` + ).join("\n"); + return ` + + + + + ${escapeHtml(title)} + + + + +
${escapeHtml(site.title)}
${body}
+ + +`; +} + +async function collectDocs(roots) { + const docs = []; + for (const root of roots) { + try { await stat(root); } catch { continue; } + await walk(root, docs); + } + return docs.sort((a, b) => a.title.localeCompare(b.title)); +} + +async function walk(dir, docs) { + for (const entry of await readdir(dir, { withFileTypes: true })) { + const path = join(dir, entry.name); + if (entry.isDirectory()) { + if (["node_modules", ".git", ".async", "dist", "_site"].includes(entry.name)) continue; + await walk(path, docs); + continue; + } + if (!entry.isFile() || !entry.name.endsWith(".md")) continue; + const source = await readFile(path, "utf8"); + const rel = relative(process.cwd(), path).replaceAll("\\", "/"); + const href = rel.replace(/\.md$/, ".html"); + docs.push({ path, source: rel, href, title: firstHeading(source) || basename(path, ".md") }); + } +} + +function renderMarkdown(source) { + const lines = source.split(/\r?\n/); + const html = []; + let list = ""; + let code = null; + let table = []; + const closeList = () => { if (list) { html.push(``); list = ""; } }; + const closeTable = () => { if (table.length) { html.push(renderTable(table)); table = []; } }; + for (const line of lines) { + if (line.startsWith("```")) { + closeList(); closeTable(); + if (code) { html.push(`
${escapeHtml(code.join("\n"))}
`); code = null; } else { code = []; } + continue; + } + if (code) { code.push(line); continue; } + if (line.startsWith("|")) { closeList(); table.push(line); continue; } + closeTable(); + if (/^###\s+/.test(line)) { closeList(); html.push(`

${renderInline(line.slice(4))}

`); continue; } + if (/^##\s+/.test(line)) { closeList(); html.push(`

${renderInline(line.slice(3))}

`); continue; } + if (/^#\s+/.test(line)) { closeList(); html.push(`

${renderInline(line.slice(2))}

`); continue; } + if (/^-\s+/.test(line)) { if (list !== "ul") { closeList(); list = "ul"; html.push("