@agentvibes/site 0.0.0-stage → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -2
  3. package/bin/agentvibes-site.mjs +7 -0
  4. package/package.json +87 -3
  5. package/src/app-mode.tsx +110 -0
  6. package/src/catalog.ts +80 -0
  7. package/src/cli.ts +51 -0
  8. package/src/client.ts +5 -0
  9. package/src/components/changelog-entry-view.tsx +11 -0
  10. package/src/components/doctor-throw/doctor-throw.tsx +15 -0
  11. package/src/components/errors-init.tsx +14 -0
  12. package/src/components/hero/hero-action-link.tsx +15 -0
  13. package/src/components/hero/hero.tsx +31 -0
  14. package/src/components/home-shell.tsx +19 -0
  15. package/src/components/jsdoc-view/group-view.tsx +14 -0
  16. package/src/components/jsdoc-view/item-view.tsx +24 -0
  17. package/src/components/jsdoc-view/jsdoc-view.tsx +16 -0
  18. package/src/components/jsdoc-view/member-view.tsx +15 -0
  19. package/src/components/module-cards/module-card.tsx +13 -0
  20. package/src/components/module-cards/module-cards.tsx +15 -0
  21. package/src/components/page-actions/copy-button.tsx +40 -0
  22. package/src/components/page-actions/page-actions.tsx +53 -0
  23. package/src/components/page-footer.tsx +20 -0
  24. package/src/components/page-link/page-link.tsx +19 -0
  25. package/src/components/site-error-boundary/error-panel.tsx +16 -0
  26. package/src/components/site-error-boundary/site-error-boundary.tsx +12 -0
  27. package/src/components/site-error-boundary/site-global-error-boundary.tsx +19 -0
  28. package/src/components/site-footer/footer-column-view.tsx +15 -0
  29. package/src/components/site-footer/footer-link.tsx +20 -0
  30. package/src/components/site-footer/site-footer.tsx +32 -0
  31. package/src/components/site-video/site-video.tsx +25 -0
  32. package/src/components/telemetry-scripts.tsx +18 -0
  33. package/src/config.ts +9 -0
  34. package/src/context.ts +68 -0
  35. package/src/data.ts +64 -0
  36. package/src/docs-title.ts +8 -0
  37. package/src/doctor-routes.tsx +44 -0
  38. package/src/index.ts +39 -0
  39. package/src/instrumentation.ts +53 -0
  40. package/src/jsdoc.ts +38 -0
  41. package/src/layout-options.ts +18 -0
  42. package/src/layouts.tsx +63 -0
  43. package/src/mdx-config.d.ts +2 -0
  44. package/src/mdx-config.js +7 -0
  45. package/src/mdx.tsx +18 -0
  46. package/src/nav.ts +97 -0
  47. package/src/og-route.tsx +29 -0
  48. package/src/output.ts +90 -0
  49. package/src/pages.tsx +186 -0
  50. package/src/prepare/assets.ts +98 -0
  51. package/src/prepare/author-next-config.ts +46 -0
  52. package/src/prepare/changelog.ts +17 -0
  53. package/src/prepare/files.ts +95 -0
  54. package/src/prepare/finish-static.ts +122 -0
  55. package/src/prepare/index.ts +291 -0
  56. package/src/prepare/jsdoc-extract.ts +182 -0
  57. package/src/prepare/release.ts +12 -0
  58. package/src/prepare/templates.ts +415 -0
  59. package/src/prepare/unimplemented.ts +32 -0
  60. package/src/proxy.ts +57 -0
  61. package/src/routes.ts +158 -0
  62. package/src/runtime.tsx +165 -0
  63. package/src/theme.ts +59 -0
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Build-time API extraction with the TypeScript compiler (ported from the
3
+ * npm-factory site's scripts/build-jsdoc.mjs). Reads each npm module's source
4
+ * entry and returns its exports with signatures and JSDoc.
5
+ */
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+
9
+ import ts from "typescript";
10
+ import { z } from "zod/v4";
11
+
12
+ import type { ApiItem, ApiItemKind, ApiMember } from "../data";
13
+
14
+ function jsdocText(node: ts.Node): string | undefined {
15
+ const parts = ts
16
+ .getJSDocCommentsAndTags(node)
17
+ .filter(ts.isJSDoc)
18
+ .map((doc) => (typeof doc.comment === "string" ? doc.comment : (doc.comment ?? []).map((part) => part.text).join("")).trim())
19
+ .filter((text) => text !== "");
20
+ return parts.length === 0 ? undefined : parts.join("\n\n");
21
+ }
22
+
23
+ function symbolDoc(symbol: ts.Symbol, checker: ts.TypeChecker): string | undefined {
24
+ const text = ts.displayPartsToString(symbol.getDocumentationComment(checker)).trim();
25
+ return text === "" ? undefined : text;
26
+ }
27
+
28
+ function isPublicMember(member: ts.Node): boolean {
29
+ const modifiers = ts.canHaveModifiers(member) ? ts.getModifiers(member) : undefined;
30
+ return !(modifiers ?? []).some((m) => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword);
31
+ }
32
+
33
+ function classify(decl: ts.Declaration): ApiItemKind | undefined {
34
+ if (ts.isClassDeclaration(decl)) return "class";
35
+ if (ts.isFunctionDeclaration(decl)) return "function";
36
+ if (ts.isInterfaceDeclaration(decl)) return "interface";
37
+ if (ts.isTypeAliasDeclaration(decl)) return "type";
38
+ if (ts.isEnumDeclaration(decl)) return "enum";
39
+ if (ts.isVariableDeclaration(decl)) return "const";
40
+ return undefined;
41
+ }
42
+
43
+ function signatureOf(checker: ts.TypeChecker, name: string, decl: ts.SignatureDeclaration): string | undefined {
44
+ const sig = checker.getSignatureFromDeclaration(decl);
45
+ if (sig === undefined) return undefined;
46
+ const raw = checker.signatureToString(sig, decl, ts.TypeFormatFlags.NoTruncation);
47
+ return raw.startsWith("(") ? `${name}${raw}` : raw;
48
+ }
49
+
50
+ function typeText(checker: ts.TypeChecker, node: ts.Node): string {
51
+ return checker.typeToString(checker.getTypeAtLocation(node), node, ts.TypeFormatFlags.NoTruncation);
52
+ }
53
+
54
+ function withDescription<T extends object>(value: T, description: string | undefined): T & { description?: string } {
55
+ return description === undefined ? value : { ...value, description };
56
+ }
57
+
58
+ function classMembers(checker: ts.TypeChecker, decl: ts.ClassDeclaration): ApiMember[] {
59
+ const members: ApiMember[] = [];
60
+ for (const member of decl.members) {
61
+ if (!isPublicMember(member)) continue;
62
+ if (ts.isConstructorDeclaration(member)) {
63
+ members.push(withDescription({ kind: "constructor" as const, name: "constructor", signature: signatureOf(checker, "constructor", member) ?? "constructor()" }, jsdocText(member)));
64
+ } else if (ts.isMethodDeclaration(member)) {
65
+ const name = member.name.getText();
66
+ members.push(withDescription({ kind: "method" as const, name, signature: signatureOf(checker, name, member) ?? `${name}()` }, jsdocText(member)));
67
+ } else if (ts.isPropertyDeclaration(member)) {
68
+ const name = member.name.getText();
69
+ members.push(withDescription({ kind: "property" as const, name, signature: `${name}: ${typeText(checker, member)}` }, jsdocText(member)));
70
+ }
71
+ }
72
+ return members;
73
+ }
74
+
75
+ function interfaceMembers(checker: ts.TypeChecker, decl: ts.InterfaceDeclaration): ApiMember[] {
76
+ const members: ApiMember[] = [];
77
+ for (const member of decl.members) {
78
+ if (ts.isMethodSignature(member)) {
79
+ const name = member.name.getText();
80
+ members.push(withDescription({ kind: "method" as const, name, signature: signatureOf(checker, name, member) ?? `${name}()` }, jsdocText(member)));
81
+ } else if (ts.isPropertySignature(member)) {
82
+ const name = member.name.getText();
83
+ members.push(withDescription({ kind: "property" as const, name, signature: `${name}: ${typeText(checker, member)}` }, jsdocText(member)));
84
+ }
85
+ }
86
+ return members;
87
+ }
88
+
89
+ function itemOf(symbol: ts.Symbol, checker: ts.TypeChecker, sourceFile: ts.SourceFile, name: string): ApiItem | undefined {
90
+ const decls = symbol.getDeclarations() ?? [];
91
+ const decl = decls.find((d) => classify(d) !== undefined);
92
+ if (decl === undefined) return undefined;
93
+ const description = symbolDoc(symbol, checker) ?? jsdocText(decl);
94
+ if (ts.isFunctionDeclaration(decl)) {
95
+ return withDescription({ kind: "function" as const, name, signature: signatureOf(checker, name, decl) ?? `${name}()` }, description);
96
+ }
97
+ if (ts.isClassDeclaration(decl)) {
98
+ return withDescription({ kind: "class" as const, name, signature: `class ${name}`, members: classMembers(checker, decl) }, description);
99
+ }
100
+ if (ts.isInterfaceDeclaration(decl)) {
101
+ return withDescription({ kind: "interface" as const, name, signature: `interface ${name}`, members: interfaceMembers(checker, decl) }, description);
102
+ }
103
+ if (ts.isTypeAliasDeclaration(decl)) {
104
+ const text = typeText(checker, decl);
105
+ return withDescription({ kind: "type" as const, name, signature: `type ${name} = ${text === name ? decl.type.getText() : text}` }, description);
106
+ }
107
+ if (ts.isEnumDeclaration(decl)) {
108
+ const members = decl.members.map((m) => withDescription({ kind: "property" as const, name: m.name.getText(), signature: m.name.getText() }, jsdocText(m)));
109
+ return withDescription({ kind: "enum" as const, name, signature: `enum ${name}`, members }, description);
110
+ }
111
+ if (ts.isVariableDeclaration(decl)) {
112
+ const text = checker.typeToString(checker.getTypeOfSymbolAtLocation(symbol, sourceFile), decl, ts.TypeFormatFlags.NoTruncation);
113
+ return withDescription({ kind: "const" as const, name, signature: `const ${name}: ${text}` }, description);
114
+ }
115
+ return undefined;
116
+ }
117
+
118
+ /** Exports of a TypeScript entry file. */
119
+ export function extractApi(entry: string): ApiItem[] {
120
+ const options: ts.CompilerOptions = {
121
+ target: ts.ScriptTarget.ES2022,
122
+ module: ts.ModuleKind.ESNext,
123
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
124
+ jsx: ts.JsxEmit.ReactJSX
125
+ };
126
+ const program = ts.createProgram([entry], options, ts.createCompilerHost(options, true));
127
+ const sourceFile = program.getSourceFile(entry);
128
+ if (sourceFile === undefined) return [];
129
+ const checker = program.getTypeChecker();
130
+ const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
131
+ if (moduleSymbol === undefined) return [];
132
+ return checker.getExportsOfModule(moduleSymbol).flatMap((symbol) => {
133
+ const target = (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(symbol) : symbol;
134
+ const item = itemOf(target, checker, sourceFile, symbol.getName());
135
+ return item === undefined ? [] : [item];
136
+ });
137
+ }
138
+
139
+ const ExportTarget = z.union([z.string(), z.looseObject({ import: z.string().optional(), default: z.string().optional(), types: z.string().optional() })]);
140
+ const PackageJson = z.looseObject({
141
+ name: z.string().optional(),
142
+ exports: z.union([z.string(), z.looseObject({ ".": ExportTarget.optional() })]).optional(),
143
+ types: z.string().optional(),
144
+ main: z.string().optional(),
145
+ module: z.string().optional()
146
+ });
147
+
148
+ export type PackageJsonInfo = z.output<typeof PackageJson>;
149
+
150
+ export function readPackageJson(packageDir: string): PackageJsonInfo | undefined {
151
+ const file = path.join(packageDir, "package.json");
152
+ if (!fs.existsSync(file)) return undefined;
153
+ return PackageJson.parse(JSON.parse(fs.readFileSync(file, "utf8")));
154
+ }
155
+
156
+ function candidates(pkg: PackageJsonInfo): string[] {
157
+ const out: string[] = [];
158
+ const root = typeof pkg.exports === "string" ? pkg.exports : pkg.exports?.["."];
159
+ if (typeof root === "string") out.push(root);
160
+ else if (root !== undefined) for (const key of ["import", "default", "types"] as const) if (root[key] !== undefined) out.push(root[key]);
161
+ for (const field of [pkg.types, pkg.main, pkg.module]) if (field !== undefined) out.push(field);
162
+ return out;
163
+ }
164
+
165
+ /** `dist/x.js` → `src/x.ts`; a `src/*.ts(x)` path stays. */
166
+ function sourceOf(candidate: string): string | undefined {
167
+ const stripped = candidate.replace(/^\.\//, "");
168
+ if (/^src\/.+\.tsx?$/.test(stripped)) return stripped;
169
+ const dist = stripped.match(/^dist\/(.+?)\.(?:m?js|cjs|d\.ts)$/);
170
+ return dist?.[1] === undefined ? undefined : path.join("src", `${dist[1]}.ts`);
171
+ }
172
+
173
+ /** The TypeScript source entry of a package directory, or undefined. */
174
+ export function resolveSourceEntry(packageDir: string): string | undefined {
175
+ const pkg = readPackageJson(packageDir);
176
+ for (const candidate of pkg === undefined ? [] : candidates(pkg)) {
177
+ const rel = sourceOf(candidate);
178
+ if (rel !== undefined && fs.existsSync(path.join(packageDir, rel))) return path.join(packageDir, rel);
179
+ }
180
+ const fallback = path.join(packageDir, "src", "index.ts");
181
+ return fs.existsSync(fallback) ? fallback : undefined;
182
+ }
@@ -0,0 +1,12 @@
1
+ /** Environment variables that carry the build's git SHA, in priority order. */
2
+ const RELEASE_ENV = ["VERCEL_GIT_COMMIT_SHA", "GITHUB_SHA"] as const;
3
+
4
+ /**
5
+ * The git SHA tagged on every analytics event and error. An empty variable
6
+ * counts as unset: `vercel pull` writes `VERCEL_GIT_COMMIT_SHA=""` for CLI
7
+ * deploys, which would otherwise hide `GITHUB_SHA` and the git checkout.
8
+ */
9
+ export function releaseOf(env: Record<string, string | undefined>, gitHead: () => string | undefined): string {
10
+ const fromEnv = RELEASE_ENV.map((name) => env[name]?.trim()).find((value) => value !== undefined && value !== "");
11
+ return fromEnv ?? gitHead() ?? "unknown";
12
+ }
@@ -0,0 +1,415 @@
1
+ /**
2
+ * The files of a generated site. Each route file only binds a factory from
3
+ * `@agentvibes/site` to the site runtime, so all behaviour lives (and is
4
+ * tested) in the package, and regenerating never loses anything.
5
+ */
6
+ import { APP_SHELL_CSS } from "@agentvibes/site-app";
7
+ import type { SiteAppConfig } from "@agentvibes/site-config";
8
+ import { FONTS_URL, siteMediaCss, terminalFontFaceCss, terminalFontStack } from "@agentvibes/site-mdx/media";
9
+
10
+ import type { GeneratedSiteData } from "../data";
11
+ import { withBasePath, type SiteOutput } from "../output";
12
+ import { withHeader, type GeneratedFile } from "./files";
13
+
14
+ export type TemplateInput = {
15
+ /** `server`, or `static` for `next build` with `output: "export"`. */
16
+ readonly output: SiteOutput;
17
+ /** site.config `index`: a static build writes sitemap.xml and llms.txt only when it is true. */
18
+ readonly indexed: boolean;
19
+ /** Package name of the preset, when site.config names one. */
20
+ readonly presetPackage: string | undefined;
21
+ readonly generated: GeneratedSiteData;
22
+ /** CSS of the preset theme (may be empty). */
23
+ readonly themeCss: string;
24
+ /** Tailwind `@source` globs, relative to `app/`. */
25
+ readonly tailwindSources: readonly string[];
26
+ /** Packages Next must compile (workspace packages that ship TypeScript). */
27
+ readonly transpilePackages: readonly string[];
28
+ readonly docs: boolean;
29
+ readonly jsdoc: boolean;
30
+ readonly changelog: boolean;
31
+ /** The site's icon (its SVG, or the default letter mark); undefined when the author's `app/` has its own icon. */
32
+ readonly icon: string | undefined;
33
+ /** `/favicon.ico` (base64), rendered from `icon`; undefined when the author serves their own favicon. */
34
+ readonly favicon: string | undefined;
35
+ /** The site has `site.entries.ts`: the runtime passes its search documents and llms.txt text to the routes. */
36
+ readonly authorEntries: boolean;
37
+ /** `app` from site.config (spec §5); undefined for a docs site. */
38
+ readonly app: SiteAppConfig | undefined;
39
+ /** kind "app": the author writes `app/page.tsx` (the app's home); prepare does not generate it. */
40
+ readonly authorHome: boolean;
41
+ /** The preset's mono font stack, the terminal font's fallback. */
42
+ readonly monoFallback: string | undefined;
43
+ /** `sections.media.tui.frames` (a glob with one `*`, relative to the site), for `<TerminalFrame name>`. */
44
+ readonly framesGlob: string | undefined;
45
+ /** site.config `lang`: the `<html lang>` of the global error page, which replaces the root layout. */
46
+ readonly lang: string;
47
+ /** The site has `site.next.mjs`: next.config passes its config through that file's default export. */
48
+ readonly authorNextConfig: boolean;
49
+ /** The site has `site.css`: globals.css imports it after the theme. */
50
+ readonly authorCss: boolean;
51
+ /** The site has `site.proxy.ts`: the generated proxy runs its default export first. */
52
+ readonly authorProxy: boolean;
53
+ /** The site has `site.chrome.tsx`: the generated runtime passes its default export. */
54
+ readonly authorChrome: boolean;
55
+ };
56
+
57
+ /**
58
+ * Optional files the site author owns, for what site.config does not
59
+ * describe (workspace packages to compile, module aliases, redirects, the
60
+ * site's own component styles).
61
+ */
62
+ export const AUTHOR_NEXT_CONFIG = "site.next.mjs";
63
+ export const AUTHOR_CSS = "site.css";
64
+ /** Optional author file: search documents and llms.txt text beyond the MDX pages (`defineSiteEntries`). */
65
+ export const AUTHOR_ENTRIES = "site.entries.ts";
66
+ /** Optional author file: a guard run before every request (`SiteGuard`), for a login gate. */
67
+ export const AUTHOR_PROXY = "site.proxy.ts";
68
+ /** Optional author file: the site's own header title, footer and theme storage key (`defineSiteChrome`). */
69
+ export const AUTHOR_CHROME = "site.chrome.tsx";
70
+
71
+ const DYNAMIC = `// Rendered per request: indexing, robots and telemetry depend on the request host (spec §4).\nexport const dynamic = "force-dynamic";\n`;
72
+ const STATIC = `// Static output: rendered once at build time, for the origin given to prepare --static.\nexport const dynamic = "force-static";\n`;
73
+
74
+ function rendering(input: TemplateInput): string {
75
+ return input.output.kind === "static" ? STATIC : DYNAMIC;
76
+ }
77
+
78
+ function ts(path: string, body: string): GeneratedFile {
79
+ return { path, content: withHeader("slash", body) };
80
+ }
81
+
82
+ function runtimeFile(input: TemplateInput): GeneratedFile {
83
+ const presetImport =
84
+ input.presetPackage === undefined ? `const preset = undefined;` : `import preset from ${JSON.stringify(input.presetPackage)};`;
85
+ const entriesImport = input.authorEntries ? `import entries from "../../${AUTHOR_ENTRIES.replace(/\.ts$/, "")}";\n` : "";
86
+ const entriesField = input.authorEntries ? ",\n entries" : "";
87
+ const chromeImport = input.authorChrome ? `import chrome from "../../${AUTHOR_CHROME.replace(/\.tsx$/, "")}";\n` : "";
88
+ const chromeField = input.authorChrome ? ",\n chrome" : "";
89
+ return ts(
90
+ "app/_site/runtime.ts",
91
+ `import { createSiteRuntime, loadConfig, siteSource } from "@agentvibes/site";
92
+ import { loader } from "fumadocs-core/source";
93
+
94
+ ${presetImport}
95
+ import { docs, pages } from "../../.source/server";
96
+ ${entriesImport}${chromeImport}import rawConfig from "../../site.config";
97
+ import generated from "./generated";
98
+
99
+ export const site = createSiteRuntime({
100
+ config: loadConfig(rawConfig),
101
+ preset,
102
+ generated,
103
+ docs: siteSource(loader({ baseUrl: "/docs", source: docs.toFumadocsSource() })),
104
+ pages: siteSource(loader({ baseUrl: "/", source: pages.toFumadocsSource() }))${entriesField}${chromeField}
105
+ });
106
+ `
107
+ );
108
+ }
109
+
110
+ function generatedFile(data: GeneratedSiteData): GeneratedFile {
111
+ return ts(
112
+ "app/_site/generated.ts",
113
+ `import type { GeneratedSiteData } from "@agentvibes/site";
114
+
115
+ const generated: GeneratedSiteData = ${JSON.stringify(data, null, 2)};
116
+
117
+ export default generated;
118
+ `
119
+ );
120
+ }
121
+
122
+ function pageFile(input: TemplateInput, path: string, factory: string, withMetadata: boolean): GeneratedFile {
123
+ const metadata = withMetadata ? `export const generateMetadata = page.generateMetadata;\nexport const viewport = page.viewport;\n` : "";
124
+ const params = input.output.kind === "static" && path.includes("[") ? `export const dynamicParams = false;\nexport const generateStaticParams = page.generateStaticParams;\n` : "";
125
+ return ts(
126
+ path,
127
+ `import { ${factory} } from "@agentvibes/site";
128
+
129
+ import { site } from "${"../".repeat(path.split("/").length - 2) || "./"}_site/runtime";
130
+
131
+ ${rendering(input)}
132
+ const page = ${factory}(site);
133
+ ${metadata}${params}export default page.Page;
134
+ `
135
+ );
136
+ }
137
+
138
+ function routeFile(input: TemplateInput, path: string, factory: string, methods: readonly string[], paramsFactory?: string): GeneratedFile {
139
+ const depth = path.split("/").length - 2;
140
+ const imports = paramsFactory === undefined ? factory : `${factory}, ${paramsFactory}`;
141
+ const params = paramsFactory === undefined ? "" : `export const dynamicParams = false;\nexport const generateStaticParams = ${paramsFactory}(site);\n`;
142
+ const exports = methods.map((method) => `export const ${method} = handler;`).join("\n");
143
+ return ts(
144
+ path,
145
+ `import { ${imports} } from "@agentvibes/site";
146
+
147
+ import { site } from "${"../".repeat(depth)}_site/runtime";
148
+
149
+ ${rendering(input)}
150
+ const handler = ${factory}(site);
151
+ ${params}${exports}
152
+ `
153
+ );
154
+ }
155
+
156
+ /** Routes that answer per request: the host-dependent lists, the Markdown and OG routes, the telemetry tunnels, doctor's probes. */
157
+ function serverOnlyRoutes(input: TemplateInput): GeneratedFile[] {
158
+ return [
159
+ routeFile(input, "app/sitemap.xml/route.ts", "createSitemapRoute", ["GET"]),
160
+ routeFile(input, "app/llms.txt/route.ts", "createLlmsRoute", ["GET"]),
161
+ routeFile(input, "app/llms.mdx/[[...slug]]/route.ts", "createMarkdownRoute", ["GET"]),
162
+ routeFile(input, "app/og/[[...slug]]/route.ts", "createOgRoute", ["GET"]),
163
+ // `%5F` is an escaped "_": Next treats `_name` folders as private.
164
+ routeFile(input, "app/%5Fa/[...path]/route.ts", "createAnalyticsTunnel", ["GET", "POST"]),
165
+ routeFile(input, "app/%5Fe/route.ts", "createErrorsTunnel", ["POST"]),
166
+ // site doctor's errors-captured probes: 404 unless the server runs with AGENTVIBES_DOCTOR_ROUTES=1.
167
+ ts(
168
+ "app/%5Fdoctor/throw/route.ts",
169
+ `import { createDoctorThrowRoute } from "@agentvibes/site";\n\n${DYNAMIC}export const GET = createDoctorThrowRoute();\n`
170
+ ),
171
+ ts(
172
+ "app/%5Fdoctor/throw-client/page.tsx",
173
+ `import { createDoctorThrowPage } from "@agentvibes/site";\n\n${DYNAMIC}export default createDoctorThrowPage();\n`
174
+ )
175
+ ];
176
+ }
177
+
178
+ /**
179
+ * The same content as files. Every path ends in an extension, because Next
180
+ * writes a route's output to a file named after its path, and `/docs` next to
181
+ * `/docs/init` would need `docs` to be a file and a directory at once.
182
+ */
183
+ function staticRoutes(input: TemplateInput): GeneratedFile[] {
184
+ return [
185
+ ...(input.indexed
186
+ ? [routeFile(input, "app/sitemap.xml/route.ts", "createSitemapRoute", ["GET"]), routeFile(input, "app/llms.txt/route.ts", "createLlmsRoute", ["GET"])]
187
+ : []),
188
+ routeFile(input, "app/llms.mdx/[...slug]/route.ts", "createMarkdownRoute", ["GET"], "createMarkdownParams"),
189
+ routeFile(input, "app/og/[...slug]/route.ts", "createOgRoute", ["GET"], "createOgParams")
190
+ ];
191
+ }
192
+
193
+ function appFiles(input: TemplateInput): GeneratedFile[] {
194
+ const files: GeneratedFile[] = [
195
+ runtimeFile(input),
196
+ generatedFile(input.generated),
197
+ ts(
198
+ "app/layout.tsx",
199
+ `import "./globals.css";
200
+
201
+ import { createRootLayout } from "@agentvibes/site";
202
+
203
+ import { site } from "./_site/runtime";
204
+
205
+ ${rendering(input)}
206
+ const root = createRootLayout(site);
207
+ export const generateMetadata = root.generateMetadata;
208
+ export const viewport = root.viewport;
209
+ export default root.RootLayout;
210
+ `
211
+ ),
212
+ ts(
213
+ "app/not-found.tsx",
214
+ `import { createNotFoundPage } from "@agentvibes/site";
215
+
216
+ import { site } from "./_site/runtime";
217
+
218
+ export default createNotFoundPage(site);
219
+ `
220
+ ),
221
+ ts("app/error.tsx", `"use client";\n\nimport { SiteErrorBoundary } from "@agentvibes/site/client";\n\nexport default SiteErrorBoundary;\n`),
222
+ ts(
223
+ "app/global-error.tsx",
224
+ `"use client";\n\nimport { SiteGlobalErrorBoundary } from "@agentvibes/site/client";\n\nexport default function GlobalError(props: Parameters<typeof SiteGlobalErrorBoundary>[0]) {\n return <SiteGlobalErrorBoundary {...props} lang={${JSON.stringify(input.lang)}} />;\n}\n`
225
+ ),
226
+ {
227
+ path: "app/globals.css",
228
+ content: withHeader(
229
+ "css",
230
+ [
231
+ `@import "tailwindcss";`,
232
+ `@import "fumadocs-ui/css/neutral.css";`,
233
+ `@import "fumadocs-ui/css/preset.css";`,
234
+ "",
235
+ ...input.tailwindSources.map((glob) => `@source ${JSON.stringify(glob)};`),
236
+ "",
237
+ input.themeCss,
238
+ terminalFontFaceCss(withBasePath(input.output, FONTS_URL)),
239
+ // Code blocks use the served terminal font too, so site doctor's fonts-coverage can check every glyph.
240
+ `:root {\n --font-mono: ${terminalFontStack(input.monoFallback)};\n}\n`,
241
+ siteMediaCss(input.monoFallback),
242
+ ...(input.app?.fullscreen === true ? [APP_SHELL_CSS] : []),
243
+ // The site's own styles, last so they win over the defaults above.
244
+ ...(input.authorCss ? [`@import "../${AUTHOR_CSS}";\n`] : [])
245
+ ].join("\n")
246
+ )
247
+ },
248
+ routeFile(input, "app/robots.txt/route.ts", "createRobotsRoute", ["GET"]),
249
+ ...(input.output.kind === "server" ? serverOnlyRoutes(input) : staticRoutes(input))
250
+ ];
251
+ if (!input.authorHome) files.push(pageFile(input, "app/page.tsx", "createLandingPage", true));
252
+ files.push(...appModeFiles(input));
253
+ // The search handler is an object of methods, not a function.
254
+ files.push(ts(
255
+ "app/api/search/route.ts",
256
+ `import { createSearchRoute } from "@agentvibes/site";
257
+
258
+ import { site } from "../../_site/runtime";
259
+
260
+ ${rendering(input)}
261
+ export const { ${input.output.kind === "static" ? "staticGET: GET" : "GET"} } = createSearchRoute(site);
262
+ `
263
+ ));
264
+ if (input.docs) {
265
+ files.push(
266
+ ts(
267
+ "app/docs/layout.tsx",
268
+ `import { createDocsLayout } from "@agentvibes/site";
269
+
270
+ import { site } from "../_site/runtime";
271
+
272
+ const layout = createDocsLayout(site);
273
+ export const viewport = layout.viewport;
274
+ export default layout.Layout;
275
+ `
276
+ ),
277
+ pageFile(input, "app/docs/[[...slug]]/page.tsx", "createDocsPage", true)
278
+ );
279
+ }
280
+ if (input.docs && input.jsdoc) files.push(pageFile(input, "app/docs/[moduleId]/jsdoc/page.tsx", "createJsdocPage", true));
281
+ if (input.changelog) files.push(pageFile(input, "app/changelog/page.tsx", "createChangelogPage", true));
282
+ if (input.icon !== undefined) {
283
+ const svg = input.icon.replace(/^\s*(<\?xml[^>]*\?>\s*)?/, "");
284
+ files.push({ path: "app/icon.svg", content: withHeader("xml", svg) });
285
+ }
286
+ if (input.favicon !== undefined) {
287
+ files.push(
288
+ ts(
289
+ "app/favicon.ico/route.ts",
290
+ `import { createFaviconRoute } from "@agentvibes/site";\n\n// The bytes of /favicon.ico (16, 32 and 48 px), rendered from app/icon.svg.\nexport const dynamic = "force-static";\nexport const GET = createFaviconRoute(${JSON.stringify(input.favicon)});\n`
291
+ )
292
+ );
293
+ }
294
+ return files;
295
+ }
296
+
297
+ /** `app.pwa` → the manifest; `app.pwa.offline` → the service worker and the offline page. */
298
+ function appModeFiles(input: TemplateInput): GeneratedFile[] {
299
+ const pwa = input.app?.pwa;
300
+ if (pwa === undefined) return [];
301
+ const files = [
302
+ ts(
303
+ "app/manifest.ts",
304
+ `import { createManifest } from "@agentvibes/site";\n\nimport { site } from "./_site/runtime";\n\nexport default createManifest(site);\n`
305
+ )
306
+ ];
307
+ if (pwa.offline) {
308
+ files.push(routeFile(input, "app/sw.js/route.ts", "createServiceWorkerRoute", ["GET"]), pageFile(input, "app/~offline/page.tsx", "createOfflinePage", false));
309
+ }
310
+ return files;
311
+ }
312
+
313
+ /** The fields a static export needs, applied after the author's hook so it cannot undo them. `from` names the config they extend. */
314
+ function staticNextConfig(output: Extract<SiteOutput, { kind: "static" }>, from: string): string {
315
+ const basePath = output.basePath === "" ? "" : ` basePath: ${JSON.stringify(output.basePath)},`;
316
+ return `{ ...${from}, output: "export", trailingSlash: true, images: { unoptimized: true },${basePath} env: { ...${from}.env, NEXT_PUBLIC_SITE_STATIC: "1" } }`;
317
+ }
318
+
319
+ function nextConfigFile(input: TemplateInput): GeneratedFile {
320
+ const base = `/** @type {import("next").NextConfig} */
321
+ const nextConfig = {
322
+ reactStrictMode: true,
323
+ transpilePackages: ${JSON.stringify(input.transpilePackages)}
324
+ };
325
+ `;
326
+ const extended = input.authorNextConfig ? "await extendNextConfig(nextConfig)" : "nextConfig";
327
+ const authored = input.output.kind === "static" ? `const authored = ${extended};\n` : "";
328
+ const finalConfig = input.output.kind === "static" ? staticNextConfig(input.output, "authored") : extended;
329
+ const authorImport = input.authorNextConfig ? `\nimport extendNextConfig from "./${AUTHOR_NEXT_CONFIG}";\n` : "";
330
+ const authorNote = input.authorNextConfig ? `// ${AUTHOR_NEXT_CONFIG} (the site's own file) receives the generated config and returns the one Next uses.\n` : "";
331
+ return ts(
332
+ "next.config.mjs",
333
+ `import { createMDX } from "fumadocs-mdx/next";
334
+ ${authorImport}
335
+ const withMDX = createMDX();
336
+
337
+ ${base}
338
+ ${authorNote}${authored}export default withMDX(${finalConfig});
339
+ `
340
+ );
341
+ }
342
+
343
+ /** The proxy and server instrumentation: a static host runs neither. */
344
+ function serverRootFiles(input: TemplateInput): GeneratedFile[] {
345
+ if (input.output.kind === "static") return [];
346
+ return [
347
+ ts(
348
+ "proxy.ts",
349
+ `import { createProxy, loadConfig } from "@agentvibes/site/proxy";
350
+ ${input.authorProxy ? `\nimport guard from "./${AUTHOR_PROXY.replace(/\.ts$/, "")}";` : ""}
351
+ import rawConfig from "./site.config";
352
+
353
+ export const proxy = createProxy(loadConfig(rawConfig)${input.authorProxy ? ", guard" : ""});
354
+
355
+ export const config = {
356
+ matcher: ["/((?!_next/static|_next/image|_av/|favicon.ico|icon.svg|sw.js).*)"]
357
+ };
358
+ `
359
+ ),
360
+ // Server-side errors (spec §4): Next calls onRequestError for render, route and action errors.
361
+ ts(
362
+ "instrumentation.ts",
363
+ `import { createRequestErrorHandler } from "@agentvibes/site/instrumentation";
364
+
365
+ // The runtime is loaded on the first error, and only in the Node.js runtime
366
+ // (the edge bundle must not pull in the site's content).
367
+ export const onRequestError = createRequestErrorHandler(async () => {
368
+ if (process.env.NEXT_RUNTIME === "nodejs") return (await import("./app/_site/runtime")).site;
369
+ return undefined;
370
+ });
371
+ `
372
+ )
373
+ ];
374
+ }
375
+
376
+ function rootFiles(input: TemplateInput): GeneratedFile[] {
377
+ const collection = (name: string, dir: string) => `export const ${name} = defineDocs({ dir: ${JSON.stringify(dir)} });`;
378
+ return [
379
+ ts(
380
+ "source.config.ts",
381
+ `import { remarkInlineMedia, remarkMarkdownCopy } from "@agentvibes/site/mdx-config";
382
+ import { defineConfig, defineDocs } from "fumadocs-mdx/config";
383
+
384
+ ${collection("docs", "content/docs")}
385
+
386
+ ${collection("pages", "content/pages")}
387
+
388
+ // remarkMarkdownCopy writes what page.data.getText("processed") returns
389
+ // (the .md copies, llms-full.txt), in place of includeProcessedMarkdown.
390
+ // remarkInlineMedia then inlines <Diagram src> and <TerminalFrame src|name>
391
+ // files into the page (after the copy, so the .md keeps the short src).
392
+ export default defineConfig({
393
+ mdxOptions: { remarkPlugins: [remarkMarkdownCopy, [remarkInlineMedia, ${JSON.stringify({ frames: input.framesGlob ?? null })}]] }
394
+ });
395
+ `
396
+ ),
397
+ nextConfigFile(input),
398
+ ts("postcss.config.mjs", `export default {\n plugins: {\n "@tailwindcss/postcss": {}\n }\n};\n`),
399
+ ...serverRootFiles(input)
400
+ ];
401
+ }
402
+
403
+ /** `app/.gitignore`: the generated files under app/, so only the author's own routes are committed. */
404
+ function appGitignore(files: readonly GeneratedFile[]): GeneratedFile {
405
+ const lines = files
406
+ .map((file) => file.path)
407
+ .filter((path) => path.startsWith("app/"))
408
+ .map((path) => `/${path.slice("app/".length).replace(/[[\]]/g, (c) => `\\${c}`)}`);
409
+ return { path: "app/.gitignore", content: withHeader("hash", `/.gitignore\n${lines.join("\n")}\n`) };
410
+ }
411
+
412
+ export function siteFiles(input: TemplateInput): GeneratedFile[] {
413
+ const app = appFiles(input);
414
+ return [...app, appGitignore(app), ...rootFiles(input)];
415
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Config the schema accepts but no code acts on. `prepare` refuses it, so an
3
+ * author never takes a build that silently left a declared feature out for a
4
+ * finished site. Each problem names the thing to write instead.
5
+ */
6
+ import type { SiteConfig } from "@agentvibes/site-config";
7
+
8
+ /** The extension points that do exist, for the plugin message. */
9
+ const AUTHOR_FILES = "site.next.mjs (Next config), site.css, site.entries.ts (search and llms.txt), site.chrome.tsx (header title and footer), site.proxy.ts (request guard)";
10
+
11
+ /** Messages for every declared-but-unimplemented feature in `config`; empty when there is none. */
12
+ export function unimplementedConfig(config: SiteConfig): string[] {
13
+ const problems: string[] = [];
14
+ const api = config.sections.api;
15
+ if ((api?.typescript?.length ?? 0) > 0) {
16
+ problems.push(
17
+ "sections.api.typescript is not implemented: no API pages are generated from entry files. Write the reference as an MDX page, e.g. content/docs/api.mdx (with title and description in its frontmatter). Packages listed in sections.npm.modules already get generated JSDoc pages."
18
+ );
19
+ }
20
+ if ((api?.openapi?.length ?? 0) > 0) {
21
+ problems.push("sections.api.openapi is not implemented: no OpenAPI reference is generated. Write the reference as an MDX page, e.g. content/docs/api.mdx.");
22
+ }
23
+ if (config.sections.cli !== undefined) {
24
+ problems.push("sections.cli is not implemented: no CLI reference is generated. Write it as an MDX page, e.g. content/docs/cli.mdx, with the output of your --help in a code block.");
25
+ }
26
+ if (config.plugins.length > 0) {
27
+ problems.push(
28
+ `plugins is not implemented (${config.plugins.map((plugin) => plugin.name).join(", ")}): no plugin hook runs, so a plugin would change nothing. Use the author files instead: ${AUTHOR_FILES}.`
29
+ );
30
+ }
31
+ return problems;
32
+ }