apisurf
Version:
Analyze API surface changes between npm package versions to catch breaking changes
87 lines (86 loc) • 3.51 kB
JavaScript
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
};
}