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)
103106const JSDOC_LABELED_FENCE_PATTERN =
104- / ^ ( \s * \* \s * ) ` ` ` ( t s | t s x ) (?: \s + ( \S + ) ) ? \s + s o u r c e = " ( [ ^ " # ] + ) # ( [ ^ " ] + ) " / ;
107+ / ^ ( \s * \* \s * ) ` ` ` ( \w + ) (?: \s + ( \S + ) ) ? \s + s o u r c e = " ( [ ^ " # ] + ) (?: # ( [ ^ " ] + ) ) ? " / ;
105108const 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)
110114const MARKDOWN_LABELED_FENCE_PATTERN =
111- / ^ ` ` ` ( t s | t s x ) (?: \s + ( \S + ) ) ? \s + s o u r c e = " ( [ ^ " # ] + ) # ( [ ^ " ] + ) " / ;
115+ / ^ ` ` ` ( \w + ) (?: \s + ( \S + ) ) ? \s + s o u r c e = " ( [ ^ " # ] + ) (?: # ( [ ^ " ] + ) ) ? " / ;
112116const 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 */
288300function 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