mirror of
https://github.com/LukeHagar/prettier-plugin-openapi.git
synced 2025-12-06 20:57:47 +00:00
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:
12
package.json
12
package.json
@@ -2,11 +2,19 @@
|
||||
"name": "prettier-plugin-openapi",
|
||||
"version": "1.0.0",
|
||||
"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",
|
||||
"module": "dist/index.js",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist"
|
||||
"dist",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
@@ -23,8 +31,6 @@
|
||||
"json",
|
||||
"formatting"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"prettier": "^3.0.0"
|
||||
},
|
||||
|
||||
@@ -85,37 +85,9 @@ export function getVendorExtensions(): Record<string, Record<string, number>> {
|
||||
// Try automatic discovery first
|
||||
return loadAllVendorExtensions();
|
||||
} 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
|
||||
const extensions: Record<string, Record<string, number>> = {};
|
||||
|
||||
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;
|
||||
// Return empty extensions if automatic discovery fails
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
6
src/extensions/vendor/example-usage.ts
vendored
6
src/extensions/vendor/example-usage.ts
vendored
@@ -4,19 +4,19 @@
|
||||
|
||||
// Function-based extensions with before/after helpers
|
||||
export const extensions = {
|
||||
'top-level': (before, after) => {
|
||||
'top-level': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-example-before-info': before('info'), // Before 'info'
|
||||
'x-example-after-paths': after('paths'), // After 'paths'
|
||||
};
|
||||
},
|
||||
'operation': (before, after) => {
|
||||
'operation': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-example-before-parameters': before('parameters'), // Before 'parameters'
|
||||
'x-example-after-responses': after('responses'), // After 'responses'
|
||||
};
|
||||
},
|
||||
'schema': (before, after) => {
|
||||
'schema': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-example-validation': after('type'), // After 'type'
|
||||
'x-example-example': after('example'), // After 'example'
|
||||
|
||||
10
src/extensions/vendor/postman.ts
vendored
10
src/extensions/vendor/postman.ts
vendored
@@ -8,23 +8,23 @@
|
||||
import { defineVendorExtensions } from "..";
|
||||
|
||||
// Function-based extensions with before/after helpers
|
||||
export const extensions = defineVendorExtensions({
|
||||
'top-level': (before, after) => {
|
||||
export const extensions = {
|
||||
'top-level': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-postman-collection': before('info'), // Before 'info'
|
||||
'x-postman-version': after('paths'), // After 'paths'
|
||||
};
|
||||
},
|
||||
'operation': (before, after) => {
|
||||
'operation': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-postman-test': after('responses'), // After 'responses'
|
||||
'x-postman-pre-request': before('parameters'), // Before 'parameters'
|
||||
};
|
||||
},
|
||||
'schema': (before, after) => {
|
||||
'schema': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-postman-example': after('example'), // After 'example'
|
||||
'x-postman-mock': after('deprecated'), // After 'deprecated'
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
12
src/extensions/vendor/redoc.ts
vendored
12
src/extensions/vendor/redoc.ts
vendored
@@ -8,28 +8,28 @@
|
||||
import { defineVendorExtensions } from "..";
|
||||
|
||||
// Function-based extensions with before/after helpers
|
||||
export const extensions = defineVendorExtensions({
|
||||
'top-level': (before, after) => {
|
||||
export const extensions = {
|
||||
'top-level': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-redoc-version': before('info'), // Before 'info'
|
||||
'x-redoc-theme': after('paths'), // After 'paths'
|
||||
};
|
||||
},
|
||||
'info': (before, after) => {
|
||||
'info': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-redoc-info': after('version'), // After 'version'
|
||||
};
|
||||
},
|
||||
'operation': (before, after) => {
|
||||
'operation': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-redoc-group': after('tags'), // After 'tags'
|
||||
'x-redoc-hide': before('responses'), // Before 'responses'
|
||||
};
|
||||
},
|
||||
'schema': (before, after) => {
|
||||
'schema': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-redoc-example': after('example'), // After 'example'
|
||||
'x-redoc-readonly': after('deprecated'), // After 'deprecated'
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
30
src/extensions/vendor/speakeasy.ts
vendored
30
src/extensions/vendor/speakeasy.ts
vendored
@@ -8,74 +8,74 @@
|
||||
import { defineVendorExtensions } from '../index';
|
||||
|
||||
// Function-based extensions with before/after helpers
|
||||
export const extensions = defineVendorExtensions({
|
||||
'top-level': (before, after) => {
|
||||
export const extensions = {
|
||||
'top-level': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-speakeasy-sdk': before('info'), // Before 'info'
|
||||
'x-speakeasy-auth': after('paths'), // After 'paths'
|
||||
};
|
||||
},
|
||||
'info': (before, after) => {
|
||||
'info': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-speakeasy-info': after('version'), // After 'version'
|
||||
};
|
||||
},
|
||||
'operation': (before, after) => {
|
||||
'operation': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-speakeasy-retries': after('parameters'), // After 'parameters'
|
||||
'x-speakeasy-timeout': before('responses'), // Before 'responses'
|
||||
'x-speakeasy-cache': after('servers'), // After 'servers'
|
||||
};
|
||||
},
|
||||
'schema': (before, after) => {
|
||||
'schema': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-speakeasy-validation': after('type'), // After 'type'
|
||||
'x-speakeasy-example': after('example'), // After 'example'
|
||||
};
|
||||
},
|
||||
'parameter': (before, after) => {
|
||||
'parameter': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-speakeasy-param': after('schema'), // After 'schema'
|
||||
};
|
||||
},
|
||||
'response': (before, after) => {
|
||||
'response': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-speakeasy-response': after('description'), // After 'description'
|
||||
};
|
||||
},
|
||||
'securityScheme': (before, after) => {
|
||||
'securityScheme': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-speakeasy-auth': after('type'), // After 'type'
|
||||
};
|
||||
},
|
||||
'server': (before, after) => {
|
||||
'server': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-speakeasy-server': after('url'), // After 'url'
|
||||
};
|
||||
},
|
||||
'tag': (before, after) => {
|
||||
'tag': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-speakeasy-tag': after('name'), // After 'name'
|
||||
};
|
||||
},
|
||||
'externalDocs': (before, after) => {
|
||||
'externalDocs': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-speakeasy-docs': after('url'), // After 'url'
|
||||
};
|
||||
},
|
||||
'webhook': (before, after) => {
|
||||
'webhook': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-speakeasy-webhook': after('operationId'), // After 'operationId'
|
||||
};
|
||||
},
|
||||
'definitions': (before, after) => {
|
||||
'definitions': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-speakeasy-definition': after('type'), // After 'type'
|
||||
};
|
||||
},
|
||||
'securityDefinitions': (before, after) => {
|
||||
'securityDefinitions': (before: (key: string) => number, after: (key: string) => number) => {
|
||||
return {
|
||||
'x-speakeasy-security': after('type'), // After 'type'
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
878
src/index.ts
878
src/index.ts
File diff suppressed because it is too large
Load Diff
@@ -87,6 +87,7 @@ export const OPERATION_KEYS = [
|
||||
'deprecated',
|
||||
'security',
|
||||
'servers', // OpenAPI 3.0+
|
||||
'externalDocs', // OpenAPI 3.0+
|
||||
] as const;
|
||||
|
||||
// Parameter keys in preferred order
|
||||
@@ -197,8 +198,6 @@ export const SCHEMA_KEYS = [
|
||||
'contentEncoding', // JSON Schema draft
|
||||
'contentMediaType', // JSON Schema draft
|
||||
'contentSchema', // JSON Schema draft
|
||||
'unevaluatedItems', // JSON Schema draft
|
||||
'unevaluatedProperties', // JSON Schema draft
|
||||
] as const;
|
||||
|
||||
// Response keys in preferred order
|
||||
|
||||
192
test/file-detection.test.ts
Normal file
192
test/file-detection.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user