diff --git a/package.json b/package.json index c78051b..a58db2f 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "scripts": { "build": "rollup -c && tsc --emitDeclarationOnly", "dev": "rollup -c --watch", - "pretest": "rollup -c", + "pretest": "bun run build", "test": "bun test", "test:coverage": "bun test --coverage", "test:watch": "bun test --watch", @@ -86,4 +86,4 @@ "@types/js-yaml": "^4.0.0", "js-yaml": "^4.1.0" } -} +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 566625b..dceb6ad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -300,10 +300,9 @@ function sortOpenAPIKeys(obj: any): any { const standardKeys = getStandardKeysForContext(contextKey); const customExtensions = vendorExtensions[contextKey] || {}; - const sortedKeys = Object.keys(obj).sort((a, b) => { // Use the unified sorting function - return sortKeys(a, b, standardKeys, customExtensions); + return sortKeys(a, b, standardKeys, customExtensions);; }); const sortedObj: any = {}; @@ -515,39 +514,52 @@ function sortKeys(a: string, b: string, standardKeys: readonly string[], customE const aCustomPos = customExtensions[a]; const bCustomPos = customExtensions[b]; - // Handle custom extensions first + // Get standard key positions + const aStandardIndex = standardKeys.indexOf(a); + const bStandardIndex = standardKeys.indexOf(b); + + // Both are custom extensions if (aCustomPos !== undefined && bCustomPos !== undefined) { return aCustomPos - bCustomPos; } - if (aCustomPos !== undefined) { - if (aCustomPos < standardKeys.length) { - return -1; // Custom key should come before standard keys + // Both are standard keys + if (aStandardIndex !== -1 && bStandardIndex !== -1) { + return aStandardIndex - bStandardIndex; + } + + // One is custom, one is standard + if (aCustomPos !== undefined && bStandardIndex !== -1) { + // Custom key should be positioned relative to standard keys + if (aCustomPos < bStandardIndex) { + return -1; // Custom key comes before this standard key + } else if (aCustomPos > bStandardIndex) { + return 1; // Custom key comes after this standard key + } else { + return -1; // Custom key comes before standard key at same position } } - if (bCustomPos !== undefined) { - if (bCustomPos < standardKeys.length) { - return 1; // Custom key should come before standard keys + if (bCustomPos !== undefined && aStandardIndex !== -1) { + // Custom key should be positioned relative to standard keys + if (bCustomPos < aStandardIndex) { + return 1; // Custom key comes before this standard key + } else if (bCustomPos > aStandardIndex) { + return -1; // Custom key comes after this standard key + } else { + return 1; // Standard key comes after custom key at same position } } - // Standard sorting - const aIndex = standardKeys.indexOf(a); - const bIndex = standardKeys.indexOf(b); + // One is standard, one is unknown + if (aStandardIndex !== -1) return -1; // Standard key comes first + if (bStandardIndex !== -1) return 1; // Standard key comes first - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } + // One is custom, one is unknown + if (aCustomPos !== undefined) return -1; // Custom key comes before unknown + if (bCustomPos !== undefined) return 1; // Custom key comes before unknown - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - // Handle custom extensions after standard keys - if (aCustomPos !== undefined) return -1; - if (bCustomPos !== undefined) return 1; - - // For unknown keys, sort alphabetically at the end + // Both are unknown - sort alphabetically return a.localeCompare(b); } //#endregion diff --git a/test/custom-extensions.test.ts b/test/custom-extensions.test.ts index 1641578..6cc2644 100644 --- a/test/custom-extensions.test.ts +++ b/test/custom-extensions.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'bun:test'; -import {parsers, printers} from '../src/index'; +import { parsers, printers } from '../src/index'; describe('Custom Extensions Support', () => { it('should handle custom extensions in top-level keys', () => { @@ -110,7 +110,7 @@ describe('Custom Extensions Support', () => { } }; -// @ts-expect-error We are mocking things here so we don't need to pass a print function + // @ts-expect-error We are mocking things here so we don't need to pass a print function const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }, () => ''); expect(result).toBeDefined(); expect(result).toContain('x-custom-field'); @@ -175,7 +175,7 @@ describe('Custom Extensions Support', () => { } }; - // @ts-expect-error We are mocking things here + // @ts-expect-error We are mocking things here const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }, () => ''); expect(result).toBeDefined(); @@ -184,7 +184,7 @@ describe('Custom Extensions Support', () => { } const resultString = result.toString(); - + // Custom extensions should come after standard keys const openapiIndex = resultString.indexOf('openapi'); const infoIndex = resultString.indexOf('info'); @@ -217,7 +217,7 @@ describe('Custom Extensions Support', () => { } }; - // @ts-expect-error We are mocking things here + // @ts-expect-error We are mocking things here const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }, () => ''); expect(result).toBeDefined(); @@ -263,7 +263,7 @@ describe('Custom Extensions Support', () => { } }; - // @ts-expect-error We are mocking things here + // @ts-expect-error We are mocking things here const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }, () => ''); expect(result).toBeDefined(); @@ -272,7 +272,7 @@ describe('Custom Extensions Support', () => { } const resultString = result.toString(); - + const summaryIndex = resultString.indexOf('summary'); const responsesIndex = resultString.indexOf('responses'); const xRateLimitIndex = resultString.indexOf('x-rate-limit'); @@ -306,7 +306,7 @@ describe('Custom Extensions Support', () => { } }; - // @ts-expect-error We are mocking things here + // @ts-expect-error We are mocking things here const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }, () => ''); expect(result).toBeDefined(); @@ -345,7 +345,7 @@ describe('Custom Extensions Support', () => { } }; - // @ts-expect-error We are mocking things here + // @ts-expect-error We are mocking things here const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }, () => ''); expect(result).toBeDefined(); @@ -371,6 +371,45 @@ describe('Custom Extensions Support', () => { expect(anotherUnknownIndex).toBeLessThan(unknownFieldIndex); }); + it('should handle specific extension placement', () => { + const printer = printers?.['openapi-ast']; + expect(printer).toBeDefined(); + + const testData = { + isOpenAPI: true, + format: 'yaml', + content: { + 'info': { 'title': 'Test API', 'version': '1.0.0' }, + 'openapi': '3.0.0', + 'x-tagGroups': 'value', + 'tags': 'value', + 'paths': {}, + 'x-metadata': { 'custom': 'data' }, + } + }; + + // @ts-expect-error We are mocking things here + const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }, () => ''); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + const resultString = result.toString(); + + // Standard keys first, then custom extensions, then unknown keys alphabetically + const openapiIndex = resultString.indexOf('openapi'); + const infoIndex = resultString.indexOf('info'); + const pathsIndex = resultString.indexOf('paths'); + const xTagGroupsIndex = resultString.indexOf('x-tagGroups:'); + const tagsIndex = resultString.indexOf('tags:'); + + expect(openapiIndex).toBeLessThan(infoIndex); + expect(infoIndex).toBeLessThan(pathsIndex); + expect(tagsIndex).toBeLessThan(xTagGroupsIndex); + }); + it('should handle mixed custom extensions and unknown keys', () => { const printer = printers?.['openapi-ast']; expect(printer).toBeDefined(); @@ -389,7 +428,7 @@ describe('Custom Extensions Support', () => { } }; - // @ts-expect-error We are mocking things here + // @ts-expect-error We are mocking things here const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }, () => ''); expect(result).toBeDefined(); @@ -398,7 +437,7 @@ describe('Custom Extensions Support', () => { } const resultString = result.toString(); - + // Standard keys first, then custom extensions, then unknown keys alphabetically const openapiIndex = resultString.indexOf('openapi'); const infoIndex = resultString.indexOf('info'); diff --git a/test/key-ordering.test.ts b/test/key-ordering.test.ts index 55c5947..756f060 100644 --- a/test/key-ordering.test.ts +++ b/test/key-ordering.test.ts @@ -140,8 +140,6 @@ describe('Key Ordering Tests', () => { throw new Error('Result is undefined'); } - console.log('result', result); - const resultString = result.toString(); const testIndex = resultString.indexOf('test"'); @@ -198,7 +196,6 @@ describe('Key Ordering Tests', () => { // @ts-expect-error We are mocking things here so we don't need to pass a print function const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }); - console.log('result', result); expect(result).toBeDefined(); if (!result) { diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 09190ce..017c4ce 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from 'bun:test'; -import plugin from '../dist/index.js'; +import { parsers, printers } from '../src/index.js'; describe('Prettier OpenAPI Plugin', () => { it('should format OpenAPI JSON files', () => { - const parser = plugin.parsers?.['openapi-parser']; - const printer = plugin.printers?.['openapi-ast']; - + const parser = parsers?.['openapi-parser']; + const printer = printers?.['openapi-ast']; + expect(parser).toBeDefined(); expect(printer).toBeDefined(); @@ -39,7 +39,7 @@ describe('Prettier OpenAPI Plugin', () => { // Format the parsed content // @ts-expect-error We are mocking things here const result = printer?.print({ getNode: () => parsed }, { tabWidth: 2 }, () => ''); - + expect(result).toBeDefined(); expect(result).toContain('"openapi"'); expect(result).toContain('"info"'); @@ -53,15 +53,15 @@ describe('Prettier OpenAPI Plugin', () => { const openapiIndex = result.toString().indexOf('"openapi"'); const infoIndex = result.toString().indexOf('"info"'); const pathsIndex = result.toString().indexOf('"paths"'); - + expect(openapiIndex).toBeLessThan(infoIndex); expect(infoIndex).toBeLessThan(pathsIndex); }); it('should format OpenAPI YAML files', () => { - const parser = plugin.parsers?.['openapi-parser']; - const printer = plugin.printers?.['openapi-ast']; - + const parser = parsers?.['openapi-parser']; + const printer = printers?.['openapi-ast']; + expect(parser).toBeDefined(); expect(printer).toBeDefined(); @@ -87,12 +87,12 @@ openapi: 3.0.0`; // Format the parsed content // @ts-expect-error We are mocking things here const result = printer?.print({ getNode: () => parsed }, { tabWidth: 2 }, () => ''); - + expect(result).toBeDefined(); expect(result).toContain('openapi:'); expect(result).toContain('info:'); expect(result).toContain('paths:'); - + // Verify that the YAML contains the expected keys // Note: YAML key ordering may not be working correctly yet expect(result).toContain('openapi:'); @@ -102,21 +102,21 @@ openapi: 3.0.0`; if (!result) { throw new Error('Result is undefined'); } - + // For now, just verify the keys exist (key ordering in YAML needs investigation) const openapiIndex = result.toString().indexOf('openapi:'); const infoIndex = result.toString().indexOf('info:'); const pathsIndex = result.toString().indexOf('paths:'); - + expect(openapiIndex).toBeGreaterThanOrEqual(0); expect(infoIndex).toBeGreaterThanOrEqual(0); expect(pathsIndex).toBeGreaterThanOrEqual(0); }); it('should format Swagger 2.0 JSON files', () => { - const parser = plugin.parsers?.['openapi-parser']; - const printer = plugin.printers?.['openapi-ast']; - + const parser = parsers?.['openapi-parser']; + const printer = printers?.['openapi-ast']; + expect(parser).toBeDefined(); expect(printer).toBeDefined(); @@ -155,7 +155,7 @@ openapi: 3.0.0`; // Format the parsed content // @ts-expect-error We are mocking things here const result = printer?.print({ getNode: () => parsed }, { tabWidth: 2 }, () => ''); - + expect(result).toBeDefined(); expect(result).toContain('"swagger"'); expect(result).toContain('"info"'); @@ -165,13 +165,13 @@ openapi: 3.0.0`; if (!result) { throw new Error('Result is undefined'); } - + // Verify correct Swagger 2.0 key ordering const swaggerIndex = result.toString().indexOf('"swagger"'); const infoIndex = result.toString().indexOf('"info"'); const pathsIndex = result.toString().indexOf('"paths"'); const definitionsIndex = result.toString().indexOf('"definitions"'); - + expect(swaggerIndex).toBeLessThan(infoIndex); expect(infoIndex).toBeLessThan(pathsIndex); expect(pathsIndex).toBeLessThan(definitionsIndex); @@ -181,9 +181,9 @@ openapi: 3.0.0`; describe('Key Ordering Tests', () => { describe('Top-level key ordering', () => { it('should sort OpenAPI 3.0+ keys correctly', () => { - const parser = plugin.parsers?.['openapi-parser']; - const printer = plugin.printers?.['openapi-ast']; - + const parser = parsers?.['openapi-parser']; + const printer = printers?.['openapi-ast']; + expect(parser).toBeDefined(); expect(printer).toBeDefined(); @@ -233,9 +233,9 @@ describe('Key Ordering Tests', () => { }); it('should sort Swagger 2.0 keys correctly', () => { - const parser = plugin.parsers?.['openapi-parser']; - const printer = plugin.printers?.['openapi-ast']; - + const parser = parsers?.['openapi-parser']; + const printer = printers?.['openapi-ast']; + expect(parser).toBeDefined(); expect(printer).toBeDefined(); diff --git a/test/simple-ordering.test.ts b/test/simple-ordering.test.ts index 5b1d8fa..4a77bd2 100644 --- a/test/simple-ordering.test.ts +++ b/test/simple-ordering.test.ts @@ -23,7 +23,7 @@ describe('Simple Key Ordering Tests', () => { // @ts-expect-error We are mocking things here const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }, () => ''); expect(result).toBeDefined(); - + if (!result) { throw new Error('Result is undefined'); } @@ -86,7 +86,7 @@ describe('Simple Key Ordering Tests', () => { } const resultString = result.toString(); - + // Check that operation keys appear in the correct order const summaryIndex = resultString.indexOf('summary'); const operationIdIndex = resultString.indexOf('operationId'); @@ -141,7 +141,7 @@ describe('Simple Key Ordering Tests', () => { } const resultString = result.toString(); - + // Check that info keys appear in the correct order const titleIndex = resultString.indexOf('title'); const versionIndex = resultString.indexOf('version'); @@ -175,13 +175,12 @@ describe('Simple Key Ordering Tests', () => { // @ts-expect-error We are mocking things here const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 }, () => ''); - console.log('result', result); expect(result).toBeDefined(); if (!result) { throw new Error('Result is undefined'); } - + // Custom extensions should come after standard keys const openapiIndex = result.toString().indexOf('openapi'); const infoIndex = result.toString().indexOf('info');