Refactor OpenAPI plugin: remove unused index.ts, update package.json with author info, enhance file detection logic, and add comprehensive tests for OpenAPI file detection.

This commit is contained in:
Luke Hagar
2025-09-25 16:32:08 +00:00
parent 61b49e4616
commit 8ffbcc25fa
10 changed files with 424 additions and 758 deletions

View File

@@ -1 +0,0 @@
console.log("Hello via Bun!");

View File

@@ -2,11 +2,19 @@
"name": "prettier-plugin-openapi", "name": "prettier-plugin-openapi",
"version": "1.0.0", "version": "1.0.0",
"description": "A Prettier plugin for formatting OpenAPI/Swagger JSON and YAML files", "description": "A Prettier plugin for formatting OpenAPI/Swagger JSON and YAML files",
"author": {
"name": "Luke Hagar",
"email": "lukeslakemail@gmail.com",
"url": "https://LukeHagar.com/"
},
"license": "MIT",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.js", "module": "dist/index.js",
"type": "module", "type": "module",
"files": [ "files": [
"dist" "dist",
"README.md",
"LICENSE"
], ],
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
@@ -23,8 +31,6 @@
"json", "json",
"formatting" "formatting"
], ],
"author": "",
"license": "MIT",
"peerDependencies": { "peerDependencies": {
"prettier": "^3.0.0" "prettier": "^3.0.0"
}, },

View File

@@ -85,37 +85,9 @@ export function getVendorExtensions(): Record<string, Record<string, number>> {
// Try automatic discovery first // Try automatic discovery first
return loadAllVendorExtensions(); return loadAllVendorExtensions();
} catch (error) { } catch (error) {
console.warn('Automatic vendor discovery failed, falling back to manual list:', error); console.warn('Automatic vendor discovery failed, falling back to empty extensions:', error);
// Fallback to manual list // Return empty extensions if automatic discovery fails
const extensions: Record<string, Record<string, number>> = {}; return {};
const vendorModules = [
require('./vendor/speakeasy'),
require('./vendor/example-usage'),
// Add more vendor files here as they are created
];
for (const vendorModule of vendorModules) {
if (vendorModule && vendorModule.extensions) {
for (const [context, contextFunction] of Object.entries(vendorModule.extensions)) {
if (typeof contextFunction === 'function') {
// Create context-specific before/after functions
const contextBefore = (key: string) => before(context as keyof ContextKeys, key);
const contextAfter = (key: string) => after(context as keyof ContextKeys, key);
// Execute the function to get the extensions
const contextExtensions = contextFunction(contextBefore, contextAfter);
if (!extensions[context]) {
extensions[context] = {};
}
Object.assign(extensions[context], contextExtensions);
}
}
}
}
return extensions;
} }
} }

View File

@@ -4,19 +4,19 @@
// Function-based extensions with before/after helpers // Function-based extensions with before/after helpers
export const extensions = { export const extensions = {
'top-level': (before, after) => { 'top-level': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-example-before-info': before('info'), // Before 'info' 'x-example-before-info': before('info'), // Before 'info'
'x-example-after-paths': after('paths'), // After 'paths' 'x-example-after-paths': after('paths'), // After 'paths'
}; };
}, },
'operation': (before, after) => { 'operation': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-example-before-parameters': before('parameters'), // Before 'parameters' 'x-example-before-parameters': before('parameters'), // Before 'parameters'
'x-example-after-responses': after('responses'), // After 'responses' 'x-example-after-responses': after('responses'), // After 'responses'
}; };
}, },
'schema': (before, after) => { 'schema': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-example-validation': after('type'), // After 'type' 'x-example-validation': after('type'), // After 'type'
'x-example-example': after('example'), // After 'example' 'x-example-example': after('example'), // After 'example'

View File

@@ -8,23 +8,23 @@
import { defineVendorExtensions } from ".."; import { defineVendorExtensions } from "..";
// Function-based extensions with before/after helpers // Function-based extensions with before/after helpers
export const extensions = defineVendorExtensions({ export const extensions = {
'top-level': (before, after) => { 'top-level': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-postman-collection': before('info'), // Before 'info' 'x-postman-collection': before('info'), // Before 'info'
'x-postman-version': after('paths'), // After 'paths' 'x-postman-version': after('paths'), // After 'paths'
}; };
}, },
'operation': (before, after) => { 'operation': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-postman-test': after('responses'), // After 'responses' 'x-postman-test': after('responses'), // After 'responses'
'x-postman-pre-request': before('parameters'), // Before 'parameters' 'x-postman-pre-request': before('parameters'), // Before 'parameters'
}; };
}, },
'schema': (before, after) => { 'schema': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-postman-example': after('example'), // After 'example' 'x-postman-example': after('example'), // After 'example'
'x-postman-mock': after('deprecated'), // After 'deprecated' 'x-postman-mock': after('deprecated'), // After 'deprecated'
}; };
} }
}); };

View File

@@ -8,28 +8,28 @@
import { defineVendorExtensions } from ".."; import { defineVendorExtensions } from "..";
// Function-based extensions with before/after helpers // Function-based extensions with before/after helpers
export const extensions = defineVendorExtensions({ export const extensions = {
'top-level': (before, after) => { 'top-level': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-redoc-version': before('info'), // Before 'info' 'x-redoc-version': before('info'), // Before 'info'
'x-redoc-theme': after('paths'), // After 'paths' 'x-redoc-theme': after('paths'), // After 'paths'
}; };
}, },
'info': (before, after) => { 'info': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-redoc-info': after('version'), // After 'version' 'x-redoc-info': after('version'), // After 'version'
}; };
}, },
'operation': (before, after) => { 'operation': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-redoc-group': after('tags'), // After 'tags' 'x-redoc-group': after('tags'), // After 'tags'
'x-redoc-hide': before('responses'), // Before 'responses' 'x-redoc-hide': before('responses'), // Before 'responses'
}; };
}, },
'schema': (before, after) => { 'schema': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-redoc-example': after('example'), // After 'example' 'x-redoc-example': after('example'), // After 'example'
'x-redoc-readonly': after('deprecated'), // After 'deprecated' 'x-redoc-readonly': after('deprecated'), // After 'deprecated'
}; };
} }
}); };

View File

@@ -8,74 +8,74 @@
import { defineVendorExtensions } from '../index'; import { defineVendorExtensions } from '../index';
// Function-based extensions with before/after helpers // Function-based extensions with before/after helpers
export const extensions = defineVendorExtensions({ export const extensions = {
'top-level': (before, after) => { 'top-level': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-speakeasy-sdk': before('info'), // Before 'info' 'x-speakeasy-sdk': before('info'), // Before 'info'
'x-speakeasy-auth': after('paths'), // After 'paths' 'x-speakeasy-auth': after('paths'), // After 'paths'
}; };
}, },
'info': (before, after) => { 'info': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-speakeasy-info': after('version'), // After 'version' 'x-speakeasy-info': after('version'), // After 'version'
}; };
}, },
'operation': (before, after) => { 'operation': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-speakeasy-retries': after('parameters'), // After 'parameters' 'x-speakeasy-retries': after('parameters'), // After 'parameters'
'x-speakeasy-timeout': before('responses'), // Before 'responses' 'x-speakeasy-timeout': before('responses'), // Before 'responses'
'x-speakeasy-cache': after('servers'), // After 'servers' 'x-speakeasy-cache': after('servers'), // After 'servers'
}; };
}, },
'schema': (before, after) => { 'schema': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-speakeasy-validation': after('type'), // After 'type' 'x-speakeasy-validation': after('type'), // After 'type'
'x-speakeasy-example': after('example'), // After 'example' 'x-speakeasy-example': after('example'), // After 'example'
}; };
}, },
'parameter': (before, after) => { 'parameter': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-speakeasy-param': after('schema'), // After 'schema' 'x-speakeasy-param': after('schema'), // After 'schema'
}; };
}, },
'response': (before, after) => { 'response': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-speakeasy-response': after('description'), // After 'description' 'x-speakeasy-response': after('description'), // After 'description'
}; };
}, },
'securityScheme': (before, after) => { 'securityScheme': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-speakeasy-auth': after('type'), // After 'type' 'x-speakeasy-auth': after('type'), // After 'type'
}; };
}, },
'server': (before, after) => { 'server': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-speakeasy-server': after('url'), // After 'url' 'x-speakeasy-server': after('url'), // After 'url'
}; };
}, },
'tag': (before, after) => { 'tag': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-speakeasy-tag': after('name'), // After 'name' 'x-speakeasy-tag': after('name'), // After 'name'
}; };
}, },
'externalDocs': (before, after) => { 'externalDocs': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-speakeasy-docs': after('url'), // After 'url' 'x-speakeasy-docs': after('url'), // After 'url'
}; };
}, },
'webhook': (before, after) => { 'webhook': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-speakeasy-webhook': after('operationId'), // After 'operationId' 'x-speakeasy-webhook': after('operationId'), // After 'operationId'
}; };
}, },
'definitions': (before, after) => { 'definitions': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-speakeasy-definition': after('type'), // After 'type' 'x-speakeasy-definition': after('type'), // After 'type'
}; };
}, },
'securityDefinitions': (before, after) => { 'securityDefinitions': (before: (key: string) => number, after: (key: string) => number) => {
return { return {
'x-speakeasy-security': after('type'), // After 'type' 'x-speakeasy-security': after('type'), // After 'type'
}; };
} }
}); };

File diff suppressed because it is too large Load Diff

View File

@@ -87,6 +87,7 @@ export const OPERATION_KEYS = [
'deprecated', 'deprecated',
'security', 'security',
'servers', // OpenAPI 3.0+ 'servers', // OpenAPI 3.0+
'externalDocs', // OpenAPI 3.0+
] as const; ] as const;
// Parameter keys in preferred order // Parameter keys in preferred order
@@ -197,8 +198,6 @@ export const SCHEMA_KEYS = [
'contentEncoding', // JSON Schema draft 'contentEncoding', // JSON Schema draft
'contentMediaType', // JSON Schema draft 'contentMediaType', // JSON Schema draft
'contentSchema', // JSON Schema draft 'contentSchema', // JSON Schema draft
'unevaluatedItems', // JSON Schema draft
'unevaluatedProperties', // JSON Schema draft
] as const; ] as const;
// Response keys in preferred order // Response keys in preferred order

192
test/file-detection.test.ts Normal file
View File

@@ -0,0 +1,192 @@
import { describe, it, expect } from 'bun:test';
import plugin from '../src/index';
describe('File Detection Tests', () => {
it('should detect OpenAPI root files', () => {
const yamlParser = plugin.parsers?.['openapi-yaml-parser'];
expect(yamlParser).toBeDefined();
const testYaml = `openapi: 3.0.0
info:
title: Test API
version: 1.0.0
paths:
/test:
get:
responses:
'200':
description: Success`;
// @ts-ignore We are mocking things here
const result = yamlParser?.parse(testYaml, { filepath: 'openapi.yaml' });
expect(result).toBeDefined();
expect(result?.type).toBe('openapi-yaml');
expect(result?.content.openapi).toBe('3.0.0');
});
it('should detect partial schema files', () => {
const yamlParser = plugin.parsers?.['openapi-yaml-parser'];
expect(yamlParser).toBeDefined();
const schemaYaml = `type: object
properties:
id:
type: integer
name:
type: string
required:
- id
- name`;
// @ts-ignore We are mocking things here
const result = yamlParser?.parse(schemaYaml, { filepath: 'components/schemas/User.yaml' });
expect(result).toBeDefined();
expect(result?.type).toBe('openapi-yaml');
expect(result?.content.type).toBe('object');
});
it('should detect parameter files', () => {
const yamlParser = plugin.parsers?.['openapi-yaml-parser'];
expect(yamlParser).toBeDefined();
const parameterYaml = `name: id
in: path
required: true
description: User ID
schema:
type: integer`;
// @ts-ignore We are mocking things here
const result = yamlParser?.parse(parameterYaml, { filepath: 'components/parameters/UserId.yaml' });
expect(result).toBeDefined();
expect(result?.type).toBe('openapi-yaml');
expect(result?.content.name).toBe('id');
});
it('should detect response files', () => {
const yamlParser = plugin.parsers?.['openapi-yaml-parser'];
expect(yamlParser).toBeDefined();
const responseYaml = `description: User response
content:
application/json:
schema:
type: object
properties:
id:
type: integer
name:
type: string`;
// @ts-ignore We are mocking things here
const result = yamlParser?.parse(responseYaml, { filepath: 'components/responses/UserResponse.yaml' });
expect(result).toBeDefined();
expect(result?.type).toBe('openapi-yaml');
expect(result?.content.description).toBe('User response');
});
it('should detect path files', () => {
const yamlParser = plugin.parsers?.['openapi-yaml-parser'];
expect(yamlParser).toBeDefined();
const pathYaml = `get:
summary: Get users
responses:
'200':
description: Success
post:
summary: Create user
requestBody:
content:
application/json:
schema:
type: object`;
// @ts-ignore We are mocking things here
const result = yamlParser?.parse(pathYaml, { filepath: 'paths/users.yaml' });
expect(result).toBeDefined();
expect(result?.type).toBe('openapi-yaml');
expect(result?.content.get).toBeDefined();
expect(result?.content.post).toBeDefined();
});
it('should detect security scheme files', () => {
const yamlParser = plugin.parsers?.['openapi-yaml-parser'];
expect(yamlParser).toBeDefined();
const securityYaml = `type: http
scheme: bearer
bearerFormat: JWT
description: JWT authentication`;
// @ts-ignore We are mocking things here
const result = yamlParser?.parse(securityYaml, { filepath: 'components/securitySchemes/BearerAuth.yaml' });
expect(result).toBeDefined();
expect(result?.type).toBe('openapi-yaml');
expect(result?.content.type).toBe('http');
});
it('should reject non-OpenAPI files', () => {
const yamlParser = plugin.parsers?.['openapi-yaml-parser'];
expect(yamlParser).toBeDefined();
const nonOpenAPIYaml = `name: John
age: 30
city: New York`;
// @ts-ignore We are mocking things here
expect(() => yamlParser?.parse(nonOpenAPIYaml, { filepath: 'config/data.yaml' })).toThrow('Not an OpenAPI file');
});
it('should accept files in OpenAPI directories even with simple content', () => {
const yamlParser = plugin.parsers?.['openapi-yaml-parser'];
expect(yamlParser).toBeDefined();
const simpleYaml = `name: John
age: 30
city: New York`;
// @ts-ignore We are mocking things here
const result = yamlParser?.parse(simpleYaml, { filepath: 'components/schemas/User.yaml' });
expect(result).toBeDefined();
expect(result?.type).toBe('openapi-yaml');
});
it('should support component directory patterns', () => {
const yamlParser = plugin.parsers?.['openapi-yaml-parser'];
expect(yamlParser).toBeDefined();
const componentYaml = `type: object
properties:
message:
type: string`;
// Test various component directory patterns
const paths = [
'components/schemas/Error.yaml',
'components/parameters/CommonPagination.yaml',
'components/responses/ErrorResponse.yaml',
'components/requestBodies/UserCreateBody.yaml',
'components/headers/RateLimitHeaders.yaml',
'components/examples/UserExample.yaml',
'components/securitySchemes/BearerAuth.yaml',
'components/links/UserCreatedLink.yaml',
'components/callbacks/NewMessageCallback.yaml',
'webhooks/messageCreated.yaml',
'paths/users.yaml'
];
paths.forEach(path => {
// @ts-ignore We are mocking things here
const result = yamlParser?.parse(componentYaml, { filepath: path });
expect(result).toBeDefined();
expect(result?.type).toBe('openapi-yaml');
});
});
});