mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-03 06:47:33 +00:00
Implements a GitHub Actions workflow that automatically generates API changelogs by comparing TypeScript type definitions between versions. Changes: - Add release-api-changelogs.yaml workflow triggered after npm types release - Create snapshot-api.js script to extract API surface from TypeScript defs - Create compare-api-snapshots.js to generate human-readable changelogs - Initialize docs/API-CHANGELOG.md to track public API changes The workflow: 1. Triggers after Release NPM Types workflow completes 2. Builds and snapshots current and previous API surfaces 3. Compares snapshots to detect additions, removals, and modifications 4. Generates formatted changelog with breaking changes highlighted 5. Creates draft PR for review before merging This automates documentation of breaking changes for extension developers without manual effort, supporting the large extension ecosystem. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
239 lines
6.3 KiB
JavaScript
239 lines
6.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 * 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)
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
api.types[name] = {
|
|
kind: 'type',
|
|
name,
|
|
text: node.getText(sourceFile),
|
|
exported: hasExportModifier(node)
|
|
}
|
|
}
|
|
|
|
// Extract interfaces
|
|
if (ts.isInterfaceDeclaration(node) && node.name) {
|
|
const name = node.name.text
|
|
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'
|
|
})
|
|
}
|
|
})
|
|
|
|
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()
|
|
: []
|
|
}
|
|
}
|
|
|
|
// Extract enums
|
|
if (ts.isEnumDeclaration(node) && node.name) {
|
|
const name = node.name.text
|
|
const members = node.members.map((member) => ({
|
|
name: member.name.getText(sourceFile),
|
|
value: member.initializer
|
|
? member.initializer.getText(sourceFile)
|
|
: undefined
|
|
}))
|
|
|
|
api.enums[name] = {
|
|
kind: 'enum',
|
|
name,
|
|
members,
|
|
exported: hasExportModifier(node)
|
|
}
|
|
}
|
|
|
|
// Extract functions
|
|
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
const name = node.name.text
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Extract classes
|
|
if (ts.isClassDeclaration(node) && node.name) {
|
|
const name = node.name.text
|
|
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)
|
|
})
|
|
}
|
|
})
|
|
|
|
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()
|
|
: []
|
|
}
|
|
}
|
|
|
|
// Extract variable declarations (constants)
|
|
if (ts.isVariableStatement(node)) {
|
|
node.declarationList.declarations.forEach((decl) => {
|
|
if (decl.name && ts.isIdentifier(decl.name)) {
|
|
const name = decl.name.text
|
|
api.constants[name] = {
|
|
kind: 'constant',
|
|
name,
|
|
type: decl.type ? decl.type.getText(sourceFile) : 'unknown',
|
|
exported: hasExportModifier(node)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
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
|
|
console.log(JSON.stringify(apiSurface, null, 2))
|