- After completion, visiting the published
docGeneratorHTTP val with a?val=username/valnamequery returns generated API docs < 3s for a representative public val. - Success criteria:
GET /api/docs?val=acme/sampleresponds 200 with parsed exports (functions, types, constants) in JSON validated againstDocManifestschema.- UI route
/renders documentation cards for the same sample val with syntax highlighting and navigation. - Cache hits logged for repeat requests within 1 hour, cache misses stay under 2 API calls per request and serve cached manifests/HTML within ~20 ms.
- Scope boundaries:
- No authentication for private vals, no editing/workspace features.
- Multi-val/project aggregation and version diffing remain out of scope for v1.
- UI polish limited to Tailwind-based theming without custom theming engine.
- Key components and responsibilities (to be created under
/backend,/frontend,/sharedas per AGENTS guidelines):- backend/docGenerator.http.ts — Planned Hono HTTP entrypoint orchestrating fetch → parse → cache → render.
- backend/services/valFetcher.ts — Planned Val Town API client resolving val metadata and source bundle.
- backend/services/tsParser.ts — Planned TypeScript compiler API walker emitting
ParsedDocumentation. - backend/services/docCache.ts — Planned blob-backed cache with memory front.
- shared/docTypes.ts — Planned shared TypeScript interfaces for parser output and UI consumption.
- frontend/index.html & frontend/App.tsx — Planned React shell rendering docs, search, and navigation.
- Data/types affected:
DocManifest,FunctionDoc,TypeDoc,ClassDoc,ConstantDoc,ImportInfo; caching metadata{ cacheKey: string; createdAt: number; version: string; }.- Request DTO
{ val: string; refresh?: boolean }, error envelope{ code: string; message: string; details?: unknown }.
- Current flow:
- Request enters Hono → query param validation.
- Cache lookup by
valIdentifier@version(memory → blob fallback). - On miss: fetch val metadata + files → parse AST → build manifest → persist cache.
- API route returns JSON; UI route hydrates React with initial manifest + fetch fallback.
- Establish shared documentation schemas and validation helpers first so backend and frontend contract is locked before logic work.
- Implement val fetching and caching seams prior to TypeScript parsing to minimize external API pressure and enable testability via fixtures.
- Build the TypeScript parser as a pure module with exhaustive unit tests, then integrate into the fetcher pipeline behind cache lookups.
- Compose the Hono HTTP val with separate API (
/api/docs) and UI (/) routes, injecting fetch/parse/cache services for easy stubbing. - Deliver the React/Twind frontend last, hydrating with server-injected manifest while supporting client-side refresh via the API.
- Assumptions:
npm:typescriptbundling works in Val Town serverless context; Val Town REST endpoints for public vals require no auth; Tailwind via Twind CDN is acceptable for styling.
Sequence logically: refactor → scaffold → integrate (flagged) → validate → cleanup.
Intent: Lock parser ↔ renderer contract and validation helpers before logic diverges. Files: shared/docTypes.ts, shared/docTypes.test.ts Change sketch:
+ export interface DocManifest {
+ val: string;
+ version: string;
+ generatedAt: string;
+ exports: {
+ functions: FunctionDoc[];
+ classes: ClassDoc[];
+ interfaces: InterfaceDoc[];
+ types: TypeAliasDoc[];
+ constants: ConstantDoc[];
+ };
+ imports: ImportInfo[];
+ metadata: ValMetadata;
+ }
+
+ export function assertDocManifest(data: unknown): asserts data is DocManifest {
+ // runtime validation leveraging type predicates or zod-like helper (decide on zod dependency vs hand-rolled guards)
+ }
Preconditions: None. Postconditions: Typed contract with runtime guard available for backend and frontend. Checks: Unit tests covering validation happy path and failure scenarios.
Intent: Provide deterministic data sources and caching hooks prior to parser integration. Files: backend/services/valFetcher.ts, backend/services/docCache.ts, backend/services/tests/valFetcher.test.ts Change sketch:
+ export interface ValFetcher {
+ fetchLatest(val: string): Promise<ValBundle>;
+ }
+
+ export const docCache = createDocCache({
+ ttlMs: 60 * 60 * 1000,
+ blobPrefix: "doc-manifest",
+ });
Preconditions: Step 1 contract finalized. Postconditions: Fetcher returns mocked data in tests; cache exposes get/set/isFresh with blob-backed implementation (no parser usage yet). Checks: Unit tests hitting Val Town API via mocked fetch; cache tests verifying TTL & key derivation.
Intent: Build pure parsing logic producing the shared manifest structures without I/O coupling. Files: backend/services/tsParser.ts, backend/services/tests/tsParser.test.ts, test-fixtures/vals/simple.ts Change sketch:
+ export function parseValBundle(bundle: ValBundle): DocManifest {
+ const program = ts.createProgram(
+ bundle.files.map((file) => file.path),
+ compilerOptions,
+ createInMemoryCompilerHost(bundle)
+ );
+ // walk AST, fill manifest.exports
+ return manifest;
+ }
Preconditions: Step 1 types available; Step 2 supplies ValBundle shape. Postconditions: Parser returns manifest for fixture bundles with deterministic ordering; no cache integration yet. Checks: Unit tests for functions, classes, interfaces, type aliases, constants, malformed inputs.
Intent: Integrate fetcher, parser, and cache into orchestrated service exposing generateDocs, caching manifests and prerendered HTML for <3s responses.
Files: backend/services/docService.ts, backend/services/tests/docService.test.ts
Change sketch:
+ export interface CachedDocPayload {
+ manifest: DocManifest;
+ ssrHtml?: string;
+ cachedAt: number;
+ }
+
+ export async function generateDocs(val: string, opts: { refresh?: boolean }) {
+ const cacheKey = deriveCacheKey(val);
+ if (!opts.refresh) {
+ const cached = await docCache.get<CachedDocPayload>(cacheKey);
+ if (cached && !docCache.isExpired(cacheKey)) return cached;
+ }
+ const bundle = await valFetcher.fetchLatest(val);
+ const manifest = parseValBundle(bundle);
+ const payload: CachedDocPayload = { manifest, cachedAt: Date.now() };
+ await docCache.set(cacheKey, payload, { version: bundle.version });
+ return payload;
+ }
Preconditions: Steps 1–3 merged. Postconditions: Single orchestration entry point with cache hit logging hook, rate-limited fetch usage; cached payload reusable for API and UI routes. Checks: Service tests mocking cache/fetcher to assert cache strategy, ttl handling, and refresh behavior.
Intent: Expose API and UI endpoints leveraging docService; ensure error handling & logging. Files: backend/docGenerator.http.ts, backend/routes/apiDocs.ts, backend/routes/ui.ts, backend/routes/tests/apiDocs.test.ts Change sketch:
+ app.get("/api/docs", async (c) => {
+ const val = c.req.query("val");
+ if (!val) return c.json({ code: "BAD_REQUEST" }, 400);
+ const { manifest } = await generateDocs(val, { refresh: c.req.query("refresh") === "true" });
+ return c.json(manifest);
+ });
+
+ app.get("/", async (c) => {
+ const initialVal = c.req.query("val");
+ const cached = initialVal ? await generateDocs(initialVal, { refresh: false }) : null;
+ if (!cached?.manifest) {
+ const emptyHtml = await renderIndexPage({ manifest: null, val: initialVal });
+ return c.html(emptyHtml);
+ }
+ if (!cached.ssrHtml) {
+ cached.ssrHtml = await renderIndexPage({ manifest: cached.manifest, val: initialVal });
+ await docCache.set(deriveCacheKey(initialVal), cached, { version: cached.manifest.version });
+ }
+ return c.html(cached.ssrHtml);
+ });
Preconditions: Step 4 ready. Postconditions: HTTP val deployed with API contract, SSR injecting initial manifest, error states handled. Checks: Integration tests via superdeno hitting API and root route; lint/typecheck.
Intent: Deliver interactive documentation UI consuming manifest and supporting client fetch refresh. Files: frontend/index.html, frontend/App.tsx, frontend/components/FunctionDoc.tsx, frontend/components/Sidebar.tsx, frontend/tests/App.test.tsx Change sketch:
+ /** @jsxImportSource https://esm.sh/react@18.2.0 */
+ import { useEffect, useState } from "https://esm.sh/react@18.2.0";
+ export function App(props: { initialManifest?: DocManifest; initialVal?: string }) {
+ const [manifest, setManifest] = useState(props.initialManifest ?? null);
+ // render header, sidebar, main, search, dark mode toggle
+ }
Preconditions: Step 5 HTML render helper able to inject initialManifest.
Postconditions: Responsive UI with search/filter, syntax highlighting via Prism/Twind, copy-to-clipboard.
Checks: Frontend unit tests with pre-rendered manifest, visual smoke test in Val Town.
Intent: Add request logging, cache hit metrics, rate limiting guard, and README instructions. Files: backend/services/logger.ts, backend/docGenerator.http.ts, README.md Change sketch:
+ app.use("/api/docs", rateLimit({ windowMs: 60_000, max: 30 }));
+ logger.info({ event: "doc_cache_hit", val, cacheKey });
+ logger.info({ event: "doc_cache_miss", val, durationMs });
+
+## Usage
+- Deploy `docGenerator` via Val Town HTTP val.
Preconditions: Core functionality stable (Steps 1–6). Postconditions: Observability and docs in place; rate limiting prevents abuse. Checks: Manual smoke tests, documentation review.
- Confirm whether we must support fetching specific val versions (e.g.,
?version=query) in v1 or default to latest only. - Clarify if Tailwind via Twind CDN is acceptable for production or if inline Tailwind build step is preferred.
- Determine acceptable rate limit thresholds and logging retention (where should logs emit?).
- Build/test:
deno task check,deno test,deno lint,biome check . - Docs: Val Town API, TypeScript Compiler API, TSDoc Spec.
- Reference vals:
https://www.val.town/v/std/utils@85-main/index.ts,https://www.val.town/v/std/blob.