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:
parent
6eb82b5562
commit
56a2e79bba
2 changed files with 66 additions and 64 deletions
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue