From 60599fc35ad1fba9ced1d36cc3fddc94281590b4 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Thu, 30 Jul 2026 19:25:54 +1000 Subject: [PATCH 1/3] docs(prisma): add generated API reference --- .gitignore | 2 + IA.md | 2 +- .../prisma/api-reference/meta.json | 14 ++ content/docs/integrations/prisma/index.mdx | 1 + content/docs/integrations/prisma/meta.json | 1 + package.json | 3 +- scripts/generate-prisma-docs.ts | 45 +++++++ scripts/lib/docs-generator.ts | 121 +++++++++++++----- scripts/validate-content-api.ts | 1 + scripts/validate-links.ts | 5 +- 10 files changed, 159 insertions(+), 36 deletions(-) create mode 100644 content/docs/integrations/prisma/api-reference/meta.json create mode 100644 scripts/generate-prisma-docs.ts diff --git a/.gitignore b/.gitignore index 856a089..7e2c4ba 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,8 @@ content/stack/reference/stack/latest/ content/stack/reference/stack/v*/ content/stack/reference/drizzle/latest/ content/stack/reference/drizzle/v*/ +content/docs/integrations/prisma/api-reference/* +!content/docs/integrations/prisma/api-reference/meta.json .tmp-* .env.local diff --git a/IA.md b/IA.md index 3244e94..1b7436f 100644 --- a/IA.md +++ b/IA.md @@ -309,7 +309,7 @@ Two notes: - [ ] ⛔ `/integrations/supabase/edge-functions` — pending Deno/FFI answer - [ ] ⛔ `/integrations/supabase/realtime` — pending product verification - [x] `/integrations/drizzle` -- [x] `/integrations/prisma` — Prisma ORM 8 RC, EQL v3, Prisma Postgres, and Prisma Compute +- [x] `/integrations/prisma` — Prisma ORM 8 RC, EQL v3, Prisma Postgres, Prisma Compute, and generated API reference - [ ] `/integrations/aws/rds-aurora` — Proxy path - [ ] `/integrations/aws/dynamodb` - [ ] `/integrations/clerk` diff --git a/content/docs/integrations/prisma/api-reference/meta.json b/content/docs/integrations/prisma/api-reference/meta.json new file mode 100644 index 0000000..10d114f --- /dev/null +++ b/content/docs/integrations/prisma/api-reference/meta.json @@ -0,0 +1,14 @@ +{ + "title": "API reference", + "pages": [ + "index", + "codec-types", + "column-types", + "control", + "operation-types", + "pack", + "runtime", + "stack", + "v3" + ] +} diff --git a/content/docs/integrations/prisma/index.mdx b/content/docs/integrations/prisma/index.mdx index d8fe996..3fa9652 100644 --- a/content/docs/integrations/prisma/index.mdx +++ b/content/docs/integrations/prisma/index.mdx @@ -28,6 +28,7 @@ It does not extend the classic `@prisma/client` API. For earlier Prisma releases | --- | --- | | Encrypt and query your first Prisma field | [Prisma ORM Quickstart](/integrations/prisma/quickstart) | | Choose column types or look up query operators | [Schema and queries](/integrations/prisma/schema-and-queries) | +| Look up every exported function and type | [API reference](/integrations/prisma/api-reference) | | Encrypt data already in production | [Prisma deployment guide](/integrations/prisma/deployment) | | Use Prisma's managed database | [Prisma Postgres](/integrations/prisma/prisma-postgres) | | Deploy the application with Prisma | [Prisma Compute](/integrations/prisma/prisma-compute) | diff --git a/content/docs/integrations/prisma/meta.json b/content/docs/integrations/prisma/meta.json index 67e8db1..6a9ebfb 100644 --- a/content/docs/integrations/prisma/meta.json +++ b/content/docs/integrations/prisma/meta.json @@ -5,6 +5,7 @@ "index", "quickstart", "schema-and-queries", + "api-reference", "deployment", "prisma-postgres", "prisma-compute" diff --git a/package.json b/package.json index 44256fd..db688e4 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "private": true, "scripts": { - "prebuild": "bun run generate-docs && bun run generate-docs:eql && bun run generate-docs:eql-api && bun run generate-docs:cli && bun run validate-content && bun run validate-mermaid && bun run validate-links && bun run validate-redirects", + "prebuild": "bun run generate-docs && bun run generate-docs:prisma && bun run generate-docs:eql && bun run generate-docs:eql-api && bun run generate-docs:cli && bun run validate-content && bun run validate-mermaid && bun run validate-links && bun run validate-redirects", "build": "next build", "dev": "next dev -p 3001", "start": "next start", @@ -12,6 +12,7 @@ "lint": "biome check", "format": "biome format --write", "generate-docs": "tsx scripts/generate-docs.ts", + "generate-docs:prisma": "tsx scripts/generate-prisma-docs.ts", "generate-docs:eql": "tsx scripts/generate-eql-docs.ts", "generate-docs:eql-api": "tsx scripts/generate-eql-api-docs.ts", "generate-docs:cli": "tsx scripts/generate-cli-docs.ts", diff --git a/scripts/generate-prisma-docs.ts b/scripts/generate-prisma-docs.ts new file mode 100644 index 0000000..9675aa4 --- /dev/null +++ b/scripts/generate-prisma-docs.ts @@ -0,0 +1,45 @@ +#!/usr/bin/env tsx +/** Generate the Prisma ORM integration's API reference from Stack main. */ +import path from "node:path"; +import { type DocsConfig, generateDocs } from "./lib/docs-generator.js"; + +const prismaConfig: DocsConfig = { + packageName: "@cipherstash/stack-prisma", + projectName: "@cipherstash/stack-prisma", + repoUrl: "https://github.com/cipherstash/stack.git", + sourceRef: "main", + tempDirName: ".tmp-stack-prisma", + baseOutputDir: path.join( + process.cwd(), + "content/docs/integrations/prisma/api-reference", + ), + publicPath: "/integrations/prisma/api-reference", + metaTitle: "API reference", + versionedOutput: false, + entryPointBasePath: "packages/stack-prisma/src/exports", + router: "module", + flattenOutputFiles: true, + entryPoints: [ + "./packages/stack-prisma/src/exports/codec-types.ts", + "./packages/stack-prisma/src/exports/column-types.ts", + "./packages/stack-prisma/src/exports/control.ts", + "./packages/stack-prisma/src/exports/operation-types.ts", + "./packages/stack-prisma/src/exports/pack.ts", + "./packages/stack-prisma/src/exports/runtime.ts", + "./packages/stack-prisma/src/exports/stack.ts", + "./packages/stack-prisma/src/exports/v3.ts", + ], + tsconfigInclude: ["packages/stack-prisma/src/**/*"], + tagFilter: () => false, + referencePathSegment: "prisma", + frontmatterGlobals: { + type: "reference", + components: ["encryption", "eql"], + audience: ["developer"], + }, +}; + +generateDocs(prismaConfig).catch((error) => { + console.error("Failed to generate the Prisma API reference:", error); + process.exit(1); +}); diff --git a/scripts/lib/docs-generator.ts b/scripts/lib/docs-generator.ts index 92cc52b..5309ff9 100644 --- a/scripts/lib/docs-generator.ts +++ b/scripts/lib/docs-generator.ts @@ -22,6 +22,24 @@ export interface DocsConfig { tagFilter: (tag: string) => boolean; /** Reference URL path segment (e.g., 'stack' or 'drizzle') */ referencePathSegment: string; + /** Generate from a branch or commit instead of selecting a release tag. */ + sourceRef?: string; + /** Public route before the generated version directory. */ + publicPath?: string; + /** Navigation title written to the package-level meta.json. */ + metaTitle?: string; + /** TypeDoc project name. */ + projectName?: string; + /** Additional frontmatter added to every generated page. */ + frontmatterGlobals?: Record; + /** Generate directly into baseOutputDir instead of a latest/version folder. */ + versionedOutput?: boolean; + /** Base path TypeDoc uses to name entry-point modules. */ + entryPointBasePath?: string; + /** TypeDoc Markdown output router. */ + router?: "member" | "module"; + /** Write generated module pages into one directory. */ + flattenOutputFiles?: boolean; } /** @@ -66,6 +84,9 @@ export async function stripMdxExtensions(dir: string): Promise { // TypeDoc generates links to index pages (e.g., ../index or /stack/.../index) // but Fumadocs serves index.mdx at the directory URL without /index. content = content.replace(/\]\(([^)#]*)\/index([#)])/g, "]($1$2"); + // A flattened module sits beside the root index and links back to it as + // `(index)`, without a preceding slash for the rule above to match. + content = content.replace(/\]\(index([#)])/g, "](./$1"); // Strip temp directory prefix from source link text (e.g., .tmp-stack/) // Matches: [.tmp-stack/packages/...](url) → [packages/...](url) content = content.replace(/\[\.tmp-[^/]+\//g, "["); @@ -102,7 +123,11 @@ export async function generateMetaJson(dir: string): Promise { } const metaPath = path.join(dir, "meta.json"); - await fs.writeFile(metaPath, JSON.stringify({ pages }, null, 2), "utf8"); + await fs.writeFile( + metaPath, + `${JSON.stringify({ pages }, null, 2)}\n`, + "utf8", + ); } /** @@ -223,12 +248,16 @@ export async function generateDocsForTag( const versionString = version ? `v${version.major}.${version.minor}.${version.patch}` : tag; - const dirName = isLatest ? "latest" : versionString; - const outputDir = path.join(config.baseOutputDir, dirName); + const dirName = + config.versionedOutput === false ? "" : isLatest ? "latest" : versionString; + const outputDir = dirName + ? path.join(config.baseOutputDir, dirName) + : config.baseOutputDir; + const displayDirName = dirName || "unversioned API reference"; console.log(`\n${"=".repeat(60)}`); console.log( - `Generating docs for ${dirName}${isLatest ? ` (${versionString})` : ""}`, + `Generating docs for ${displayDirName}${isLatest ? ` (${versionString})` : ""}`, ); console.log("=".repeat(60)); @@ -296,9 +325,12 @@ export async function generateDocsForTag( // Create TypeDoc configuration const typedocConfig = { + name: config.projectName, entryPoints: config.entryPoints, tsconfig: "./typedoc.tsconfig.json", - basePath: workingDir, + basePath: config.entryPointBasePath + ? path.join(workingDir, config.entryPointBasePath) + : workingDir, plugin: [ "typedoc-plugin-markdown", "typedoc-plugin-frontmatter", @@ -308,6 +340,7 @@ export async function generateDocsForTag( readme: "none", frontmatterGlobals: { packageVersion: versionString, + ...config.frontmatterGlobals, }, frontmatterCommentTags: ["author", "description"], githubPages: false, @@ -385,6 +418,8 @@ export async function generateDocsForTag( sanitizeComments: true, fileExtension: ".mdx", entryFileName: "index", + router: config.router, + flattenOutputFiles: config.flattenOutputFiles, // Type errors fail the build, deliberately. This was `true` to tolerate // "cross-package type references unresolved even though the source is // correct" — but that diagnosis was wrong, and tolerating it was not free: @@ -403,7 +438,7 @@ export async function generateDocsForTag( "Variable", "Enum", ], - publicPath: `/stack/reference/${config.referencePathSegment}/${dirName}/`, + publicPath: `${(config.publicPath ?? `/stack/reference/${config.referencePathSegment}`).replace(/\/$/, "")}${dirName ? `/${dirName}` : ""}/`, }; const configPath = path.join(workingDir, "typedoc.json"); @@ -426,7 +461,7 @@ export async function generateDocsForTag( console.log("Generating meta.json files..."); await generateMetaJson(outputDir); - console.log(`Docs for ${dirName} generated successfully!`); + console.log(`Docs for ${displayDirName} generated successfully!`); console.log(`Output directory: ${outputDir}`); return { dirName, versionString, isLatest }; @@ -475,27 +510,34 @@ export async function generateDocs(config: DocsConfig): Promise { }); workingDir = tempDir; - console.log("Fetching all tags..."); - execSync("git fetch --tags", { cwd: workingDir, stdio: "inherit" }); - - allTags = execSync("git tag --sort=-v:refname", { - cwd: workingDir, - encoding: "utf8", - }) - .trim() - .split("\n"); - - if (allTags.length === 0 || allTags[0] === "") { - throw new Error(`No tags found in ${config.packageName} repository`); + if (config.sourceRef) { + allTags = [config.sourceRef]; + console.log(`Using source ref ${config.sourceRef}`); + } else { + console.log("Fetching all tags..."); + execSync("git fetch --tags", { cwd: workingDir, stdio: "inherit" }); + + allTags = execSync("git tag --sort=-v:refname", { + cwd: workingDir, + encoding: "utf8", + }) + .trim() + .split("\n"); + + if (allTags.length === 0 || allTags[0] === "") { + throw new Error(`No tags found in ${config.packageName} repository`); + } + + console.log(`Found ${allTags.length} tags`); } - - console.log(`Found ${allTags.length} tags`); } // Determine which versions to generate const versionsToGenerate = localPath ? [{ tag: "local-dev", isLatest: true }] - : getVersionsToGenerate(allTags, config.tagFilter); + : config.sourceRef + ? [{ tag: config.sourceRef, isLatest: true }] + : getVersionsToGenerate(allTags, config.tagFilter); if (!localPath && versionsToGenerate.length === 0) { throw new Error( @@ -511,12 +553,17 @@ export async function generateDocs(config: DocsConfig): Promise { // Clean existing generated output (preserve hand-authored files) for (const { tag, isLatest } of versionsToGenerate) { const version = parseVersion(tag); - const dirName = isLatest - ? "latest" - : version - ? `v${version.major}.${version.minor}.${version.patch}` - : tag; - const versionDir = path.join(config.baseOutputDir, dirName); + const dirName = + config.versionedOutput === false + ? "" + : isLatest + ? "latest" + : version + ? `v${version.major}.${version.minor}.${version.patch}` + : tag; + const versionDir = dirName + ? path.join(config.baseOutputDir, dirName) + : config.baseOutputDir; await fs.rm(versionDir, { recursive: true, force: true }); } @@ -533,14 +580,22 @@ export async function generateDocs(config: DocsConfig): Promise { generatedVersions.push(versionInfo); } - // Generate a meta.json for the package directory listing versions + // Generate package-level navigation. An unversioned reference already has + // a complete meta.json from generateMetaJson; preserve its page list. const packageMetaPath = path.join(config.baseOutputDir, "meta.json"); - const packageMeta = { - pages: generatedVersions.map(({ dirName }) => dirName), - }; + const packageMeta = + config.versionedOutput === false + ? { + ...(config.metaTitle ? { title: config.metaTitle } : {}), + ...JSON.parse(await fs.readFile(packageMetaPath, "utf8")), + } + : { + ...(config.metaTitle ? { title: config.metaTitle } : {}), + pages: generatedVersions.map(({ dirName }) => dirName), + }; await fs.writeFile( packageMetaPath, - JSON.stringify(packageMeta, null, 2), + `${JSON.stringify(packageMeta, null, 2)}\n`, "utf8", ); diff --git a/scripts/validate-content-api.ts b/scripts/validate-content-api.ts index b260ead..a421360 100644 --- a/scripts/validate-content-api.ts +++ b/scripts/validate-content-api.ts @@ -36,6 +36,7 @@ import path from "node:path"; const SKIP_PATHS = [ "content/stack/reference/stack/", // TypeDoc output "content/docs/reference/stack/", // TypeDoc output (post-repoint) + "content/docs/integrations/prisma/api-reference/", // TypeDoc output "content/stack/reference/eql/index.mdx", // generated EQL API reference "content/docs/reference/eql/functions.mdx", // generated "content/docs/reference/cli/", // generated from `stash manifest --json` diff --git a/scripts/validate-links.ts b/scripts/validate-links.ts index 5f1dc8b..6cc963b 100644 --- a/scripts/validate-links.ts +++ b/scripts/validate-links.ts @@ -358,7 +358,10 @@ allBroken.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line); * every link into those pages looks broken — say so rather than let a hundred * spurious errors imply the docs are falling apart. */ -const GENERATED = ["content/stack/reference/stack/latest"]; +const GENERATED = [ + "content/stack/reference/stack/latest", + "content/docs/integrations/prisma/api-reference/index.mdx", +]; const missing = GENERATED.filter((d) => !fs.existsSync(path.join(ROOT, d))); console.log( From 2aafd644f01302d479870813dccb68f8213a4b4a Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Thu, 30 Jul 2026 20:53:20 +1000 Subject: [PATCH 2/3] fix(docs): flatten TypeDoc code spans Fumadocs can parse braces on wrapped continuation lines as MDX expressions. --- .github/workflows/docs.yml | 3 ++ scripts/lib/docs-generator.ts | 90 +++++++++++++++++++++++++++++++++++ scripts/validate-links.ts | 4 +- 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f30a96f..220f09c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -49,5 +49,8 @@ jobs: - name: Generate the SDK reference run: bun run generate-docs + - name: Generate the Prisma reference + run: bun run generate-docs:prisma + - name: Validate internal links and anchors run: bun run validate-links diff --git a/scripts/lib/docs-generator.ts b/scripts/lib/docs-generator.ts index 5309ff9..ef29cdc 100644 --- a/scripts/lib/docs-generator.ts +++ b/scripts/lib/docs-generator.ts @@ -51,6 +51,95 @@ export interface VersionInfo { patch: number; } +/** + * Keep inline code spans on one physical line. + * + * TSDoc comments are commonly wrapped at a fixed width, including in the + * middle of a backtick span. Markdown permits that, but MDX can reinterpret a + * `{ ... }` on the continuation line as an expression inside its surrounding + * list or blockquote container. Fenced code blocks are deliberately excluded. + */ +function collapseMultilineInlineCode(content: string): string { + // TypeDoc's frontmatter plugin can truncate a summary midway through an + // inline-code span. Never pair that unmatched backtick with one in the MDX + // body: frontmatter is YAML and does not need Markdown normalization. + const frontmatter = content.match(/^---\n[\s\S]*?\n---\n?/)?.[0] ?? ""; + const lines = content.slice(frontmatter.length).split("\n"); + const output: string[] = []; + let prose: string[] = []; + let fence: { character: string; length: number } | undefined; + + const collapseProse = () => { + if (prose.length === 0) return; + const block = prose.join("\n"); + let result = ""; + let cursor = 0; + + while (cursor < block.length) { + const start = block.indexOf("`", cursor); + if (start === -1) { + result += block.slice(cursor); + break; + } + + let openingLength = 1; + while (block[start + openingLength] === "`") openingLength += 1; + result += block.slice(cursor, start + openingLength); + cursor = start + openingLength; + + let closing = cursor; + while (closing < block.length) { + closing = block.indexOf("`", closing); + if (closing === -1) break; + let closingLength = 1; + while (block[closing + closingLength] === "`") closingLength += 1; + if (closingLength === openingLength) break; + closing += closingLength; + } + + if (closing === -1) { + result += block.slice(cursor); + cursor = block.length; + break; + } + + const body = block.slice(cursor, closing); + result += body.includes("\n") + ? body.replace(/[ \t]*\n[ \t]*/g, " ") + : body; + result += "`".repeat(openingLength); + cursor = closing + openingLength; + } + + output.push(...result.split("\n")); + prose = []; + }; + + for (const line of lines) { + const match = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/); + const marker = match?.[1]; + if (!fence && marker) { + collapseProse(); + output.push(line); + fence = { character: marker[0] ?? "", length: marker.length }; + } else if (fence) { + output.push(line); + if ( + marker?.[0] === fence.character && + marker.length >= fence.length && + match?.[2]?.trim() === "" + ) { + fence = undefined; + } + } else { + prose.push(line); + } + } + collapseProse(); + + return `${frontmatter}${output.join("\n")}`; +} + /** * Cleans up markdown links in generated .mdx files: * 1. Strips .mdx extensions from link targets @@ -75,6 +164,7 @@ export async function stripMdxExtensions(dir: string): Promise { await stripMdxExtensions(fullPath); } else if (entry.isFile() && entry.name.endsWith(".mdx")) { let content = await fs.readFile(fullPath, "utf8"); + content = collapseMultilineInlineCode(content); content = content.replace(/\]\(([^)#]+)\.mdx([#)])/g, "]($1$2"); content = content.replace( /\]\(\/docs\/reference\//g, diff --git a/scripts/validate-links.ts b/scripts/validate-links.ts index 6cc963b..c33b356 100644 --- a/scripts/validate-links.ts +++ b/scripts/validate-links.ts @@ -372,8 +372,8 @@ console.log( if (missing.length > 0) { console.log( `\n! Generated API pages are absent (${missing.join(", ")}), so links into\n` + - " them will be reported as missing. Run `bun run generate-docs` first —\n" + - " `prebuild` does, which is why CI does not hit this.", + " them will be reported as missing. Run the matching `generate-docs:*`\n" + + " task first; `prebuild` and the links workflow do this automatically.", ); } From acf865f33343a1b227a0a6b505b9cfa90a48cff8 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Thu, 30 Jul 2026 21:13:09 +1000 Subject: [PATCH 3/3] docs(prisma): move API reference nav last --- content/docs/integrations/prisma/meta.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/docs/integrations/prisma/meta.json b/content/docs/integrations/prisma/meta.json index 6a9ebfb..3f6502e 100644 --- a/content/docs/integrations/prisma/meta.json +++ b/content/docs/integrations/prisma/meta.json @@ -5,9 +5,9 @@ "index", "quickstart", "schema-and-queries", - "api-reference", "deployment", "prisma-postgres", - "prisma-compute" + "prisma-compute", + "api-reference" ] }