Skip to content

Commit 381521b

Browse files
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>
1 parent b197935 commit 381521b

2 files changed

Lines changed: 66 additions & 64 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ View (App) <--PostMessageTransport--> Host (AppBridge) <--MCP Client--> MCP Serv
8080

8181
## Documentation
8282

83-
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`).
83+
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.
8484

8585
Standalone docs in `docs/` (listed in `typedoc.config.mjs` `projectDocuments`) can also have type-checked companion `.ts`/`.tsx` files using the same pattern.
8686

scripts/sync-snippets.ts

Lines changed: 65 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
/**
22
* Code Snippet Sync Script
33
*
4-
* This script syncs code snippets from `.examples.ts/.examples.tsx` files
5-
* into JSDoc comments containing labeled code fences.
4+
* This script syncs code snippets into JSDoc comments and markdown files
5+
* containing labeled code fences.
66
*
7-
* The script replaces the content inside code fences that have a path#region
8-
* reference in their info string.
7+
* ## Supported Source Files
8+
*
9+
* - **Full-file inclusion**: Any file type (e.g., `.json`, `.yaml`, `.sh`, `.ts`)
10+
* - **Region extraction**: Only `.ts` and `.tsx` files (using `//#region` markers)
911
*
1012
* ## Code Fence Format
1113
*
14+
* Full-file inclusion (any file type):
15+
*
16+
* ``````typescript
17+
* ```json source="./config.json"
18+
* // entire file content is synced here
19+
* ```
20+
* ``````
21+
*
22+
* Region extraction (.ts/.tsx only):
23+
*
1224
* ``````typescript
1325
* ```ts source="./path.examples.ts#regionName"
14-
* // code is synced here
26+
* // region content is synced here
1527
* ```
1628
* ``````
1729
*
@@ -55,10 +67,10 @@ interface LabeledCodeFence {
5567
displayName?: string;
5668
/** Relative path to the example file (e.g., "./app.examples.ts") */
5769
examplePath: string;
58-
/** Region name (e.g., "App_basicUsage") */
59-
regionName: string;
60-
/** Language from the code fence (ts or tsx) */
61-
language: "ts" | "tsx";
70+
/** Region name (e.g., "App_basicUsage"), or undefined for whole file */
71+
regionName?: string;
72+
/** Language from the code fence (e.g., "ts", "json", "yaml") */
73+
language: string;
6274
/** Character index of the opening fence line start */
6375
openingFenceStart: number;
6476
/** Character index after the opening fence line (after newline) */
@@ -69,22 +81,12 @@ interface LabeledCodeFence {
6981
linePrefix: string;
7082
}
7183

72-
/**
73-
* Represents extracted region content from an example file.
74-
*/
75-
interface RegionContent {
76-
/** The dedented code content */
77-
code: string;
78-
/** Language for code fence (ts or tsx) */
79-
language: "ts" | "tsx";
80-
}
81-
8284
/**
8385
* Cache for example file regions to avoid re-reading files.
84-
* Key: absolute example file path
85-
* Value: Map<regionName, RegionContent>
86+
* Key: `${absoluteExamplePath}#${regionName}` (empty regionName for whole file)
87+
* Value: extracted code string
8688
*/
87-
type RegionCache = Map<string, Map<string, RegionContent>>;
89+
type RegionCache = Map<string, string>;
8890

8991
/**
9092
* Processing result for a source file.
@@ -97,18 +99,20 @@ interface FileProcessingResult {
9799
}
98100

99101
// JSDoc patterns - for code fences inside JSDoc comments with " * " prefix
100-
// Matches: <prefix>```<lang> [displayName] source="<path>#<region>"
102+
// Matches: <prefix>```<lang> [displayName] source="<path>" or source="<path>#<region>"
101103
// Example: " * ```ts my-app.ts source="./app.examples.ts#App_basicUsage""
102104
// Example: " * ```ts source="./app.examples.ts#App_basicUsage""
105+
// Example: " * ```ts source="./complete-example.ts"" (whole file)
103106
const JSDOC_LABELED_FENCE_PATTERN =
104-
/^(\s*\*\s*)```(ts|tsx)(?:\s+(\S+))?\s+source="([^"#]+)#([^"]+)"/;
107+
/^(\s*\*\s*)```(\w+)(?:\s+(\S+))?\s+source="([^"#]+)(?:#([^"]+))?"/;
105108
const JSDOC_CLOSING_FENCE_PATTERN = /^(\s*\*\s*)```\s*$/;
106109

107110
// Markdown patterns - for plain code fences in markdown files (no prefix)
108-
// Matches: ```<lang> [displayName] source="<path>#<region>"
111+
// Matches: ```<lang> [displayName] source="<path>" or source="<path>#<region>"
109112
// Example: ```tsx source="./patterns.tsx#chunkedDataServer"
113+
// Example: ```tsx source="./complete-example.tsx" (whole file)
110114
const MARKDOWN_LABELED_FENCE_PATTERN =
111-
/^```(ts|tsx)(?:\s+(\S+))?\s+source="([^"#]+)#([^"]+)"/;
115+
/^```(\w+)(?:\s+(\S+))?\s+source="([^"#]+)(?:#([^"]+))?"/;
112116
const MARKDOWN_CLOSING_FENCE_PATTERN = /^```\s*$/;
113117

114118
/**
@@ -184,7 +188,7 @@ function findLabeledCodeFences(
184188
displayName,
185189
examplePath,
186190
regionName,
187-
language: language as "ts" | "tsx",
191+
language,
188192
openingFenceStart,
189193
openingFenceEnd,
190194
closingFenceStart,
@@ -242,6 +246,14 @@ function extractRegion(
242246
regionName: string,
243247
examplePath: string,
244248
): string {
249+
// Region extraction only supported for .ts/.tsx files (uses //#region syntax)
250+
if (!examplePath.endsWith(".ts") && !examplePath.endsWith(".tsx")) {
251+
throw new Error(
252+
`Region extraction (#${regionName}) is only supported for .ts/.tsx files. ` +
253+
`Use full-file inclusion (without #regionName) for: ${examplePath}`,
254+
);
255+
}
256+
245257
const regionStart = `//#region ${regionName}`;
246258
const regionEnd = `//#endregion ${regionName}`;
247259

@@ -281,53 +293,46 @@ function extractRegion(
281293
* Get or load a region from the cache.
282294
* @param sourceFilePath The source file requesting the region
283295
* @param examplePath The relative path to the example file
284-
* @param regionName The region name to extract
296+
* @param regionName The region name to extract, or undefined for whole file
285297
* @param cache The region cache
286-
* @returns The region content
298+
* @returns The extracted code string
287299
*/
288300
function getOrLoadRegion(
289301
sourceFilePath: string,
290302
examplePath: string,
291-
regionName: string,
303+
regionName: string | undefined,
292304
cache: RegionCache,
293-
): RegionContent {
305+
): string {
294306
// Resolve the example path relative to the source file
295307
const sourceDir = dirname(sourceFilePath);
296308
const absoluteExamplePath = resolve(sourceDir, examplePath);
297309

298-
// Check cache first
299-
let fileCache = cache.get(absoluteExamplePath);
300-
if (fileCache) {
301-
const cached = fileCache.get(regionName);
302-
if (cached) {
303-
return cached;
304-
}
305-
}
310+
// File content is always cached with key ending in "#" (empty region)
311+
const fileKey = `${absoluteExamplePath}#`;
312+
let fileContent = cache.get(fileKey);
306313

307-
// Load the example file
308-
let exampleContent: string;
309-
try {
310-
exampleContent = readFileSync(absoluteExamplePath, "utf-8");
311-
} catch {
312-
throw new Error(`Example file not found: ${absoluteExamplePath}`);
314+
if (fileContent === undefined) {
315+
try {
316+
fileContent = readFileSync(absoluteExamplePath, "utf-8").trim();
317+
} catch {
318+
throw new Error(`Example file not found: ${absoluteExamplePath}`);
319+
}
320+
cache.set(fileKey, fileContent);
313321
}
314322

315-
// Initialize file cache if needed
316-
if (!fileCache) {
317-
fileCache = new Map();
318-
cache.set(absoluteExamplePath, fileCache);
323+
// If no region name, return whole file
324+
if (!regionName) {
325+
return fileContent;
319326
}
320327

321-
// Determine language from file extension
322-
const language: "ts" | "tsx" = absoluteExamplePath.endsWith(".tsx")
323-
? "tsx"
324-
: "ts";
328+
// Extract region from cached file content, cache the result
329+
const regionKey = `${absoluteExamplePath}#${regionName}`;
330+
let regionContent = cache.get(regionKey);
325331

326-
// Extract the region
327-
const code = extractRegion(exampleContent, regionName, examplePath);
328-
329-
const regionContent: RegionContent = { code, language };
330-
fileCache.set(regionName, regionContent);
332+
if (regionContent === undefined) {
333+
regionContent = extractRegion(fileContent, regionName, examplePath);
334+
cache.set(regionKey, regionContent);
335+
}
331336

332337
return regionContent;
333338
}
@@ -393,17 +398,14 @@ function processFile(
393398
const fence = fences[i];
394399

395400
try {
396-
const regionContent = getOrLoadRegion(
401+
const code = getOrLoadRegion(
397402
filePath,
398403
fence.examplePath,
399404
fence.regionName,
400405
cache,
401406
);
402407

403-
const formattedCode = formatCodeLines(
404-
regionContent.code,
405-
fence.linePrefix,
406-
);
408+
const formattedCode = formatCodeLines(code, fence.linePrefix);
407409

408410
// Replace content between opening fence end and closing fence start
409411
content =

0 commit comments

Comments
 (0)