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
+${renderInline(site.description)}
+${renderInline(site.lead)}
+ +${escapeHtml(site.quickstart)}
+ ${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.replace(/^>\s?/, ""))}`); continue; } + if (!line.trim()) { closeList(); continue; } + closeList(); html.push(`
${renderInline(line.trim())}
`); + } + closeList(); closeTable(); + return html.join("\n"); +} + +function renderTable(lines) { + const rows = lines.map((line) => line.trim().slice(1, -1).split("|").map((cell) => cell.trim())); + const body = rows.filter((row) => !row.every((cell) => /^:?-{3,}:?$/.test(cell))).map((row, index) => { + const tag = index === 0 ? "th" : "td"; + return `${escapeHtml(segment.slice(1, -1))}`;
+ }
+ return renderTextLinks(segment);
+ }).join("");
+}
+
+function renderTextLinks(value) {
+ return String(value).split(/(\[[^\]]+\]\([^)]+\))/g).map((segment) => {
+ const match = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(segment);
+ if (match) return `${escapeHtml(match[1])}`;
+ let html = escapeHtml(segment);
+ for (const [name, url] of asyncProjects) {
+ if (name === site.title) continue;
+ html = html.replace(new RegExp(escapeRegExp(name), "g"), `${name}`);
+ }
+ return html;
+ }).join("");
+}
+
+function firstHeading(source) {
+ return source.split(/\r?\n/).find((line) => line.startsWith("# "))?.replace(/^#\s+/, "").trim() || "";
+}
+function slug(value) { return String(value).toLowerCase().replace(/<[^>]+>/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "section"; }
+function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
+function escapeHtml(value) { return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); }
diff --git a/test/package/exports.test.ts b/test/package/exports.test.ts
index 71034e4..d1e20f7 100644
--- a/test/package/exports.test.ts
+++ b/test/package/exports.test.ts
@@ -461,7 +461,7 @@ test('generated async-pipeline workflow owns release, preview, snapshot, and Pag
assert.match(workflow, /release:/);
assert.match(workflow, /name: pages/);
assert.match(workflow, /name: pages-deploy/);
- assert.match(workflow, /path: "\.\/website\/dist"/);
+ assert.match(workflow, /path: "\.async\/pages"/);
assert.match(workflow, /name: preview/);
assert.match(workflow, /name: snapshot/);
assert.match(workflow, /name: publish-github/);
@@ -496,6 +496,7 @@ test('generated async-pipeline workflow owns release, preview, snapshot, and Pag
assert(lock.jobs.some((job: { id: string }) => job.id === 'publish'));
assert(lock.jobs.some((job: { id: string }) => job.id === 'release-doctor'));
assert(taskLock.commands.some((command: { name: string }) => command.name === 'pipeline:release:ensure'));
+ assert(taskLock.commands.some((command: { name: string }) => command.name === 'pipeline:task:docs.site'));
assert.deepEqual(releaseConfig.packages['.'], {
'release-type': 'node',
'package-name': '@async/db',