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 <noreply@anthropic.com>
This commit is contained in:
Jonathan Hefner 2026-01-23 12:47:30 -06:00
commit 56a2e79bba
No known key found for this signature in database
GPG key ID: 0DA40AFD21043691
2 changed files with 66 additions and 64 deletions

View file

@ -80,7 +80,7 @@ View (App) <--PostMessageTransport--> Host (AppBridge) <--MCP Client--> MCP Serv
## Documentation ## 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. Standalone docs in `docs/` (listed in `typedoc.config.mjs` `projectDocuments`) can also have type-checked companion `.ts`/`.tsx` files using the same pattern.

View file

@ -1,17 +1,29 @@
/** /**
* Code Snippet Sync Script * Code Snippet Sync Script
* *
* This script syncs code snippets from `.examples.ts/.examples.tsx` files * This script syncs code snippets into JSDoc comments and markdown files
* into JSDoc comments containing labeled code fences. * containing labeled code fences.
* *
* The script replaces the content inside code fences that have a path#region * ## Supported Source Files
* reference in their info string. *
* - **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 * ## 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 * ``````typescript
* ```ts source="./path.examples.ts#regionName" * ```ts source="./path.examples.ts#regionName"
* // code is synced here * // region content is synced here
* ``` * ```
* `````` * ``````
* *
@ -55,10 +67,10 @@ interface LabeledCodeFence {
displayName?: string; displayName?: string;
/** Relative path to the example file (e.g., "./app.examples.ts") */ /** Relative path to the example file (e.g., "./app.examples.ts") */
examplePath: string; examplePath: string;
/** Region name (e.g., "App_basicUsage") */ /** Region name (e.g., "App_basicUsage"), or undefined for whole file */
regionName: string; regionName?: string;
/** Language from the code fence (ts or tsx) */ /** Language from the code fence (e.g., "ts", "json", "yaml") */
language: "ts" | "tsx"; language: string;
/** Character index of the opening fence line start */ /** Character index of the opening fence line start */
openingFenceStart: number; openingFenceStart: number;
/** Character index after the opening fence line (after newline) */ /** Character index after the opening fence line (after newline) */
@ -69,22 +81,12 @@ interface LabeledCodeFence {
linePrefix: string; 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. * Cache for example file regions to avoid re-reading files.
* Key: absolute example file path * Key: `${absoluteExamplePath}#${regionName}` (empty regionName for whole file)
* Value: Map<regionName, RegionContent> * Value: extracted code string
*/ */
type RegionCache = Map<string, Map<string, RegionContent>>; type RegionCache = Map<string, string>;
/** /**
* Processing result for a source file. * Processing result for a source file.
@ -97,18 +99,20 @@ interface FileProcessingResult {
} }
// JSDoc patterns - for code fences inside JSDoc comments with " * " prefix // JSDoc patterns - for code fences inside JSDoc comments with " * " prefix
// Matches: <prefix>```<lang> [displayName] source="<path>#<region>" // Matches: <prefix>```<lang> [displayName] source="<path>" or source="<path>#<region>"
// Example: " * ```ts my-app.ts source="./app.examples.ts#App_basicUsage"" // Example: " * ```ts my-app.ts source="./app.examples.ts#App_basicUsage""
// Example: " * ```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 = 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*$/; const JSDOC_CLOSING_FENCE_PATTERN = /^(\s*\*\s*)```\s*$/;
// Markdown patterns - for plain code fences in markdown files (no prefix) // Markdown patterns - for plain code fences in markdown files (no prefix)
// Matches: ```<lang> [displayName] source="<path>#<region>" // Matches: ```<lang> [displayName] source="<path>" or source="<path>#<region>"
// Example: ```tsx source="./patterns.tsx#chunkedDataServer" // Example: ```tsx source="./patterns.tsx#chunkedDataServer"
// Example: ```tsx source="./complete-example.tsx" (whole file)
const MARKDOWN_LABELED_FENCE_PATTERN = const MARKDOWN_LABELED_FENCE_PATTERN =
/^```(ts|tsx)(?:\s+(\S+))?\s+source="([^"#]+)#([^"]+)"/; /^```(\w+)(?:\s+(\S+))?\s+source="([^"#]+)(?:#([^"]+))?"/;
const MARKDOWN_CLOSING_FENCE_PATTERN = /^```\s*$/; const MARKDOWN_CLOSING_FENCE_PATTERN = /^```\s*$/;
/** /**
@ -184,7 +188,7 @@ function findLabeledCodeFences(
displayName, displayName,
examplePath, examplePath,
regionName, regionName,
language: language as "ts" | "tsx", language,
openingFenceStart, openingFenceStart,
openingFenceEnd, openingFenceEnd,
closingFenceStart, closingFenceStart,
@ -242,6 +246,14 @@ function extractRegion(
regionName: string, regionName: string,
examplePath: string, examplePath: string,
): 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 regionStart = `//#region ${regionName}`;
const regionEnd = `//#endregion ${regionName}`; const regionEnd = `//#endregion ${regionName}`;
@ -281,54 +293,47 @@ function extractRegion(
* Get or load a region from the cache. * Get or load a region from the cache.
* @param sourceFilePath The source file requesting the region * @param sourceFilePath The source file requesting the region
* @param examplePath The relative path to the example file * @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 * @param cache The region cache
* @returns The region content * @returns The extracted code string
*/ */
function getOrLoadRegion( function getOrLoadRegion(
sourceFilePath: string, sourceFilePath: string,
examplePath: string, examplePath: string,
regionName: string, regionName: string | undefined,
cache: RegionCache, cache: RegionCache,
): RegionContent { ): string {
// Resolve the example path relative to the source file // Resolve the example path relative to the source file
const sourceDir = dirname(sourceFilePath); const sourceDir = dirname(sourceFilePath);
const absoluteExamplePath = resolve(sourceDir, examplePath); const absoluteExamplePath = resolve(sourceDir, examplePath);
// Check cache first // File content is always cached with key ending in "#" (empty region)
let fileCache = cache.get(absoluteExamplePath); const fileKey = `${absoluteExamplePath}#`;
if (fileCache) { let fileContent = cache.get(fileKey);
const cached = fileCache.get(regionName);
if (cached) { if (fileContent === undefined) {
return cached; try {
fileContent = readFileSync(absoluteExamplePath, "utf-8").trim();
} catch {
throw new Error(`Example file not found: ${absoluteExamplePath}`);
} }
cache.set(fileKey, fileContent);
} }
// Load the example file // If no region name, return whole file
let exampleContent: string; if (!regionName) {
try { return fileContent;
exampleContent = readFileSync(absoluteExamplePath, "utf-8");
} catch {
throw new Error(`Example file not found: ${absoluteExamplePath}`);
} }
// Initialize file cache if needed // Extract region from cached file content, cache the result
if (!fileCache) { const regionKey = `${absoluteExamplePath}#${regionName}`;
fileCache = new Map(); let regionContent = cache.get(regionKey);
cache.set(absoluteExamplePath, fileCache);
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; return regionContent;
} }
@ -393,17 +398,14 @@ function processFile(
const fence = fences[i]; const fence = fences[i];
try { try {
const regionContent = getOrLoadRegion( const code = getOrLoadRegion(
filePath, filePath,
fence.examplePath, fence.examplePath,
fence.regionName, fence.regionName,
cache, cache,
); );
const formattedCode = formatCodeLines( const formattedCode = formatCodeLines(code, fence.linePrefix);
regionContent.code,
fence.linePrefix,
);
// Replace content between opening fence end and closing fence start // Replace content between opening fence end and closing fence start
content = content =