Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/npm-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@ jobs:

- name: Run Unit Tests
run: npm run test

- name: Check Type Export Parity
run: npm run check:exports

- name: Check TypeScript Consumer
run: npm run check:types
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## unreleased

## [1.29.5] - 2026-02-28

- Types: Update Typescript typings (#194)

## [1.29.4] - 2026-02-23

- Overlay: refactor: Switch to jsonpathly library for RFC 9535 compliance (#190)
Expand Down
51 changes: 36 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
"scripts": {
"test": "jest --colors --verbose --reporters=default --collectCoverage --no-cache --maxWorkers=2",
"lint": "npx prettier --write '**/*.js'",
"check:exports": "node scripts/audit-runtime-exports.js && node scripts/audit-types-exports.js",
"check:types": "tsc -p tsconfig.types.json",
"release": "npx np --branch main"
},
"dependencies": {
Expand All @@ -43,7 +45,8 @@
},
"devDependencies": {
"jest": "^30.2.0",
"openapi-types": "^12.1.3"
"openapi-types": "^12.1.3",
"typescript": "^5.9.3"
},
"engines": {
"node": ">=18"
Expand Down
5 changes: 5 additions & 0 deletions scripts/audit-runtime-exports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node
'use strict';

const runtimeExports = Object.keys(require('../openapi-format')).sort();
console.log(runtimeExports.join('\n'));
31 changes: 31 additions & 0 deletions scripts/audit-types-exports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env node
'use strict';

const fs = require('fs');
const path = require('path');

const runtimeExports = Object.keys(require('../openapi-format')).sort();
const dtsPath = path.join(__dirname, '..', 'types', 'openapi-format.d.ts');
const dts = fs.readFileSync(dtsPath, 'utf8');

const typeExportNames = new Set();
for (const match of dts.matchAll(/export\s+function\s+([A-Za-z0-9_]+)/g)) {
typeExportNames.add(match[1]);
}

const typeExports = [...typeExportNames].sort();
const missingInTypes = runtimeExports.filter(name => !typeExportNames.has(name));
const extrasInTypes = typeExports.filter(name => !runtimeExports.includes(name));

if (missingInTypes.length || extrasInTypes.length) {
console.error('Type export parity check failed.');
if (missingInTypes.length) {
console.error(`Missing in types: ${missingInTypes.join(', ')}`);
}
if (extrasInTypes.length) {
console.error(`Missing at runtime: ${extrasInTypes.join(', ')}`);
}
process.exit(1);
}

console.log('Type export parity check passed.');
33 changes: 33 additions & 0 deletions test/_types/consumer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as openapiFormat from 'openapi-format';

const document = {
openapi: '3.0.3',
info: {title: 'API', version: '1.0.0'},
paths: {}
};

async function verifyTypes() {
await openapiFormat.openapiSort(document, {});
await openapiFormat.openapiFilter(document, {});
await openapiFormat.openapiGenerate(document, {});
await openapiFormat.openapiChangeCase(document, {});
await openapiFormat.openapiOverlay(document, {overlaySet: {actions: []}});
await openapiFormat.openapiSplit(document, {output: 'test/_split/snap.yaml'});
await openapiFormat.openapiConvertVersion(document, {convertTo: '3.1'});
await openapiFormat.openapiRename(document, {rename: 'renamed'});

await openapiFormat.readFile('readme.md', {});
await openapiFormat.parseFile('test/yaml-default/input.yaml', {});
await openapiFormat.parseString('openapi: 3.0.3', {});
await openapiFormat.stringify(document, {});
await openapiFormat.writeFile('test/_split/_types-output.yaml', document, {});

await openapiFormat.detectFormat('openapi: 3.0.3');
openapiFormat.analyzeOpenApi(document);

openapiFormat.changeCase('hello_world', 'camelCase');
openapiFormat.resolveJsonPath(document, '$.paths');
openapiFormat.resolveJsonPathValue(document, '$.paths');
}

void verifyTypes();
6 changes: 6 additions & 0 deletions test/_types/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.types.json",
"include": [
"./**/*.ts"
]
}
144 changes: 144 additions & 0 deletions test/command.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
'use strict';

const {describe, it, expect, beforeEach, afterEach} = require('@jest/globals');

function createCommander() {
jest.resetModules();
const commander = require('../utils/command');
const exits = [];
commander.exitOverride(err => {
exits.push(err);
});
return {commander, exits};
}

describe('MiniCommander', () => {
let stdoutSpy;
let consoleErrorSpy;

beforeEach(() => {
stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
});

afterEach(() => {
stdoutSpy.mockRestore();
consoleErrorSpy.mockRestore();
});

it('displays help and version', () => {
const {commander, exits} = createCommander();
commander.usage('[file]');
commander.description('test description');
commander.version('1.2.3');

commander.parse(['node', '/tmp/cli.js', '--help']);
expect(exits[0].code).toBe('commander.helpDisplayed');
expect(exits[0].exitCode).toBe(0);
expect(stdoutSpy).toHaveBeenCalled();

commander.parse(['node', '/tmp/cli.js', '--version']);
expect(exits[1].code).toBe('commander.version');
expect(exits[1].exitCode).toBe(0);
});

it('handles unknown and missing arguments', () => {
const {commander, exits} = createCommander();
commander.option('-f, --file <path>', 'file path');

commander.parse(['node', '/tmp/cli.js', '--unknown']);
expect(exits[0].code).toBe('commander.unknownOption');
expect(exits[0].message).toContain('--unknown');

commander.parse(['node', '/tmp/cli.js', '--file']);
expect(exits[1].code).toBe('commander.missingArgument');
expect(exits[1].message).toContain('--file');
});

it('parses long options, no-* options and positional args', () => {
const {commander} = createCommander();
let capturedArg;
let capturedOptions;

commander
.option('--name <value>', 'name')
.option('--tag [value]', 'tag')
.option('--no-sort', 'disable sort')
.action((arg, options) => {
capturedArg = arg;
capturedOptions = options;
});

commander.parse(['node', '/tmp/cli.js', 'input.yaml', '--name=alice', '--tag', 'beta', '--no-sort']);

expect(capturedArg).toBe('input.yaml');
expect(capturedOptions).toMatchObject({
name: 'alice',
tag: 'beta',
sort: false
});
});

it('parses short options, including grouped flags and attached values', () => {
const {commander} = createCommander();
let capturedOptions;

commander
.option('-v, --verbose', 'verbose')
.option('-n, --number <num>', 'number', 7)
.action((arg, options) => {
capturedOptions = options;
});

commander.parse(['node', '/tmp/cli.js', 'input.yaml', '-vn5']);
expect(capturedOptions).toMatchObject({verbose: true, number: 5});

commander.parse(['node', '/tmp/cli.js', 'input.yaml', '-n', 'not-a-number']);
expect(capturedOptions.number).toBe(7);
});

it('uses parser functions with previous values and keeps default ordering behavior', () => {
const {commander} = createCommander();
let capturedOptions;

commander
.option(
'--include [value]',
'include values',
(value, previous) => {
const list = Array.isArray(previous) ? previous : [];
return value === undefined ? list : list.concat(value);
},
[]
)
.action((arg, options) => {
capturedOptions = options;
});

commander.parse(['node', '/tmp/cli.js', 'input.yaml', '--include', 'a', '--include', 'b']);

expect(capturedOptions.include).toStrictEqual(['a', 'b']);
expect(Object.keys(capturedOptions)).toContain('include');
});

it('handles async action rejection by exiting with error code', async () => {
const {commander, exits} = createCommander();

commander.action(async () => {
throw new Error('boom');
});

commander.parse(['node', '/tmp/cli.js', 'input.yaml']);

await new Promise(resolve => setImmediate(resolve));
expect(exits[0].code).toBe('commander.asyncActionRejected');
expect(exits[0].exitCode).toBe(1);
});

it('handles unknown short options', () => {
const {commander, exits} = createCommander();
commander.parse(['node', '/tmp/cli.js', '-z']);
expect(exits[0].code).toBe('commander.unknownOption');
expect(exits[0].message).toContain("'-z'");
});
});
20 changes: 20 additions & 0 deletions test/filtering.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const testUtils = require('./__utils__/test-utils');
const {isUsedComp} = require('../utils/filtering');
const {describe, it, expect} = require('@jest/globals');

describe('openapi-format CLI filtering tests', () => {
Expand Down Expand Up @@ -308,4 +309,23 @@ describe('openapi-format CLI filtering tests', () => {
expect(outputAfter).toStrictEqual(outputBefore);
});
});

describe('isUsedComp', () => {
it('returns false for non-object input', () => {
expect(isUsedComp(null, 'schemas')).toBe(false);
});

it('returns false for non-string prop', () => {
expect(isUsedComp({schemas: {used: true}}, 1)).toBe(false);
});

it('returns false when used flag is missing or false', () => {
expect(isUsedComp({schemas: {}}, 'schemas')).toBe(false);
expect(isUsedComp({schemas: {used: false}}, 'schemas')).toBe(false);
});

it('returns true when used flag is true', () => {
expect(isUsedComp({schemas: {used: true}}, 'schemas')).toBe(true);
});
});
});
Loading