apisurf
Version:
Analyze API surface changes between npm package versions to catch breaking changes
149 lines (147 loc) • 7.02 kB
JavaScript
import { describe, expect, it } from '@jest/globals';
import { parseCommonJSStatically } from './parseCommonJSStatically.js';
describe('parseCommonJSStatically', () => {
it('should parse direct exports (exports.foo = bar)', () => {
const sourceContent = `
exports.myFunction = function() {};
exports.MY_CONSTANT = 42;
exports.defaultConfig = { enabled: true };
`;
const result = parseCommonJSStatically(sourceContent, 'test-package', '1.0.0');
expect(result.namedExports.has('myFunction')).toBe(true);
expect(result.namedExports.has('MY_CONSTANT')).toBe(true);
expect(result.namedExports.has('defaultConfig')).toBe(true);
expect(result.defaultExport).toBe(false);
expect(result.packageName).toBe('test-package');
expect(result.version).toBe('1.0.0');
});
it('should parse computed exports (exports["foo"] = bar)', () => {
const sourceContent = `
exports["computed-name"] = function() {};
exports['another-export'] = 123;
exports[\`template-string\`] = "value";
`;
const result = parseCommonJSStatically(sourceContent, 'test-package', '1.0.0');
expect(result.namedExports.has('computed-name')).toBe(true);
expect(result.namedExports.has('another-export')).toBe(true);
expect(result.namedExports.has('template-string')).toBe(false); // Template strings not supported by regex
expect(result.namedExports.size).toBe(2);
});
it('should parse Object.defineProperty exports', () => {
const sourceContent = `
Object.defineProperty(exports, 'propertyExport', {
value: function() {},
enumerable: true
});
Object.defineProperty(exports, "anotherProperty", { value: 42 });
`;
const result = parseCommonJSStatically(sourceContent, 'test-package', '1.0.0');
expect(result.namedExports.has('propertyExport')).toBe(true);
expect(result.namedExports.has('anotherProperty')).toBe(true);
});
it('should detect module.exports default export', () => {
const sourceContent = `
function MyClass() {}
module.exports = MyClass;
`;
const result = parseCommonJSStatically(sourceContent, 'test-package', '1.0.0');
expect(result.defaultExport).toBe(true);
expect(result.namedExports.size).toBe(0);
});
it('should parse __exportStar calls (bundler generated)', () => {
const sourceContent = `
__exportStar(require("./internal-module"), exports);
__exportStar(require('./another-module'), exports);
`;
const result = parseCommonJSStatically(sourceContent, 'test-package', '1.0.0');
expect(result.starExports).toContain('./internal-module');
expect(result.starExports).toContain('./another-module');
expect(result.starExports.length).toBe(2);
});
it('should skip comments and empty lines', () => {
const sourceContent = `
// This is a comment
/* Multi-line
comment */
exports.realExport = function() {};
// exports.commentedExport = 'should be ignored';
/* exports.multiLineCommentExport = 'also ignored'; */
`;
const result = parseCommonJSStatically(sourceContent, 'test-package', '1.0.0');
expect(result.namedExports.has('realExport')).toBe(true);
expect(result.namedExports.has('commentedExport')).toBe(false);
expect(result.namedExports.has('multiLineCommentExport')).toBe(false);
expect(result.namedExports.size).toBe(1);
});
it('should handle mixed export patterns', () => {
const sourceContent = `
exports.directExport = 'value';
exports["computed"] = 42;
Object.defineProperty(exports, 'defined', { value: true });
__exportStar(require('./utils'), exports);
module.exports = { mixed: true };
`;
const result = parseCommonJSStatically(sourceContent, 'test-package', '1.0.0');
expect(result.namedExports.has('directExport')).toBe(true);
expect(result.namedExports.has('computed')).toBe(true);
expect(result.namedExports.has('defined')).toBe(true);
expect(result.starExports).toContain('./utils');
expect(result.defaultExport).toBe(true);
});
it('should return correct package metadata', () => {
const sourceContent = 'exports.test = true;';
const result = parseCommonJSStatically(sourceContent, 'my-package', '2.1.0');
expect(result.packageName).toBe('my-package');
expect(result.version).toBe('2.1.0');
expect(result.typeDefinitions?.size).toBe(0);
});
it('should handle exports with special characters in names', () => {
const sourceContent = `
exports._privateExport = 'value';
exports.$jquery = function() {};
exports.name123 = true;
`;
const result = parseCommonJSStatically(sourceContent, 'test-package', '1.0.0');
expect(result.namedExports.has('_privateExport')).toBe(true);
expect(result.namedExports.has('$jquery')).toBe(true);
expect(result.namedExports.has('name123')).toBe(true);
});
it('should not parse invalid export patterns', () => {
const sourceContent = `
// These should not be parsed as exports
var exports = {};
someObject.exports.notAnExport = true;
exports['with spaces'] = 'invalid';
exports[123] = 'numeric key';
exports. = 'invalid syntax';
`;
const result = parseCommonJSStatically(sourceContent, 'test-package', '1.0.0');
// Actually, exports['with spaces'] is a valid CommonJS export
expect(result.namedExports.size).toBe(1);
expect(result.namedExports.has('with spaces')).toBe(true);
});
it('should handle module.exports with various patterns', () => {
const sourceContent1 = 'module.exports = function() {};';
const result1 = parseCommonJSStatically(sourceContent1, 'test', '1.0.0');
expect(result1.defaultExport).toBe(true);
const sourceContent2 = 'module.exports = {};';
const result2 = parseCommonJSStatically(sourceContent2, 'test', '1.0.0');
expect(result2.defaultExport).toBe(true);
const sourceContent3 = 'module.exports.notDefault = true;';
const result3 = parseCommonJSStatically(sourceContent3, 'test', '1.0.0');
expect(result3.defaultExport).toBe(false);
});
it('should handle exports inside conditional blocks (but still parse them)', () => {
const sourceContent = `
if (process.env.NODE_ENV === 'production') {
exports.prodOnly = true;
} else {
exports.devOnly = true;
}
`;
const result = parseCommonJSStatically(sourceContent, 'test-package', '1.0.0');
// Static analysis will find both even though only one would be exported at runtime
expect(result.namedExports.has('prodOnly')).toBe(true);
expect(result.namedExports.has('devOnly')).toBe(true);
});
});