apisurf
Version:
Analyze API surface changes between npm package versions to catch breaking changes
253 lines (252 loc) • 10.2 kB
JavaScript
import * as ts from 'typescript';
import * as fs from 'fs';
import * as path from 'path';
/**
* Parses TypeScript definition files to extract API surface information.
*/
export function parseTypeScriptDefinitions(sourceContent, packageName, version, basePath, repositoryUrl) {
const sourceFile = ts.createSourceFile('temp.d.ts', sourceContent, ts.ScriptTarget.Latest, true);
const namedExports = new Set();
const typeOnlyExports = new Set();
const starExports = [];
let defaultExport = false;
const typeDefinitions = new Map();
function visit(node) {
if (ts.isExportDeclaration(node)) {
handleExportDeclaration(node);
}
else if (ts.isInterfaceDeclaration(node)) {
handleInterfaceDeclaration(node);
}
else if (ts.isTypeAliasDeclaration(node)) {
handleTypeAliasDeclaration(node);
}
else if (ts.isFunctionDeclaration(node)) {
handleFunctionDeclaration(node);
}
else if (ts.isVariableStatement(node)) {
handleVariableStatement(node);
}
else if (ts.isClassDeclaration(node)) {
handleClassDeclaration(node);
}
else if (ts.isEnumDeclaration(node)) {
handleEnumDeclaration(node);
}
else if (ts.isExportAssignment(node)) {
defaultExport = true;
}
ts.forEachChild(node, visit);
}
function handleExportDeclaration(node) {
if (node.moduleSpecifier) {
// export * from './module'
if (!node.exportClause) {
const moduleSpec = node.moduleSpecifier.text;
// Check if this is an internal module (starts with . or ..)
if (isInternalModule(moduleSpec) && basePath) {
// Try to expand the internal export *
const expandedSurface = expandInternalStarExport(moduleSpec, basePath, packageName, version);
if (expandedSurface) {
// Merge the expanded exports into our surface
expandedSurface.namedExports.forEach(exp => namedExports.add(exp));
expandedSurface.typeOnlyExports.forEach(exp => typeOnlyExports.add(exp));
expandedSurface.starExports.forEach(exp => starExports.push(exp));
// Merge type definitions
if (expandedSurface.typeDefinitions) {
expandedSurface.typeDefinitions.forEach((def, name) => {
typeDefinitions.set(name, def);
});
}
if (expandedSurface.defaultExport) {
defaultExport = true;
}
return;
}
}
// If it's external or expansion failed, keep as star export
starExports.push(moduleSpec);
}
return;
}
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
for (const element of node.exportClause.elements) {
const exportName = element.name.text;
if (element.isTypeOnly || (node.isTypeOnly)) {
typeOnlyExports.add(exportName);
}
else {
namedExports.add(exportName);
}
}
}
}
function handleInterfaceDeclaration(node) {
if (hasExportModifier(node)) {
const name = node.name.text;
typeOnlyExports.add(name);
const properties = new Map();
for (const member of node.members) {
if (ts.isPropertySignature(member) && member.name && ts.isIdentifier(member.name)) {
const propName = member.name.text;
const propType = member.type ? getTypeSignature(member.type) : 'any';
properties.set(propName, propType);
}
else if (ts.isMethodSignature(member) && member.name && ts.isIdentifier(member.name)) {
const methodName = member.name.text;
const methodSig = getMethodSignature(member);
properties.set(methodName, methodSig);
}
}
typeDefinitions.set(name, {
name,
kind: 'interface',
properties,
signature: getNodeText(node)
});
}
}
function handleTypeAliasDeclaration(node) {
if (hasExportModifier(node)) {
const name = node.name.text;
typeOnlyExports.add(name);
typeDefinitions.set(name, {
name,
kind: 'type',
signature: getNodeText(node)
});
}
}
function handleFunctionDeclaration(node) {
if (hasExportModifier(node) && node.name) {
const name = node.name.text;
namedExports.add(name);
const parameters = node.parameters.map(param => {
const paramName = ts.isIdentifier(param.name) ? param.name.text : 'unknown';
const paramType = param.type ? getTypeSignature(param.type) : 'any';
return `${paramName}: ${paramType}`;
});
const returnType = node.type ? getTypeSignature(node.type) : 'any';
typeDefinitions.set(name, {
name,
kind: 'function',
parameters,
returnType,
signature: getNodeText(node)
});
}
}
function handleVariableStatement(node) {
if (hasExportModifier(node)) {
for (const declaration of node.declarationList.declarations) {
if (ts.isIdentifier(declaration.name)) {
const name = declaration.name.text;
namedExports.add(name);
const varType = declaration.type ? getTypeSignature(declaration.type) : 'any';
typeDefinitions.set(name, {
name,
kind: 'variable',
signature: `${name}: ${varType}`
});
}
}
}
}
function handleClassDeclaration(node) {
if (hasExportModifier(node) && node.name) {
const name = node.name.text;
namedExports.add(name);
const properties = new Map();
for (const member of node.members) {
if (ts.isPropertyDeclaration(member) && member.name && ts.isIdentifier(member.name)) {
const propName = member.name.text;
const propType = member.type ? getTypeSignature(member.type) : 'any';
properties.set(propName, propType);
}
else if (ts.isMethodDeclaration(member) && member.name && ts.isIdentifier(member.name)) {
const methodName = member.name.text;
const methodSig = getMethodSignature(member);
properties.set(methodName, methodSig);
}
}
typeDefinitions.set(name, {
name,
kind: 'class',
properties,
signature: getNodeText(node)
});
}
}
function handleEnumDeclaration(node) {
if (hasExportModifier(node)) {
const name = node.name.text;
namedExports.add(name);
typeDefinitions.set(name, {
name,
kind: 'enum',
signature: getNodeText(node)
});
}
}
function hasExportModifier(node) {
return !!('modifiers' in node && node.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword));
}
function getTypeSignature(typeNode) {
return getNodeText(typeNode);
}
function getMethodSignature(method) {
const params = method.parameters.map(param => {
const paramName = ts.isIdentifier(param.name) ? param.name.text : 'unknown';
const paramType = param.type ? getTypeSignature(param.type) : 'any';
return `${paramName}: ${paramType}`;
}).join(', ');
const returnType = method.type ? getTypeSignature(method.type) : 'any';
return `(${params}) => ${returnType}`;
}
function getNodeText(node) {
return sourceContent.substring(node.pos, node.end).trim();
}
visit(sourceFile);
return {
namedExports,
typeOnlyExports,
defaultExport,
starExports,
packageName,
version,
typeDefinitions,
repositoryUrl
};
}
function isInternalModule(moduleSpec) {
return moduleSpec.startsWith('./') || moduleSpec.startsWith('../');
}
function expandInternalStarExport(moduleSpec, basePath, packageName, version) {
try {
// Resolve the module path
let resolvedPath = path.resolve(path.dirname(basePath), moduleSpec);
// Try different file extensions
const extensions = ['.d.ts', '.ts', '/index.d.ts', '/index.ts'];
let foundPath = null;
for (const ext of extensions) {
const testPath = resolvedPath + ext;
if (fs.existsSync(testPath)) {
foundPath = testPath;
break;
}
}
if (!foundPath) {
// Only log in verbose mode - this would be passed down if we had access to it
// For now, these warnings are less critical since they only affect expansion
return null;
}
// Read and parse the referenced file
const content = fs.readFileSync(foundPath, 'utf8');
return parseTypeScriptDefinitions(content, packageName, version, foundPath, undefined);
}
catch (error) {
// Only log in verbose mode - this would be passed down if we had access to it
// For now, these warnings are less critical since they only affect expansion
return null;
}
}