UNPKG

apisurf

Version:

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

87 lines (86 loc) 3.51 kB
import chalk from 'chalk'; import { compareTypeDefinitions } from './compareTypeDefinitions.js'; /** * Compares two API surfaces and identifies breaking and non-breaking changes. * Analyzes exports, types, and other public API elements between versions. */ export function compareApiSurfaces(base, head, pkg) { const breakingChanges = []; const nonBreakingChanges = []; // Collect names that have type definitions to avoid duplicate removal entries const baseTypeNames = new Set(base.typeDefinitions ? Array.from(base.typeDefinitions.keys()) : []); // Check for removed value exports (breaking) for (const exportName of base.namedExports) { if (!head.namedExports.has(exportName)) { // Skip if this export has a type definition - compareTypeDefinitions will handle it with more detail if (!baseTypeNames.has(exportName)) { breakingChanges.push({ type: 'export-removed', description: `Removed export '${chalk.bold(exportName)}'`, before: exportName }); } } } // Check for removed type-only exports (breaking) for (const exportName of base.typeOnlyExports) { if (!head.typeOnlyExports.has(exportName)) { // Skip if this export has a type definition - compareTypeDefinitions will handle it with more detail if (!baseTypeNames.has(exportName)) { breakingChanges.push({ type: 'export-removed', description: `Removed type export '${chalk.bold(exportName)}'`, before: exportName }); } } } // Check for removed default export (breaking) if (base.defaultExport && !head.defaultExport) { breakingChanges.push({ type: 'export-removed', description: `Removed ${chalk.bold('default export')}`, before: 'default' }); } // Check for added value exports (non-breaking) for (const exportName of head.namedExports) { if (!base.namedExports.has(exportName)) { nonBreakingChanges.push({ type: 'export-added', description: `Added export '${chalk.bold(exportName)}'`, details: exportName }); } } // Check for added type-only exports (non-breaking) for (const exportName of head.typeOnlyExports) { if (!base.typeOnlyExports.has(exportName)) { nonBreakingChanges.push({ type: 'export-added', description: `Added type export '${chalk.bold(exportName)}'`, details: exportName }); } } // Check for added default export (non-breaking) if (!base.defaultExport && head.defaultExport) { nonBreakingChanges.push({ type: 'export-added', description: `Added ${chalk.bold('default export')}`, details: 'default' }); } // Analyze TypeScript definition changes if available if (base.typeDefinitions && head.typeDefinitions) { const typeChanges = compareTypeDefinitions(base.typeDefinitions, head.typeDefinitions); breakingChanges.push(...typeChanges.breakingChanges); nonBreakingChanges.push(...typeChanges.nonBreakingChanges); } return { name: pkg.name, path: pkg.path, breakingChanges, nonBreakingChanges }; }