From 56a2e79bbaf1a54ebca7c7cc5e7cf6cb15affb42 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Fri, 23 Jan 2026 12:47:30 -0600 Subject: [PATCH] Support full-file inclusion in synced code snippets Previously, the script only supported extracting regions from `.ts/.tsx` files using the `#regionName` syntax. Now it also supports including entire files without a region specifier, enabling sync of any file type (JSON, YAML, shell scripts, etc.) into documentation. Changes: - Make `#regionName` optional in the `source=""` attribute - Accept any fence language (not just `ts`/`tsx`) - Simplify cache to flat map with composite keys - Add validation error for region extraction on non-TypeScript files Co-Authored-By: Claude Opus 4.5 --- AGENTS.md | 2 +- scripts/sync-snippets.ts | 128 ++++++++++++++++++++------------------- 2 files changed, 66 insertions(+), 64 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ae8903a..9bbd2f0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -80,7 +80,7 @@ View (App) <--PostMessageTransport--> Host (AppBridge) <--MCP Client--> MCP Serv ## Documentation -JSDoc `@example` tags should pull type-checked code from companion `.examples.ts` files (e.g., `app.ts` → `app.examples.ts`). Use ` ```ts source="./file.examples.ts#regionName" ` fences referencing `//#region regionName` blocks, then run `npm run sync:snippets`. Region names follow `exportedName_variant` or `ClassName_methodName_variant` pattern (e.g., `useApp_basicUsage`, `App_hostCapabilities_checkAfterConnection`). +JSDoc `@example` tags should pull type-checked code from companion `.examples.ts` files (e.g., `app.ts` → `app.examples.ts`). Use ` ```ts source="./file.examples.ts#regionName" ` fences referencing `//#region regionName` blocks; region names follow `exportedName_variant` or `ClassName_methodName_variant` pattern (e.g., `useApp_basicUsage`, `App_hostCapabilities_checkAfterConnection`). For whole-file inclusion (any file type), omit the `#regionName`. Run `npm run sync:snippets` to sync. Standalone docs in `docs/` (listed in `typedoc.config.mjs` `projectDocuments`) can also have type-checked companion `.ts`/`.tsx` files using the same pattern. diff --git a/scripts/sync-snippets.ts b/scripts/sync-snippets.ts index 3933727..4e9cfa8 100644 --- a/scripts/sync-snippets.ts +++ b/scripts/sync-snippets.ts @@ -1,17 +1,29 @@ /** * Code Snippet Sync Script * - * This script syncs code snippets from `.examples.ts/.examples.tsx` files - * into JSDoc comments containing labeled code fences. + * This script syncs code snippets into JSDoc comments and markdown files + * containing labeled code fences. * - * The script replaces the content inside code fences that have a path#region - * reference in their info string. + * ## Supported Source Files + * + * - **Full-file inclusion**: Any file type (e.g., `.json`, `.yaml`, `.sh`, `.ts`) + * - **Region extraction**: Only `.ts` and `.tsx` files (using `//#region` markers) * * ## Code Fence Format * + * Full-file inclusion (any file type): + * + * ``````typescript + * ```json source="./config.json" + * // entire file content is synced here + * ``` + * `````` + * + * Region extraction (.ts/.tsx only): + * * ``````typescript * ```ts source="./path.examples.ts#regionName" - * // code is synced here + * // region content is synced here * ``` * `````` * @@ -55,10 +67,10 @@ interface LabeledCodeFence { displayName?: string; /** Relative path to the example file (e.g., "./app.examples.ts") */ examplePath: string; - /** Region name (e.g., "App_basicUsage") */ - regionName: string; - /** Language from the code fence (ts or tsx) */ - language: "ts" | "tsx"; + /** Region name (e.g., "App_basicUsage"), or undefined for whole file */ + regionName?: string; + /** Language from the code fence (e.g., "ts", "json", "yaml") */ + language: string; /** Character index of the opening fence line start */ openingFenceStart: number; /** Character index after the opening fence line (after newline) */ @@ -69,22 +81,12 @@ interface LabeledCodeFence { linePrefix: string; } -/** - * Represents extracted region content from an example file. - */ -interface RegionContent { - /** The dedented code content */ - code: string; - /** Language for code fence (ts or tsx) */ - language: "ts" | "tsx"; -} - /** * Cache for example file regions to avoid re-reading files. - * Key: absolute example file path - * Value: Map + * Key: `${absoluteExamplePath}#${regionName}` (empty regionName for whole file) + * Value: extracted code string */ -type RegionCache = Map>; +type RegionCache = Map; /** * Processing result for a source file. @@ -97,18 +99,20 @@ interface FileProcessingResult { } // JSDoc patterns - for code fences inside JSDoc comments with " * " prefix -// Matches: ``` [displayName] source="#" +// Matches: ``` [displayName] source="" or source="#" // Example: " * ```ts my-app.ts source="./app.examples.ts#App_basicUsage"" // Example: " * ```ts source="./app.examples.ts#App_basicUsage"" +// Example: " * ```ts source="./complete-example.ts"" (whole file) const JSDOC_LABELED_FENCE_PATTERN = - /^(\s*\*\s*)```(ts|tsx)(?:\s+(\S+))?\s+source="([^"#]+)#([^"]+)"/; + /^(\s*\*\s*)```(\w+)(?:\s+(\S+))?\s+source="([^"#]+)(?:#([^"]+))?"/; const JSDOC_CLOSING_FENCE_PATTERN = /^(\s*\*\s*)```\s*$/; // Markdown patterns - for plain code fences in markdown files (no prefix) -// Matches: ``` [displayName] source="#" +// Matches: ``` [displayName] source="" or source="#" // Example: ```tsx source="./patterns.tsx#chunkedDataServer" +// Example: ```tsx source="./complete-example.tsx" (whole file) const MARKDOWN_LABELED_FENCE_PATTERN = - /^```(ts|tsx)(?:\s+(\S+))?\s+source="([^"#]+)#([^"]+)"/; + /^```(\w+)(?:\s+(\S+))?\s+source="([^"#]+)(?:#([^"]+))?"/; const MARKDOWN_CLOSING_FENCE_PATTERN = /^```\s*$/; /** @@ -184,7 +188,7 @@ function findLabeledCodeFences( displayName, examplePath, regionName, - language: language as "ts" | "tsx", + language, openingFenceStart, openingFenceEnd, closingFenceStart, @@ -242,6 +246,14 @@ function extractRegion( regionName: string, examplePath: string, ): string { + // Region extraction only supported for .ts/.tsx files (uses //#region syntax) + if (!examplePath.endsWith(".ts") && !examplePath.endsWith(".tsx")) { + throw new Error( + `Region extraction (#${regionName}) is only supported for .ts/.tsx files. ` + + `Use full-file inclusion (without #regionName) for: ${examplePath}`, + ); + } + const regionStart = `//#region ${regionName}`; const regionEnd = `//#endregion ${regionName}`; @@ -281,54 +293,47 @@ function extractRegion( * Get or load a region from the cache. * @param sourceFilePath The source file requesting the region * @param examplePath The relative path to the example file - * @param regionName The region name to extract + * @param regionName The region name to extract, or undefined for whole file * @param cache The region cache - * @returns The region content + * @returns The extracted code string */ function getOrLoadRegion( sourceFilePath: string, examplePath: string, - regionName: string, + regionName: string | undefined, cache: RegionCache, -): RegionContent { +): string { // Resolve the example path relative to the source file const sourceDir = dirname(sourceFilePath); const absoluteExamplePath = resolve(sourceDir, examplePath); - // Check cache first - let fileCache = cache.get(absoluteExamplePath); - if (fileCache) { - const cached = fileCache.get(regionName); - if (cached) { - return cached; + // File content is always cached with key ending in "#" (empty region) + const fileKey = `${absoluteExamplePath}#`; + let fileContent = cache.get(fileKey); + + if (fileContent === undefined) { + try { + fileContent = readFileSync(absoluteExamplePath, "utf-8").trim(); + } catch { + throw new Error(`Example file not found: ${absoluteExamplePath}`); } + cache.set(fileKey, fileContent); } - // Load the example file - let exampleContent: string; - try { - exampleContent = readFileSync(absoluteExamplePath, "utf-8"); - } catch { - throw new Error(`Example file not found: ${absoluteExamplePath}`); + // If no region name, return whole file + if (!regionName) { + return fileContent; } - // Initialize file cache if needed - if (!fileCache) { - fileCache = new Map(); - cache.set(absoluteExamplePath, fileCache); + // Extract region from cached file content, cache the result + const regionKey = `${absoluteExamplePath}#${regionName}`; + let regionContent = cache.get(regionKey); + + if (regionContent === undefined) { + regionContent = extractRegion(fileContent, regionName, examplePath); + cache.set(regionKey, regionContent); } - // Determine language from file extension - const language: "ts" | "tsx" = absoluteExamplePath.endsWith(".tsx") - ? "tsx" - : "ts"; - - // Extract the region - const code = extractRegion(exampleContent, regionName, examplePath); - - const regionContent: RegionContent = { code, language }; - fileCache.set(regionName, regionContent); - return regionContent; } @@ -393,17 +398,14 @@ function processFile( const fence = fences[i]; try { - const regionContent = getOrLoadRegion( + const code = getOrLoadRegion( filePath, fence.examplePath, fence.regionName, cache, ); - const formattedCode = formatCodeLines( - regionContent.code, - fence.linePrefix, - ); + const formattedCode = formatCodeLines(code, fence.linePrefix); // Replace content between opening fence end and closing fence start content =