From 98ed124b0243fd7de5e4cc743fadf010ca3495a6 Mon Sep 17 00:00:00 2001 From: snomiao Date: Wed, 5 Nov 2025 17:22:11 +0000 Subject: [PATCH] [feat] Add source file location discovery to API snapshot tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhance snapshot-api.js to discover source file locations for exported declarations using grep search. This enables mapping from generated .d.ts declarations back to their original source files. For each declaration, the tool now searches the src directory for: - export interface - export class - export type - export enum - export function - export const The source file path and line number are stored in the snapshot for use in generating accurate GitHub permalinks in the changelog. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- scripts/snapshot-api.js | 85 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 6 deletions(-) diff --git a/scripts/snapshot-api.js b/scripts/snapshot-api.js index 15edb7e63..0fbce44c5 100644 --- a/scripts/snapshot-api.js +++ b/scripts/snapshot-api.js @@ -5,6 +5,7 @@ * This snapshot is used to track API changes between versions. */ +import { execSync } from 'child_process' import * as fs from 'fs' import * as path from 'path' import * as ts from 'typescript' @@ -21,6 +22,60 @@ if (!fs.existsSync(filePath)) { process.exit(1) } +/** + * Search for the declaration in source files + * Returns {file, line} or null if not found + */ +function findInSourceFiles(declarationName, kind, sourceRoot = 'src') { + const searchPattern = getSearchPattern(declarationName, kind) + if (!searchPattern) return null + + try { + // Search for the declaration pattern in source files + const result = execSync( + `grep -rn "${searchPattern}" ${sourceRoot} --include="*.ts" --include="*.tsx" | head -1`, + { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] } + ).trim() + + if (result) { + // Parse grep output: filepath:line:content + const match = result.match(/^([^:]+):(\d+):/) + if (match) { + return { + file: match[1], + line: parseInt(match[2], 10) + } + } + } + } catch (error) { + // grep returns non-zero exit code if no match found + } + + return null +} + +/** + * Generate search pattern for finding declaration in source + */ +function getSearchPattern(name, kind) { + switch (kind) { + case 'interface': + return `export interface ${name}` + case 'class': + return `export class ${name}` + case 'type': + return `export type ${name}` + case 'enum': + return `export enum ${name}` + case 'function': + return `export function ${name}` + case 'constant': + return `export const ${name}` + default: + return null + } +} + /** * Extract API surface from TypeScript definitions */ @@ -39,12 +94,15 @@ function extractApiSurface(sourceFile) { if (ts.isTypeAliasDeclaration(node) && node.name) { const name = node.name.text const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart()) + const sourceLocation = findInSourceFiles(name, 'type') api.types[name] = { kind: 'type', name, text: node.getText(sourceFile), exported: hasExportModifier(node), - line: line + 1 // Convert to 1-indexed + line: line + 1, // Convert to 1-indexed + sourceFile: sourceLocation?.file, + sourceLine: sourceLocation?.line } } @@ -75,6 +133,7 @@ function extractApiSurface(sourceFile) { } }) + const sourceLocation = findInSourceFiles(name, 'interface') api.interfaces[name] = { kind: 'interface', name, @@ -87,7 +146,9 @@ function extractApiSurface(sourceFile) { ) .flat() : [], - line: line + 1 // Convert to 1-indexed + line: line + 1, // Convert to 1-indexed + sourceFile: sourceLocation?.file, + sourceLine: sourceLocation?.line } } @@ -102,12 +163,15 @@ function extractApiSurface(sourceFile) { : undefined })) + const sourceLocation = findInSourceFiles(name, 'enum') api.enums[name] = { kind: 'enum', name, members, exported: hasExportModifier(node), - line: line + 1 // Convert to 1-indexed + line: line + 1, // Convert to 1-indexed + sourceFile: sourceLocation?.file, + sourceLine: sourceLocation?.line } } @@ -115,6 +179,7 @@ function extractApiSurface(sourceFile) { if (ts.isFunctionDeclaration(node) && node.name) { const name = node.name.text const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart()) + const sourceLocation = findInSourceFiles(name, 'function') api.functions[name] = { kind: 'function', name, @@ -125,7 +190,9 @@ function extractApiSurface(sourceFile) { })), returnType: node.type ? node.type.getText(sourceFile) : 'any', exported: hasExportModifier(node), - line: line + 1 // Convert to 1-indexed + line: line + 1, // Convert to 1-indexed + sourceFile: sourceLocation?.file, + sourceLine: sourceLocation?.line } } @@ -159,6 +226,7 @@ function extractApiSurface(sourceFile) { } }) + const sourceLocation = findInSourceFiles(name, 'class') api.classes[name] = { kind: 'class', name, @@ -172,7 +240,9 @@ function extractApiSurface(sourceFile) { ) .flat() : [], - line: line + 1 // Convert to 1-indexed + line: line + 1, // Convert to 1-indexed + sourceFile: sourceLocation?.file, + sourceLine: sourceLocation?.line } } @@ -182,12 +252,15 @@ function extractApiSurface(sourceFile) { node.declarationList.declarations.forEach((decl) => { if (decl.name && ts.isIdentifier(decl.name)) { const name = decl.name.text + const sourceLocation = findInSourceFiles(name, 'constant') api.constants[name] = { kind: 'constant', name, type: decl.type ? decl.type.getText(sourceFile) : 'unknown', exported: hasExportModifier(node), - line: line + 1 // Convert to 1-indexed + line: line + 1, // Convert to 1-indexed + sourceFile: sourceLocation?.file, + sourceLine: sourceLocation?.line } } })