UNPKG

apisurf

Version:

Analyze API surface changes between npm package versions to catch breaking changes

253 lines (252 loc) 10.2 kB
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; } }