Files
ComfyUI_frontend/scripts/snapshot-api.ts
snomiao 794829db6d [refactor] Clean up API changelog implementation
- Remove demo-snapshots folder
- Merge workflow documentation into main workflows README
- Convert scripts to TypeScript (.js → .ts)
- Revert eslint.config.ts changes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 09:35:35 +00:00

315 lines
9.3 KiB
JavaScript

#!/usr/bin/env node
/**
* Generates a JSON snapshot of the public API surface from TypeScript definitions.
* 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'
const args = process.argv.slice(2)
if (args.length === 0) {
console.error('Usage: snapshot-api.js <path-to-index.d.ts>')
process.exit(1)
}
const filePath = args[0]
if (!fs.existsSync(filePath)) {
console.error(`File not found: ${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
*/
function extractApiSurface(sourceFile) {
const api = {
types: {},
interfaces: {},
enums: {},
functions: {},
classes: {},
constants: {}
}
function visit(node) {
// Extract type aliases
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
sourceFile: sourceLocation?.file,
sourceLine: sourceLocation?.line
}
}
// Extract interfaces
if (ts.isInterfaceDeclaration(node) && node.name) {
const name = node.name.text
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
const members = []
node.members.forEach((member) => {
if (ts.isPropertySignature(member) && member.name) {
members.push({
name: member.name.getText(sourceFile),
type: member.type ? member.type.getText(sourceFile) : 'any',
optional: !!member.questionToken
})
} else if (ts.isMethodSignature(member) && member.name) {
members.push({
name: member.name.getText(sourceFile),
kind: 'method',
parameters: member.parameters.map((p) => ({
name: p.name.getText(sourceFile),
type: p.type ? p.type.getText(sourceFile) : 'any',
optional: !!p.questionToken
})),
returnType: member.type ? member.type.getText(sourceFile) : 'void'
})
}
})
const sourceLocation = findInSourceFiles(name, 'interface')
api.interfaces[name] = {
kind: 'interface',
name,
members,
exported: hasExportModifier(node),
heritage: node.heritageClauses
? node.heritageClauses
.map((clause) =>
clause.types.map((type) => type.getText(sourceFile))
)
.flat()
: [],
line: line + 1, // Convert to 1-indexed
sourceFile: sourceLocation?.file,
sourceLine: sourceLocation?.line
}
}
// Extract enums
if (ts.isEnumDeclaration(node) && node.name) {
const name = node.name.text
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
const members = node.members.map((member) => ({
name: member.name.getText(sourceFile),
value: member.initializer
? member.initializer.getText(sourceFile)
: undefined
}))
const sourceLocation = findInSourceFiles(name, 'enum')
api.enums[name] = {
kind: 'enum',
name,
members,
exported: hasExportModifier(node),
line: line + 1, // Convert to 1-indexed
sourceFile: sourceLocation?.file,
sourceLine: sourceLocation?.line
}
}
// Extract functions
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,
parameters: node.parameters.map((p) => ({
name: p.name.getText(sourceFile),
type: p.type ? p.type.getText(sourceFile) : 'any',
optional: !!p.questionToken
})),
returnType: node.type ? node.type.getText(sourceFile) : 'any',
exported: hasExportModifier(node),
line: line + 1, // Convert to 1-indexed
sourceFile: sourceLocation?.file,
sourceLine: sourceLocation?.line
}
}
// Extract classes
if (ts.isClassDeclaration(node) && node.name) {
const name = node.name.text
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
const members = []
const methods = []
node.members.forEach((member) => {
if (ts.isPropertyDeclaration(member) && member.name) {
members.push({
name: member.name.getText(sourceFile),
type: member.type ? member.type.getText(sourceFile) : 'any',
static: hasStaticModifier(member),
visibility: getVisibility(member)
})
} else if (ts.isMethodDeclaration(member) && member.name) {
methods.push({
name: member.name.getText(sourceFile),
parameters: member.parameters.map((p) => ({
name: p.name.getText(sourceFile),
type: p.type ? p.type.getText(sourceFile) : 'any',
optional: !!p.questionToken
})),
returnType: member.type ? member.type.getText(sourceFile) : 'any',
static: hasStaticModifier(member),
visibility: getVisibility(member)
})
}
})
const sourceLocation = findInSourceFiles(name, 'class')
api.classes[name] = {
kind: 'class',
name,
members,
methods,
exported: hasExportModifier(node),
heritage: node.heritageClauses
? node.heritageClauses
.map((clause) =>
clause.types.map((type) => type.getText(sourceFile))
)
.flat()
: [],
line: line + 1, // Convert to 1-indexed
sourceFile: sourceLocation?.file,
sourceLine: sourceLocation?.line
}
}
// Extract variable declarations (constants)
if (ts.isVariableStatement(node)) {
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
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
sourceFile: sourceLocation?.file,
sourceLine: sourceLocation?.line
}
}
})
}
ts.forEachChild(node, visit)
}
function hasExportModifier(node) {
return (
node.modifiers &&
node.modifiers.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword)
)
}
function hasStaticModifier(node) {
return (
node.modifiers &&
node.modifiers.some((mod) => mod.kind === ts.SyntaxKind.StaticKeyword)
)
}
function getVisibility(node) {
if (!node.modifiers) return 'public'
if (node.modifiers.some((mod) => mod.kind === ts.SyntaxKind.PrivateKeyword))
return 'private'
if (
node.modifiers.some((mod) => mod.kind === ts.SyntaxKind.ProtectedKeyword)
)
return 'protected'
return 'public'
}
visit(sourceFile)
return api
}
// Read and parse the file
const sourceCode = fs.readFileSync(filePath, 'utf-8')
const sourceFile = ts.createSourceFile(
path.basename(filePath),
sourceCode,
ts.ScriptTarget.Latest,
true
)
const apiSurface = extractApiSurface(sourceFile)
// Output as JSON
// eslint-disable-next-line no-console
console.log(JSON.stringify(apiSurface, null, 2))