Saving current state

This commit is contained in:
Luke Hagar
2025-09-25 01:36:23 +00:00
commit 22726c627a
38 changed files with 7285 additions and 0 deletions

34
.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

9
.prettierrc Normal file
View File

@@ -0,0 +1,9 @@
{
"plugins": ["./dist/index.js"],
"tabWidth": 2,
"printWidth": 80,
"useTabs": false,
"semi": true,
"singleQuote": false,
"trailingComma": "es5"
}

550
CUSTOMIZATION.md Normal file
View File

@@ -0,0 +1,550 @@
# Customizing Key Ordering
This document explains how to customize the key ordering in the Prettier OpenAPI plugin.
## Overview
The plugin uses configuration arrays and maps at the top of the `src/index.ts` file to determine the order of keys in OpenAPI/Swagger files. You can easily modify these arrays to change the sorting behavior.
## Configuration Arrays
All key ordering configuration is located at the top of `src/index.ts` in the "KEY ORDERING CONFIGURATION" section.
### Top-Level Keys
```typescript
const TOP_LEVEL_KEYS = [
'openapi',
'info',
'servers',
'paths',
'components',
'security',
'tags',
'externalDocs',
] as const;
```
### Info Section Keys
```typescript
const INFO_KEYS = [
'title',
'description',
'version',
'termsOfService',
'contact',
'license',
] as const;
```
### Contact Section Keys
```typescript
const CONTACT_KEYS = [
'name',
'url',
'email',
] as const;
```
### License Section Keys
```typescript
const LICENSE_KEYS = [
'name',
'url',
] as const;
```
### Components Section Keys
```typescript
const COMPONENTS_KEYS = [
'schemas',
'responses',
'parameters',
'examples',
'requestBodies',
'headers',
'securitySchemes',
'links',
'callbacks',
] as const;
```
### Operation Keys
```typescript
const OPERATION_KEYS = [
'tags',
'summary',
'description',
'operationId',
'parameters',
'requestBody',
'responses',
'callbacks',
'deprecated',
'security',
'servers',
] as const;
```
### Parameter Keys
```typescript
const PARAMETER_KEYS = [
'name',
'in',
'description',
'required',
'deprecated',
'allowEmptyValue',
'style',
'explode',
'allowReserved',
'schema',
'example',
'examples',
] as const;
```
### Schema Keys
```typescript
const SCHEMA_KEYS = [
'type',
'format',
'title',
'description',
'default',
'example',
'examples',
'enum',
'const',
'multipleOf',
'maximum',
'exclusiveMaximum',
'minimum',
'exclusiveMinimum',
'maxLength',
'minLength',
'pattern',
'maxItems',
'minItems',
'uniqueItems',
'maxProperties',
'minProperties',
'required',
'properties',
'patternProperties',
'additionalProperties',
'items',
'allOf',
'oneOf',
'anyOf',
'not',
'discriminator',
'xml',
'externalDocs',
'deprecated',
] as const;
```
### Response Keys
```typescript
const RESPONSE_KEYS = [
'description',
'headers',
'content',
'links',
] as const;
```
### Security Scheme Keys
```typescript
const SECURITY_SCHEME_KEYS = [
'type',
'description',
'name',
'in',
'scheme',
'bearerFormat',
'flows',
'openIdConnectUrl',
] as const;
```
### OAuth Flow Keys
```typescript
const OAUTH_FLOW_KEYS = [
'authorizationUrl',
'tokenUrl',
'refreshUrl',
'scopes',
] as const;
```
### Server Keys
```typescript
const SERVER_KEYS = [
'url',
'description',
'variables',
] as const;
```
### Server Variable Keys
```typescript
const SERVER_VARIABLE_KEYS = [
'enum',
'default',
'description',
] as const;
```
### Tag Keys
```typescript
const TAG_KEYS = [
'name',
'description',
'externalDocs',
] as const;
```
### External Docs Keys
```typescript
const EXTERNAL_DOCS_KEYS = [
'description',
'url',
] as const;
```
## How to Customize
### 1. Reorder Keys
To change the order of keys, simply rearrange the arrays:
```typescript
// Example: Put 'description' before 'title' in info section
const INFO_KEYS = [
'description', // Moved to first
'title',
'version',
'termsOfService',
'contact',
'license',
] as const;
```
### 2. Add New Keys
To add support for new keys, add them to the appropriate array:
```typescript
// Example: Add custom keys to info section
const INFO_KEYS = [
'title',
'description',
'version',
'customField', // New key
'termsOfService',
'contact',
'license',
] as const;
```
### 3. Remove Keys
To remove keys from sorting (they'll be sorted alphabetically), simply remove them from the arrays:
```typescript
// Example: Remove 'termsOfService' from info section
const INFO_KEYS = [
'title',
'description',
'version',
// 'termsOfService', // Removed
'contact',
'license',
] as const;
```
### 4. Create Custom Ordering
You can create completely custom ordering by modifying the arrays:
```typescript
// Example: Custom ordering for your organization's standards
const INFO_KEYS = [
'title',
'version',
'description',
'contact',
'license',
'termsOfService',
] as const;
```
## Special Sorting Rules
Some sections have special sorting rules that can't be changed by modifying arrays:
- **Paths**: Sorted by specificity (more specific paths first)
- **Response Codes**: Sorted numerically (200, 201, 400, 404, etc.)
- **Schema Properties**: Sorted alphabetically
- **Parameters**: Sorted alphabetically
- **Security Schemes**: Sorted alphabetically
## Testing Changes
After modifying the configuration arrays:
1. Build the plugin:
```bash
bun run build
```
2. Run tests to ensure nothing is broken:
```bash
bun test
```
3. Test with your OpenAPI files:
```bash
npx prettier --write your-api.yaml
```
## Examples
### Example 1: Prioritize Security
```typescript
const TOP_LEVEL_KEYS = [
'openapi',
'info',
'security', // Moved up
'servers',
'paths',
'components',
'tags',
'externalDocs',
] as const;
```
### Example 2: Custom Info Ordering
```typescript
const INFO_KEYS = [
'title',
'version',
'description',
'contact',
'license',
'termsOfService',
] as const;
```
### Example 3: Schema-First Components
```typescript
const COMPONENTS_KEYS = [
'schemas', // Already first, but emphasizing
'responses',
'parameters',
'examples',
'requestBodies',
'headers',
'securitySchemes',
'links',
'callbacks',
] as const;
```
## Custom Extensions
The plugin supports custom extensions (keys starting with `x-`) with configurable positioning. You can specify exactly where your custom extensions should appear in the sorted output.
### Configuration
Custom extensions are configured at the top of `src/index.ts` in the "CUSTOM EXTENSION CONFIGURATION" section:
```typescript
// Custom extensions for top-level OpenAPI keys
const CUSTOM_TOP_LEVEL_EXTENSIONS: Record<string, number> = {
'x-custom-field': 2, // Will be placed after 'info' (position 1)
'x-api-version': 0, // Will be placed before 'openapi'
};
// Custom extensions for info section
const CUSTOM_INFO_EXTENSIONS: Record<string, number> = {
'x-api-id': 1, // Will be placed after 'title' (position 0)
'x-version-info': 3, // Will be placed after 'version' (position 2)
};
```
### How Positioning Works
- **Position 0**: Before the first standard key
- **Position 1**: After the first standard key
- **Position 2**: After the second standard key
- **Position N**: After the Nth standard key
- **Position > standard keys length**: After all standard keys
### Examples
#### Example 1: Top-Level Custom Extensions
```typescript
const CUSTOM_TOP_LEVEL_EXTENSIONS: Record<string, number> = {
'x-api-version': 0, // Before 'openapi'
'x-custom-field': 2, // After 'info' (position 1)
'x-metadata': 8, // After all standard keys
};
```
**Result:**
```yaml
x-api-version: "1.0"
openapi: 3.0.0
info: ...
x-custom-field: "value"
servers: ...
paths: ...
components: ...
x-metadata: {...}
```
#### Example 2: Info Section Custom Extensions
```typescript
const CUSTOM_INFO_EXTENSIONS: Record<string, number> = {
'x-api-id': 1, // After 'title'
'x-version-info': 3, // After 'version'
'x-custom-info': 6, // After all standard keys
};
```
**Result:**
```yaml
info:
title: My API
x-api-id: "api-123"
description: API Description
version: 1.0.0
x-version-info: "v1.0.0-beta"
contact: ...
license: ...
x-custom-info: {...}
```
#### Example 3: Operation Custom Extensions
```typescript
const CUSTOM_OPERATION_EXTENSIONS: Record<string, number> = {
'x-rate-limit': 5, // After 'parameters' (position 4)
'x-custom-auth': 10, // After 'servers' (position 9)
};
```
**Result:**
```yaml
get:
tags: [...]
summary: ...
description: ...
operationId: ...
parameters: [...]
x-rate-limit: 100
requestBody: ...
responses: ...
callbacks: ...
deprecated: false
security: [...]
servers: [...]
x-custom-auth: "bearer"
```
### Available Extension Contexts
| Context | Configuration Object | Description |
|---------|---------------------|-------------|
| `top-level` | `CUSTOM_TOP_LEVEL_EXTENSIONS` | Root OpenAPI object |
| `info` | `CUSTOM_INFO_EXTENSIONS` | Info section |
| `components` | `CUSTOM_COMPONENTS_EXTENSIONS` | Components section |
| `operation` | `CUSTOM_OPERATION_EXTENSIONS` | HTTP operation objects |
| `parameter` | `CUSTOM_PARAMETER_EXTENSIONS` | Parameter objects |
| `schema` | `CUSTOM_SCHEMA_EXTENSIONS` | Schema objects |
| `response` | `CUSTOM_RESPONSE_EXTENSIONS` | Response objects |
| `securityScheme` | `CUSTOM_SECURITY_SCHEME_EXTENSIONS` | Security scheme objects |
| `server` | `CUSTOM_SERVER_EXTENSIONS` | Server objects |
| `tag` | `CUSTOM_TAG_EXTENSIONS` | Tag objects |
| `externalDocs` | `CUSTOM_EXTERNAL_DOCS_EXTENSIONS` | External docs objects |
### Unknown Keys Behavior
Keys that are not in the standard arrays or custom extensions configuration will be sorted alphabetically at the bottom of their respective objects.
**Example:**
```yaml
info:
title: My API
version: 1.0.0
# Standard keys in order
contact: ...
license: ...
# Custom extensions in configured order
x-api-id: "api-123"
# Unknown keys sorted alphabetically at the end
unknown-field: "value"
x-other-extension: "value"
```
### Complete Example
Here's a complete example of custom extension configuration:
```typescript
// Top-level custom extensions
const CUSTOM_TOP_LEVEL_EXTENSIONS: Record<string, number> = {
'x-api-version': 0, // Before 'openapi'
'x-custom-field': 2, // After 'info'
'x-metadata': 8, // After all standard keys
};
// Info section custom extensions
const CUSTOM_INFO_EXTENSIONS: Record<string, number> = {
'x-api-id': 1, // After 'title'
'x-version-info': 3, // After 'version'
};
// Operation custom extensions
const CUSTOM_OPERATION_EXTENSIONS: Record<string, number> = {
'x-rate-limit': 5, // After 'parameters'
'x-custom-auth': 10, // After 'servers'
};
// Schema custom extensions
const CUSTOM_SCHEMA_EXTENSIONS: Record<string, number> = {
'x-custom-type': 0, // Before 'type'
'x-validation-rules': 30, // After 'deprecated'
};
```
## Notes
- Keys not in the configuration arrays will be sorted alphabetically
- Custom extensions can be positioned anywhere in the key order
- Unknown keys (not in standard or custom lists) default to alphabetical sorting at the bottom
- The `as const` assertion ensures type safety
- Changes require rebuilding the plugin (`bun run build`)
- Test your changes with real OpenAPI files to ensure they work as expected

248
EXTENSIONS.md Normal file
View File

@@ -0,0 +1,248 @@
# Custom Extensions Support
This document explains the custom extensions functionality in the Prettier OpenAPI plugin.
## Overview
The plugin now supports configurable positioning of custom extensions (keys starting with `x-`) and ensures that unknown keys are sorted alphabetically at the bottom of their respective objects.
## Key Features
**Configurable Extension Positioning** - Specify exactly where custom extensions should appear
**Context-Aware Sorting** - Different extension configurations for different OpenAPI sections
**Unknown Key Handling** - Unknown keys default to alphabetical sorting at the bottom
**Type Safety** - Full TypeScript support with proper typing
**Comprehensive Testing** - 15 tests covering all functionality
## Configuration
All custom extension configuration is located at the top of `src/index.ts`:
```typescript
// ============================================================================
// CUSTOM EXTENSION CONFIGURATION
// ============================================================================
// Custom extensions for top-level OpenAPI keys
const CUSTOM_TOP_LEVEL_EXTENSIONS: Record<string, number> = {
'x-custom-field': 2, // Will be placed after 'info' (position 1)
'x-api-version': 0, // Will be placed before 'openapi'
};
// Custom extensions for info section
const CUSTOM_INFO_EXTENSIONS: Record<string, number> = {
'x-api-id': 1, // Will be placed after 'title' (position 0)
'x-version-info': 3, // Will be placed after 'version' (position 2)
};
```
## Supported Contexts
| Context | Configuration Object | Standard Keys Count | Description |
|---------|---------------------|-------------------|-------------|
| `top-level` | `CUSTOM_TOP_LEVEL_EXTENSIONS` | 8 | Root OpenAPI object |
| `info` | `CUSTOM_INFO_EXTENSIONS` | 6 | Info section |
| `components` | `CUSTOM_COMPONENTS_EXTENSIONS` | 9 | Components section |
| `operation` | `CUSTOM_OPERATION_EXTENSIONS` | 11 | HTTP operation objects |
| `parameter` | `CUSTOM_PARAMETER_EXTENSIONS` | 12 | Parameter objects |
| `schema` | `CUSTOM_SCHEMA_EXTENSIONS` | 31 | Schema objects |
| `response` | `CUSTOM_RESPONSE_EXTENSIONS` | 4 | Response objects |
| `securityScheme` | `CUSTOM_SECURITY_SCHEME_EXTENSIONS` | 8 | Security scheme objects |
| `server` | `CUSTOM_SERVER_EXTENSIONS` | 3 | Server objects |
| `tag` | `CUSTOM_TAG_EXTENSIONS` | 3 | Tag objects |
| `externalDocs` | `CUSTOM_EXTERNAL_DOCS_EXTENSIONS` | 2 | External docs objects |
## Positioning Rules
### Position Numbers
- **0**: Before the first standard key
- **1**: After the first standard key
- **2**: After the second standard key
- **N**: After the Nth standard key
- **> standard keys length**: After all standard keys
### Sorting Priority
1. **Custom extensions** (in configured order)
2. **Standard keys** (in predefined order)
3. **Unknown keys** (alphabetically sorted)
## Examples
### Example 1: Top-Level Extensions
```typescript
const CUSTOM_TOP_LEVEL_EXTENSIONS: Record<string, number> = {
'x-api-version': 0, // Before 'openapi'
'x-custom-field': 2, // After 'info'
'x-metadata': 8, // After all standard keys
};
```
**Result:**
```yaml
x-api-version: "1.0"
openapi: 3.0.0
info: ...
x-custom-field: "value"
servers: ...
paths: ...
components: ...
x-metadata: {...}
```
### Example 2: Operation Extensions
```typescript
const CUSTOM_OPERATION_EXTENSIONS: Record<string, number> = {
'x-rate-limit': 5, // After 'parameters'
'x-custom-auth': 10, // After 'servers'
};
```
**Result:**
```yaml
get:
tags: [...]
summary: ...
description: ...
operationId: ...
parameters: [...]
x-rate-limit: 100
requestBody: ...
responses: ...
callbacks: ...
deprecated: false
security: [...]
servers: [...]
x-custom-auth: "bearer"
```
### Example 3: Schema Extensions
```typescript
const CUSTOM_SCHEMA_EXTENSIONS: Record<string, number> = {
'x-custom-type': 0, // Before 'type'
'x-validation-rules': 30, // After 'deprecated'
};
```
**Result:**
```yaml
User:
x-custom-type: "entity"
type: object
properties: {...}
required: [...]
deprecated: false
x-validation-rules: "required"
```
## Unknown Keys Behavior
Keys that are not in the standard arrays or custom extensions configuration will be sorted alphabetically at the bottom of their respective objects.
**Example:**
```yaml
info:
title: My API
version: 1.0.0
# Standard keys in order
contact: ...
license: ...
# Custom extensions in configured order
x-api-id: "api-123"
# Unknown keys sorted alphabetically at the end
unknown-field: "value"
x-other-extension: "value"
```
## Testing
The plugin includes comprehensive tests for custom extensions:
```bash
# Run all tests
bun test
# Run only custom extension tests
bun test test/custom-extensions.test.ts
```
**Test Coverage:**
- ✅ Custom extensions in top-level keys
- ✅ Custom extensions in info section
- ✅ Custom extensions in operation objects
- ✅ Custom extensions in schema objects
- ✅ JSON formatting with custom extensions
- ✅ YAML formatting with custom extensions
- ✅ Unknown keys handling
## Usage
### 1. Configure Extensions
Edit the configuration arrays in `src/index.ts`:
```typescript
const CUSTOM_TOP_LEVEL_EXTENSIONS: Record<string, number> = {
'x-your-extension': 2, // Position after 'info'
};
```
### 2. Build the Plugin
```bash
bun run build
```
### 3. Use with Prettier
```bash
npx prettier --write your-api.yaml
```
## Best Practices
1. **Use Descriptive Names** - Choose clear, descriptive names for your extensions
2. **Consistent Positioning** - Use consistent positioning across similar contexts
3. **Document Extensions** - Document your custom extensions in your API documentation
4. **Test Changes** - Always test your configuration with real OpenAPI files
5. **Version Control** - Keep your extension configurations in version control
## Migration Guide
If you're upgrading from a version without custom extension support:
1. **No Breaking Changes** - Existing functionality remains unchanged
2. **Add Extensions Gradually** - Start with a few key extensions
3. **Test Thoroughly** - Test with your existing OpenAPI files
4. **Update Documentation** - Update your team's documentation
## Troubleshooting
### Common Issues
1. **Extensions Not Appearing** - Check that the extension is configured in the correct context
2. **Wrong Position** - Verify the position number is correct for the context
3. **Build Errors** - Ensure the plugin is rebuilt after configuration changes
### Debug Tips
1. **Check Context** - Use the context detection to ensure extensions are in the right place
2. **Verify Configuration** - Double-check your extension configuration
3. **Test Incrementally** - Add extensions one at a time to isolate issues
## Contributing
When adding new extension contexts:
1. Add the configuration object
2. Add the context to `CUSTOM_EXTENSIONS_MAP`
3. Add the context to `getContextKey` function
4. Add the context to `getStandardKeysForContext` function
5. Add tests for the new context
6. Update documentation
## License
This functionality is part of the Prettier OpenAPI plugin and follows the same MIT license.

204
README.md Normal file
View File

@@ -0,0 +1,204 @@
# Prettier Plugin OpenAPI
A Prettier plugin for formatting OpenAPI/Swagger JSON and YAML files with intelligent key sorting and proper indentation.
## Features
- 🎯 **OpenAPI/Swagger Support**: Formats both JSON and YAML OpenAPI specifications
- 🔄 **Smart Key Sorting**: Automatically sorts OpenAPI keys in the recommended order
- 📝 **YAML & JSON**: Supports both `.yaml/.yml` and `.json` file formats
- 🎨 **Consistent Formatting**: Applies consistent indentation and line breaks
-**Fast**: Built with performance in mind using modern JavaScript
## Installation
```bash
npm install --save-dev prettier-plugin-openapi
# or
yarn add --dev prettier-plugin-openapi
# or
bun add --dev prettier-plugin-openapi
```
## Usage
### Command Line
```bash
# Format a single file
npx prettier --write api.yaml
# Format all OpenAPI files in a directory
npx prettier --write "**/*.{openapi.json,openapi.yaml,swagger.json,swagger.yaml}"
# Format with specific options
npx prettier --write api.yaml --tab-width 4 --print-width 100
```
### Configuration
Add the plugin to your Prettier configuration:
**package.json**
```json
{
"prettier": {
"plugins": ["prettier-plugin-openapi"]
}
}
```
**.prettierrc**
```json
{
"plugins": ["prettier-plugin-openapi"],
"tabWidth": 2,
"printWidth": 80
}
```
**.prettierrc.js**
```javascript
module.exports = {
plugins: ['prettier-plugin-openapi'],
tabWidth: 2,
printWidth: 80,
};
```
## Supported File Extensions
- `.openapi.json`
- `.openapi.yaml`
- `.openapi.yml`
- `.swagger.json`
- `.swagger.yaml`
- `.swagger.yml`
## Key Sorting
The plugin automatically sorts OpenAPI keys in the recommended order:
### Top-level keys:
1. `openapi`
2. `info`
3. `servers`
4. `paths`
5. `components`
6. `security`
7. `tags`
8. `externalDocs`
### Info section:
1. `title`
2. `description`
3. `version`
4. `termsOfService`
5. `contact`
6. `license`
### Components section:
1. `schemas`
2. `responses`
3. `parameters`
4. `examples`
5. `requestBodies`
6. `headers`
7. `securitySchemes`
8. `links`
9. `callbacks`
## Examples
### Before (unformatted):
```yaml
paths:
/users:
get:
responses:
'200':
description: OK
components:
schemas:
User:
type: object
openapi: 3.0.0
info:
version: 1.0.0
title: My API
```
### After (formatted):
```yaml
openapi: 3.0.0
info:
title: My API
version: 1.0.0
paths:
/users:
get:
responses:
'200':
description: OK
components:
schemas:
User:
type: object
```
## Development
### Setup
```bash
# Install dependencies
bun install
# Build the plugin
bun run build
# Run tests
bun test
# Run demo
bun run test/demo.ts
```
### Project Structure
```
src/
index.ts # Main plugin implementation
test/
plugin.test.ts # Unit tests
demo.ts # Demo script
examples/
petstore.yaml # Example OpenAPI file
```
## Configuration Options
The plugin respects standard Prettier options:
- `tabWidth`: Number of spaces for indentation (default: 2)
- `printWidth`: Maximum line length (default: 80)
- `useTabs`: Use tabs instead of spaces (default: false)
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests for new functionality
5. Run the test suite
6. Submit a pull request
## License
MIT License - see LICENSE file for details.
## Related Projects
- [Prettier](https://prettier.io/) - The core formatter
- [OpenAPI Specification](https://swagger.io/specification/) - The OpenAPI specification
- [Swagger](https://swagger.io/) - API development tools

428
SUPPORTED_KEYS.md Normal file
View File

@@ -0,0 +1,428 @@
# Supported Keys - Complete Reference
This document provides a comprehensive list of all supported keys for Swagger 2.0, OpenAPI 3.0, 3.1, and 3.2 specifications.
## Top-Level Keys
### All Versions
- `swagger` (Swagger 2.0 only)
- `openapi` (OpenAPI 3.0+)
- `info`
- `paths`
- `security`
- `tags`
- `externalDocs`
### Version-Specific
- `jsonSchemaDialect` (OpenAPI 3.1+)
- `servers` (OpenAPI 3.0+)
- `webhooks` (OpenAPI 3.1+)
- `components` (OpenAPI 3.0+)
- `host` (Swagger 2.0 only)
- `basePath` (Swagger 2.0 only)
- `schemes` (Swagger 2.0 only)
- `consumes` (Swagger 2.0 only)
- `produces` (Swagger 2.0 only)
- `definitions` (Swagger 2.0 only)
- `parameters` (Swagger 2.0 only)
- `responses` (Swagger 2.0 only)
- `securityDefinitions` (Swagger 2.0 only)
## Info Section Keys
### All Versions
- `title`
- `description`
- `version`
- `termsOfService`
- `contact`
- `license`
### Version-Specific
- `summary` (OpenAPI 3.1+)
## Contact Section Keys
### All Versions
- `name`
- `url`
- `email`
## License Section Keys
### All Versions
- `name`
- `url`
## Components Section Keys (OpenAPI 3.0+)
### All Versions
- `schemas`
- `responses`
- `parameters`
- `examples`
- `requestBodies`
- `headers`
- `securitySchemes`
- `links`
- `callbacks`
### Version-Specific
- `pathItems` (OpenAPI 3.1+)
## Operation Keys
### All Versions
- `tags`
- `summary`
- `description`
- `operationId`
- `parameters`
- `responses`
- `deprecated`
- `security`
### Version-Specific
- `consumes` (Swagger 2.0 only)
- `produces` (Swagger 2.0 only)
- `requestBody` (OpenAPI 3.0+)
- `schemes` (Swagger 2.0 only)
- `callbacks` (OpenAPI 3.0+)
- `servers` (OpenAPI 3.0+)
## Parameter Keys
### All Versions
- `name`
- `in`
- `description`
- `required`
- `deprecated`
- `allowEmptyValue`
- `style`
- `explode`
- `allowReserved`
- `schema`
- `example`
- `examples`
### Swagger 2.0 Specific
- `type`
- `format`
- `items`
- `collectionFormat`
- `default`
- `maximum`
- `exclusiveMaximum`
- `minimum`
- `exclusiveMinimum`
- `maxLength`
- `minLength`
- `pattern`
- `maxItems`
- `minItems`
- `uniqueItems`
- `enum`
- `multipleOf`
## Schema Keys
### Core JSON Schema Keywords
- `$schema`
- `$id`
- `$ref`
- `$anchor`
- `$dynamicAnchor`
- `$dynamicRef`
- `$vocabulary`
- `$comment`
- `$defs`
- `$recursiveAnchor`
- `$recursiveRef`
### Basic Type and Format
- `type`
- `format`
- `title`
- `description`
- `default`
- `example`
- `examples`
- `enum`
- `const`
### Numeric Validation
- `multipleOf`
- `maximum`
- `exclusiveMaximum`
- `minimum`
- `exclusiveMinimum`
### String Validation
- `maxLength`
- `minLength`
- `pattern`
### Array Validation
- `maxItems`
- `minItems`
- `uniqueItems`
- `items`
- `prefixItems`
- `contains`
- `minContains`
- `maxContains`
- `unevaluatedItems`
### Object Validation
- `maxProperties`
- `minProperties`
- `required`
- `properties`
- `patternProperties`
- `additionalProperties`
- `unevaluatedProperties`
- `propertyNames`
- `dependentRequired`
- `dependentSchemas`
### Schema Composition
- `allOf`
- `oneOf`
- `anyOf`
- `not`
- `if`
- `then`
- `else`
### OpenAPI Specific
- `discriminator`
- `xml`
- `externalDocs`
- `deprecated`
### Additional JSON Schema Keywords
- `contentEncoding`
- `contentMediaType`
- `contentSchema`
## Response Keys
### All Versions
- `description`
- `headers`
### Version-Specific
- `content` (OpenAPI 3.0+)
- `schema` (Swagger 2.0 only)
- `examples` (Swagger 2.0 only)
- `links` (OpenAPI 3.0+)
## Security Scheme Keys
### All Versions
- `type`
- `description`
- `name`
- `in`
- `scheme`
- `bearerFormat`
- `openIdConnectUrl`
### Version-Specific
- `flows` (OpenAPI 3.0+)
- `flow` (Swagger 2.0 only)
- `authorizationUrl` (Swagger 2.0 only)
- `tokenUrl` (Swagger 2.0 only)
- `scopes` (Swagger 2.0 only)
## OAuth Flow Keys (OpenAPI 3.0+)
### All Versions
- `authorizationUrl`
- `tokenUrl`
- `refreshUrl`
- `scopes`
## Server Keys
### All Versions
- `url`
- `description`
- `variables`
## Server Variable Keys
### All Versions
- `enum`
- `default`
- `description`
## Tag Keys
### All Versions
- `name`
- `description`
- `externalDocs`
## External Docs Keys
### All Versions
- `description`
- `url`
## Webhook Keys (OpenAPI 3.1+)
### All Versions
- `tags`
- `summary`
- `description`
- `operationId`
- `parameters`
- `requestBody`
- `responses`
- `callbacks`
- `deprecated`
- `security`
- `servers`
## Special Sorting Rules
### Paths
- Sorted by specificity (more specific paths first)
- Paths with fewer parameters come before paths with more parameters
### Response Codes
- Sorted numerically (200, 201, 400, 404, etc.)
- Non-numeric codes sorted alphabetically
### Schema Properties
- Sorted alphabetically
### Parameters
- Sorted alphabetically
### Security Schemes
- Sorted alphabetically
### Definitions (Swagger 2.0)
- Sorted alphabetically
### Security Definitions (Swagger 2.0)
- Sorted alphabetically
## Version Detection
The plugin automatically detects the OpenAPI/Swagger version based on the presence of specific keys:
- **Swagger 2.0**: Contains `swagger` key
- **OpenAPI 3.0**: Contains `openapi` key with version 3.0.x
- **OpenAPI 3.1**: Contains `openapi` key with version 3.1.x
- **OpenAPI 3.2**: Contains `openapi` key with version 3.2.x
## Custom Extensions
All custom extensions (keys starting with `x-`) are supported and can be positioned using the configuration arrays at the top of `src/index.ts`.
## Unknown Keys
Keys that are not in the standard arrays or custom extensions configuration will be sorted alphabetically at the bottom of their respective objects.
## Examples
### Swagger 2.0 Example
```yaml
swagger: "2.0"
info:
title: My API
version: 1.0.0
host: api.example.com
basePath: /v1
schemes:
- https
paths:
/users:
get:
summary: Get users
responses:
'200':
description: OK
definitions:
User:
type: object
properties:
id:
type: integer
```
### OpenAPI 3.0 Example
```yaml
openapi: 3.0.0
info:
title: My API
version: 1.0.0
servers:
- url: https://api.example.com/v1
paths:
/users:
get:
summary: Get users
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
components:
schemas:
User:
type: object
properties:
id:
type: integer
```
### OpenAPI 3.1+ Example
```yaml
openapi: 3.1.0
info:
title: My API
version: 1.0.0
summary: API for managing users
servers:
- url: https://api.example.com/v1
paths:
/users:
get:
summary: Get users
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
webhooks:
userCreated:
post:
summary: User created webhook
responses:
'200':
description: OK
components:
schemas:
User:
type: object
properties:
id:
type: integer
```
## Notes
- The plugin supports all valid keys from all versions
- Version-specific keys are handled appropriately based on the detected version
- Custom extensions are fully supported with configurable positioning
- Unknown keys are sorted alphabetically at the bottom
- All sorting is consistent and predictable

221
VENDOR_DYNAMIC_FINAL.md Normal file
View File

@@ -0,0 +1,221 @@
# Dynamic Vendor Extension System
The Prettier OpenAPI Plugin now includes a powerful dynamic vendor extension system that automatically discovers and loads any number of vendor files.
## ✅ What Was Accomplished
### 🎯 Dynamic Vendor Loading
- **Automatic Discovery** - No configuration needed
- **Unlimited Vendors** - Add as many as you want
- **Zero Complexity** - Just create a TS file
- **Type Safe** - Full TypeScript support with IntelliSense
- **Flexible** - Support for all OpenAPI contexts
- **Maintainable** - Simple function-based approach
### 🏗️ System Architecture
- **`extensions/index.ts`** - Main extension system
- **`extensions/vendor-loader.ts`** - Automatic vendor discovery
- **`extensions/vendor/`** - Vendor extensions directory
- **Automatic Loading** - Discovers all TS files in vendor directory
### 🎛️ Ultra-Simple API
- **`before(key)`** - Position before a specific key
- **`after(key)`** - Position after a specific key
- **`export const extensions`** - Function-based extension definitions
## 📁 Final Structure
```
extensions/
├── index.ts # Main extension system
├── vendor-loader.ts # Automatic vendor discovery
├── vendor/ # Vendor extensions directory
│ ├── speakeasy.ts # Speakeasy extensions
│ ├── redoc.ts # Redoc extensions
│ ├── postman.ts # Postman extensions
│ ├── example-usage.ts # Example vendor
│ └── your-company.ts # Your custom extensions
└── README.md # Documentation
src/
├── keys.ts # Centralized key arrays
└── index.ts # Main plugin (imports from keys.ts)
```
## 🚀 Usage Examples
### Adding a New Vendor (Ultra-Simple)
```typescript
// extensions/vendor/your-company.ts
export const extensions = {
'top-level': (before, after) => {
return {
'x-your-company-api-key': before('info'), // Before 'info'
'x-your-company-version': after('paths'), // After 'paths'
};
},
'operation': (before, after) => {
return {
'x-your-company-rate-limit': after('parameters'), // After 'parameters'
'x-your-company-auth': before('responses'), // Before 'responses'
};
}
};
```
### Speakeasy Vendor
```typescript
// extensions/vendor/speakeasy.ts
export const extensions = {
'top-level': (before, after) => {
return {
'x-speakeasy-sdk': before('info'), // Before 'info'
'x-speakeasy-auth': after('paths'), // After 'paths'
};
},
'operation': (before, after) => {
return {
'x-speakeasy-retries': after('parameters'), // After 'parameters'
'x-speakeasy-timeout': before('responses'), // Before 'responses'
'x-speakeasy-cache': after('servers'), // After 'servers'
};
}
};
```
### Redoc Vendor
```typescript
// extensions/vendor/redoc.ts
export const extensions = {
'top-level': (before, after) => {
return {
'x-redoc-version': before('info'), // Before 'info'
'x-redoc-theme': after('paths'), // After 'paths'
};
},
'operation': (before, after) => {
return {
'x-redoc-group': after('tags'), // After 'tags'
'x-redoc-hide': before('responses'), // Before 'responses'
};
}
};
```
### Postman Vendor
```typescript
// extensions/vendor/postman.ts
export const extensions = {
'top-level': (before, after) => {
return {
'x-postman-collection': before('info'), // Before 'info'
'x-postman-version': after('paths'), // After 'paths'
};
},
'operation': (before, after) => {
return {
'x-postman-test': after('responses'), // After 'responses'
'x-postman-pre-request': before('parameters'), // Before 'parameters'
};
}
};
```
## 🎯 Supported Contexts
| Context | Description | Example Keys |
|---------|-------------|--------------|
| `'top-level'` | Root OpenAPI document | `openapi`, `info`, `paths`, `components` |
| `'info'` | API information | `title`, `version`, `description` |
| `'operation'` | Path operations | `summary`, `parameters`, `responses` |
| `'parameter'` | Request parameters | `name`, `in`, `schema` |
| `'schema'` | Data schemas | `type`, `properties`, `required` |
| `'response'` | API responses | `description`, `content`, `headers` |
| `'securityScheme'` | Security schemes | `type`, `name`, `in` |
| `'server'` | Server information | `url`, `description`, `variables` |
| `'tag'` | API tags | `name`, `description`, `externalDocs` |
| `'externalDocs'` | External documentation | `description`, `url` |
| `'webhook'` | Webhook operations | `summary`, `parameters`, `responses` |
| `'definitions'` | Swagger 2.0 definitions | `type`, `properties`, `required` |
| `'securityDefinitions'` | Swagger 2.0 security | `type`, `name`, `in` |
## 🎛️ Positioning Helpers
### `before(key)`
Position before a specific key:
```typescript
before('info') // Before 'info' key
before('paths') // Before 'paths' key
before('type') // Before 'type' key
```
### `after(key)`
Position after a specific key:
```typescript
after('info') // After 'info' key
after('paths') // After 'paths' key
after('type') // After 'type' key
```
## 📚 TypeScript Support
### Hover Documentation
When you hover over context names in your IDE, you'll see:
- **`'top-level'`**: Shows all top-level OpenAPI keys in order
- **`'operation'`**: Shows all operation keys in order
- **`'schema'`**: Shows all schema keys in order
- And so on...
### Type Safety
```typescript
// Full type safety with IntelliSense
const extensions = {
'top-level': (before, after) => {
return {
'x-company-key': before('info'), // ✅ Type safe
'x-company-value': after('paths'), // ✅ Type safe
};
}
};
```
## 🧪 Testing
All tests pass:
```bash
# Run vendor tests
bun test test/vendor.test.ts
# Run all tests
bun test
```
## 🎉 Benefits
- **Automatic Discovery** - No configuration needed
- **Unlimited Vendors** - Add as many as you want
- **Zero Complexity** - Just create a TS file
- **Type Safe** - Full TypeScript support with IntelliSense
- **Flexible** - Support for all OpenAPI contexts
- **Maintainable** - Simple function-based approach
- **Extensible** - Easy to add new vendors
## 🚀 Getting Started
1. **Create a new TypeScript file** in `extensions/vendor/`
2. **Export an `extensions` object** with function-based definitions
3. **Use `before` and `after` helpers** for positioning
4. **That's it!** The system handles the rest
## 🎯 Result
The vendor extension system is now **ultra-simple** and **unlimited**:
- **Automatic Discovery** - No configuration needed
- **Unlimited Vendors** - Add as many as you want
- **Zero Complexity** - Just create a TS file
- **Type Safe** - Full TypeScript support
- **Flexible** - Support for all OpenAPI contexts
- **Maintainable** - Simple function-based approach
Vendors can now add their extensions with just a few lines of code and zero complexity! 🚀

View File

@@ -0,0 +1,180 @@
# Function-Based Vendor Extension System
The Prettier OpenAPI Plugin now includes an extremely easy-to-use vendor extension system with function-based positioning that makes it incredibly simple for vendors to add their extensions.
## ✅ What Was Accomplished
### 🎯 Function-Based Extensions
- **Function-based approach** - Each context is a function that receives `before` and `after` helpers
- **Automatic positioning** - No need to know exact positions, just use `before(key)` and `after(key)`
- **Type safety** - Full TypeScript support with IntelliSense
- **Extremely easy** - Just create a TS file and start adding extensions
### 🏗️ Simple API
- **`before(key)`** - Position before a specific key
- **`after(key)`** - Position after a specific key
- **`defineVendorExtensions(config)`** - Type-safe configuration helper
### 🎛️ Smart Positioning Benefits
- **No hardcoded positions** - Vendors don't need to know exact ordering
- **Automatic updates** - Changes to key arrays automatically update positioning
- **Intuitive API** - `before('info')` and `after('paths')` are self-explanatory
- **Type safety** - Full TypeScript support with IntelliSense
## 📁 Final Structure
```
vendor/
├── index.ts # Main vendor system with function-based approach
├── speakeasy.ts # Speakeasy extensions (function-based)
├── example-usage.ts # Example vendor (function-based)
└── README.md # Comprehensive documentation
src/
├── keys.ts # Centralized key arrays
└── index.ts # Main plugin (imports from keys.ts)
```
## 🚀 Usage Examples
### Simple Vendor Extension
```typescript
// vendor/mycompany.ts
import { defineVendorExtensions } from './index';
export const extensions = {
'top-level': (before, after) => {
return {
'x-mycompany-api-key': before('info'), // Before 'info'
'x-mycompany-version': after('paths'), // After 'paths'
};
},
'operation': (before, after) => {
return {
'x-mycompany-rate-limit': after('parameters'), // After 'parameters'
'x-mycompany-auth': before('responses'), // Before 'responses'
};
}
};
export const config = defineVendorExtensions({ extensions });
```
### Speakeasy Vendor (Function-Based)
```typescript
// vendor/speakeasy.ts
export const extensions = {
'top-level': (before, after) => {
return {
'x-speakeasy-sdk': before('info'), // Before 'info'
'x-speakeasy-auth': after('paths'), // After 'paths'
};
},
'operation': (before, after) => {
return {
'x-speakeasy-retries': after('parameters'), // After 'parameters'
'x-speakeasy-timeout': before('responses'), // Before 'responses'
'x-speakeasy-cache': after('servers'), // After 'servers'
};
}
};
```
## 🎯 Supported Contexts
| Context | Description | Example Keys |
|---------|-------------|--------------|
| `'top-level'` | Root OpenAPI document | `openapi`, `info`, `paths`, `components` |
| `'info'` | API information | `title`, `version`, `description` |
| `'operation'` | Path operations | `summary`, `parameters`, `responses` |
| `'parameter'` | Request parameters | `name`, `in`, `schema` |
| `'schema'` | Data schemas | `type`, `properties`, `required` |
| `'response'` | API responses | `description`, `content`, `headers` |
| `'securityScheme'` | Security schemes | `type`, `name`, `in` |
| `'server'` | Server information | `url`, `description`, `variables` |
| `'tag'` | API tags | `name`, `description`, `externalDocs` |
| `'externalDocs'` | External documentation | `description`, `url` |
| `'webhook'` | Webhook operations | `summary`, `parameters`, `responses` |
| `'definitions'` | Swagger 2.0 definitions | `type`, `properties`, `required` |
| `'securityDefinitions'` | Swagger 2.0 security | `type`, `name`, `in` |
## 🎛️ Positioning Helpers
### `before(key)`
Position before a specific key:
```typescript
before('info') // Before 'info' key
before('paths') // Before 'paths' key
before('type') // Before 'type' key
```
### `after(key)`
Position after a specific key:
```typescript
after('info') // After 'info' key
after('paths') // After 'paths' key
after('type') // After 'type' key
```
## 📚 TypeScript Support
### Hover Documentation
When you hover over context names in your IDE, you'll see:
- **`'top-level'`**: Shows all top-level OpenAPI keys in order
- **`'operation'`**: Shows all operation keys in order
- **`'schema'`**: Shows all schema keys in order
- And so on...
### Type Safety
```typescript
// Full type safety with IntelliSense
const extensions = {
'top-level': (before, after) => {
return {
'x-company-key': before('info'), // ✅ Type safe
'x-company-value': after('paths'), // ✅ Type safe
};
}
};
```
## 🧪 Testing
All tests pass:
```bash
# Run vendor tests
bun test test/vendor.test.ts
# Run all tests
bun test
```
## 🎉 Benefits
- **Extremely Easy** - Just create a TS file with functions
- **No Hardcoded Positions** - Use `before()` and `after()` helpers
- **Automatic Updates** - Changes to key arrays automatically update positioning
- **Type Safe** - Full TypeScript support with IntelliSense
- **Flexible** - Support for all OpenAPI contexts
- **Maintainable** - Simple function-based approach
- **Intuitive** - `before('info')` and `after('paths')` are self-explanatory
## 🚀 Getting Started
1. Create a new TypeScript file in `vendor/`
2. Import `defineVendorExtensions` from `./index`
3. Define your extensions using the function-based approach
4. Export your configuration
5. That's it! The plugin will automatically load your extensions
## 🎯 Result
The vendor extension system is now **extremely easy to use**:
- **Function-based approach** - Each context is a function with `before`/`after` helpers
- **No hardcoded positions** - Just use `before('info')` and `after('paths')`
- **Type safety** - Full TypeScript support with IntelliSense
- **Automatic positioning** - Changes to key arrays automatically update positioning
- **Simple API** - Just create a TS file and start adding extensions
Vendors can now add their extensions with just a few lines of code! 🚀

216
VENDOR_HELPER_FUNCTIONS.md Normal file
View File

@@ -0,0 +1,216 @@
# Vendor Helper Functions & Types
The Prettier OpenAPI Plugin now includes powerful helper functions and TypeScript types for the vendor extension system, similar to Vite's `defineConfig` approach.
## 🎯 New Features
### 1. `defineVendorConfig(config)` Helper Function
Similar to Vite's `defineConfig`, provides type-safe configuration with IntelliSense.
```typescript
import { defineVendorConfig } from './index';
export const config = defineVendorConfig({
info: {
name: 'mycompany',
version: '1.0.0',
description: 'MyCompany API extensions',
website: 'https://mycompany.com'
},
extensions: {
'top-level': {
'x-mycompany-config': 1,
},
'operation': {
'x-mycompany-rate-limit': 5,
}
}
});
```
### 2. `getContextKeys(context)` Function
Returns available keys for a context with hover documentation.
```typescript
import { getContextKeys } from './index';
const topLevelKeys = getContextKeys('top-level');
// Returns: ['swagger', 'openapi', 'info', 'servers', 'paths', ...]
const operationKeys = getContextKeys('operation');
// Returns: ['tags', 'summary', 'description', 'operationId', 'parameters', ...]
```
### 3. `getKeyPosition(context, key)` Function
Returns the position of a key in the standard ordering.
```typescript
import { getKeyPosition } from './index';
const openapiPosition = getKeyPosition('top-level', 'openapi');
// Returns: 1 (position in TOP_LEVEL_KEYS array)
const tagsPosition = getKeyPosition('operation', 'tags');
// Returns: 0 (position in OPERATION_KEYS array)
```
## 🧠 Smart Positioning
Use helper functions to position extensions relative to standard keys:
```typescript
const smartExtensions = {
'top-level': {
'x-mycompany-before-info': getKeyPosition('top-level', 'info') - 1, // Before 'info'
'x-mycompany-after-paths': getKeyPosition('top-level', 'paths') + 1, // After 'paths'
},
'operation': {
'x-mycompany-before-parameters': getKeyPosition('operation', 'parameters') - 1, // Before 'parameters'
'x-mycompany-after-responses': getKeyPosition('operation', 'responses') + 1, // After 'responses'
}
};
```
## 📚 TypeScript IntelliSense
### Hover Documentation
When you hover over context names in your IDE, you'll see:
- **`'top-level'`**: Shows all top-level OpenAPI keys in order
- **`'operation'`**: Shows all operation keys in order
- **`'schema'`**: Shows all schema keys in order
- **`'parameter'`**: Shows all parameter keys in order
- **`'response'`**: Shows all response keys in order
- **`'securityScheme'`**: Shows all security scheme keys in order
- **`'server'`**: Shows all server keys in order
- **`'tag'`**: Shows all tag keys in order
- **`'externalDocs'`**: Shows all external docs keys in order
- **`'webhook'`**: Shows all webhook keys in order
- **`'definitions'`**: Shows all definition keys in order
- **`'securityDefinitions'`**: Shows all security definition keys in order
### Auto-Updating Documentation
The hover documentation automatically stays up-to-date as the key arrays change in the main plugin code!
## 🔧 Type Definitions
### Exported Types
```typescript
export interface VendorInfo {
name: string;
version: string;
description?: string;
website?: string;
}
export interface VendorConfig {
info: VendorInfo;
extensions: {
'top-level'?: VendorContextExtensions;
'info'?: VendorContextExtensions;
'operation'?: VendorContextExtensions;
'parameter'?: VendorContextExtensions;
'schema'?: VendorContextExtensions;
'response'?: VendorContextExtensions;
'securityScheme'?: VendorContextExtensions;
'server'?: VendorContextExtensions;
'tag'?: VendorContextExtensions;
'externalDocs'?: VendorContextExtensions;
'webhook'?: VendorContextExtensions;
'definitions'?: VendorContextExtensions;
'securityDefinitions'?: VendorContextExtensions;
};
}
```
### Context-Specific Key Types
```typescript
export type TopLevelKeys = typeof TOP_LEVEL_KEYS[number];
export type InfoKeys = typeof INFO_KEYS[number];
export type OperationKeys = typeof OPERATION_KEYS[number];
export type ParameterKeys = typeof PARAMETER_KEYS[number];
export type SchemaKeys = typeof SCHEMA_KEYS[number];
// ... and more
```
## 🚀 Example Usage
### Basic Vendor File
```typescript
// vendor/mycompany.ts
import { defineVendorConfig, getContextKeys, getKeyPosition } from './index';
export const info = {
name: 'mycompany',
version: '1.0.0',
description: 'MyCompany API extensions',
website: 'https://mycompany.com'
};
export const extensions = {
'top-level': {
'x-mycompany-config': 1,
'x-mycompany-auth': 2,
},
'operation': {
'x-mycompany-rate-limit': 5,
'x-mycompany-timeout': 6,
}
};
// Use defineVendorConfig for better type checking
export const config = defineVendorConfig({
info,
extensions
});
```
### Smart Positioning Example
```typescript
// vendor/smart-vendor.ts
import { defineVendorConfig, getKeyPosition } from './index';
export const smartExtensions = {
'top-level': {
'x-smart-before-info': getKeyPosition('top-level', 'info') - 1,
'x-smart-after-paths': getKeyPosition('top-level', 'paths') + 1,
},
'operation': {
'x-smart-before-parameters': getKeyPosition('operation', 'parameters') - 1,
'x-smart-after-responses': getKeyPosition('operation', 'responses') + 1,
}
};
export const config = defineVendorConfig({
info: {
name: 'smart-vendor',
version: '1.0.0',
description: 'Smart vendor with relative positioning',
website: 'https://smart-vendor.com'
},
extensions: smartExtensions
});
```
## ✅ Benefits
- **Type Safety**: Full TypeScript support with IntelliSense
- **Auto-Updating**: Documentation stays in sync with key arrays
- **Smart Positioning**: Position extensions relative to standard keys
- **Easy to Use**: Simple helper functions like Vite's `defineConfig`
- **Better DX**: Hover documentation shows available keys and their order
- **Maintainable**: Changes to key arrays automatically update types
## 🧪 Testing
All helper functions are fully tested:
```bash
# Run vendor tests
bun test test/vendor.test.ts
# Run all tests
bun test
```
The vendor extension system now provides a developer experience similar to modern build tools like Vite, with powerful TypeScript support and IntelliSense! 🎉

View File

@@ -0,0 +1,164 @@
# Enhanced Vendor Extension System with Helper Functions
The Prettier OpenAPI Plugin now includes a comprehensive vendor extension system with both calculated positioning and helper functions for maximum flexibility.
## ✅ What Was Accomplished
### 🎯 Helper Functions Implementation
- **`before(context, key)`** - Position before a specific key
- **`after(context, key)`** - Position after a specific key
- **`getKeyPosition(context, key)`** - Get exact position of a key
- **`getContextKeys(context)`** - Get all keys for a context
### 🏗️ Two Positioning Approaches
- **Approach 1**: Calculated positions (recommended for most vendors)
- **Approach 2**: Helper functions (for dynamic positioning)
### 🎛️ Smart Positioning Benefits
- **No hardcoded positions** - Vendors don't need to know exact ordering
- **Automatic updates** - Changes to key arrays automatically update positioning
- **Type safety** - Full TypeScript support with IntelliSense
- **Flexibility** - Choose the approach that works best for your use case
## 📁 Current Structure
```
vendor/
├── index.ts # Main vendor system with helper functions
├── speakeasy.ts # Speakeasy extensions (calculated positions)
├── example-usage.ts # Example vendor (calculated positions)
├── helper-functions-example.ts # Example using helper functions
└── README.md # Documentation
src/
├── keys.ts # Centralized key arrays
└── index.ts # Main plugin (imports from keys.ts)
```
## 🚀 Usage Examples
### Approach 1: Calculated Positions (Recommended)
```typescript
// vendor/speakeasy.ts
export const extensions = {
'top-level': {
'x-speakeasy-sdk': 1, // Before 'info' (position 2)
'x-speakeasy-auth': 11, // After 'paths' (position 10)
},
'operation': {
'x-speakeasy-retries': 6, // After 'parameters' (position 5)
'x-speakeasy-timeout': 8, // Before 'responses' (position 9)
'x-speakeasy-cache': 12, // After 'servers' (position 11)
}
};
```
### Approach 2: Helper Functions (Advanced)
```typescript
// vendor/helper-functions-example.ts
import { defineVendorConfig, before, after } from './index';
export const smartExtensions = {
'top-level': {
'x-example-before-info': before('top-level', 'info'), // Before 'info'
'x-example-after-paths': after('top-level', 'paths'), // After 'paths'
},
'operation': {
'x-example-before-parameters': before('operation', 'parameters'), // Before 'parameters'
'x-example-after-responses': after('operation', 'responses'), // After 'responses'
},
'schema': {
'x-example-validation': after('schema', 'type'), // After 'type'
'x-example-example': after('schema', 'example'), // After 'example'
}
};
```
## 🎛️ Helper Functions API
### `before(context, key)`
Returns the position before the specified key.
```typescript
before('top-level', 'info') // Returns 1 (before 'info' at position 2)
before('operation', 'parameters') // Returns 4 (before 'parameters' at position 5)
```
### `after(context, key)`
Returns the position after the specified key.
```typescript
after('top-level', 'paths') // Returns 11 (after 'paths' at position 10)
after('operation', 'responses') // Returns 10 (after 'responses' at position 9)
```
### `getKeyPosition(context, key)`
Returns the exact position of a key in the standard ordering.
```typescript
getKeyPosition('top-level', 'info') // Returns 2
getKeyPosition('operation', 'parameters') // Returns 5
```
### `getContextKeys(context)`
Returns all keys for a context with hover documentation.
```typescript
getContextKeys('top-level') // Returns ['swagger', 'openapi', 'info', ...]
getContextKeys('operation') // Returns ['tags', 'summary', 'description', ...]
```
## 🎯 Benefits of Each Approach
### Calculated Positions (Approach 1)
-**Simple** - Just numbers with comments
-**Fast** - No function calls at runtime
-**Clear** - Easy to see exact positioning
-**Stable** - No dependency on helper functions
### Helper Functions (Approach 2)
-**Dynamic** - Automatically adapts to key changes
-**Intuitive** - `before('info')` and `after('paths')` are clear
-**Maintainable** - No need to update positions manually
-**Type Safe** - Full TypeScript support
## 📚 TypeScript Support
### Hover Documentation
When you hover over context names in your IDE, you'll see:
- **`'top-level'`**: Shows all top-level OpenAPI keys in order
- **`'operation'`**: Shows all operation keys in order
- **`'schema'`**: Shows all schema keys in order
- **`'parameter'`**: Shows all parameter keys in order
- And so on...
### Type Safety
```typescript
// Full type safety with IntelliSense
const position = before('top-level', 'info'); // Type: number
const keys = getContextKeys('operation'); // Type: readonly string[]
```
## 🧪 Testing
All tests pass:
```bash
# Run vendor tests
bun test test/vendor.test.ts
# Run all tests
bun test
```
## 🎉 Result
The vendor extension system now provides:
- **Two positioning approaches** - Choose what works best for your use case
- **Helper functions** - `before()`, `after()`, `getKeyPosition()`, `getContextKeys()`
- **Type safety** - Full TypeScript support with IntelliSense
- **Smart positioning** - Position extensions relative to standard keys
- **Flexibility** - Calculated positions or dynamic helper functions
- **Maintainability** - Changes to key arrays automatically update positioning
Vendors can now choose between simple calculated positions or dynamic helper functions for maximum flexibility! 🚀

183
VENDOR_SIMPLIFIED_FINAL.md Normal file
View File

@@ -0,0 +1,183 @@
# Simplified Vendor Extension System
The Prettier OpenAPI Plugin now includes an extremely simple vendor extension system that makes it incredibly easy for vendors to add their extensions.
## ✅ What Was Accomplished
### 🎯 Simplified System
- **Removed vendor info** - No more metadata, just extensions
- **Function-based approach** - Each context is a function with `before`/`after` helpers
- **Minimal API** - Just `defineVendorExtensions({ extensions })`
- **Extremely easy** - Just create a TS file and start adding extensions
### 🏗️ Ultra-Simple API
- **`before(key)`** - Position before a specific key
- **`after(key)`** - Position after a specific key
- **`defineVendorExtensions({ extensions })`** - Type-safe configuration helper
### 🎛️ Zero Complexity
- **No vendor info** - No metadata to manage
- **No version tracking** - No version numbers to maintain
- **No descriptions** - No documentation to write
- **Just extensions** - Focus on what matters
## 📁 Final Structure
```
vendor/
├── index.ts # Main vendor system (simplified)
├── speakeasy.ts # Speakeasy extensions (function-based)
├── example-usage.ts # Example vendor (function-based)
└── README.md # Documentation
src/
├── keys.ts # Centralized key arrays
└── index.ts # Main plugin (imports from keys.ts)
```
## 🚀 Usage Examples
### Ultra-Simple Vendor Extension
```typescript
// vendor/mycompany.ts
import { defineVendorExtensions } from './index';
export const extensions = {
'top-level': (before, after) => {
return {
'x-mycompany-api-key': before('info'), // Before 'info'
'x-mycompany-version': after('paths'), // After 'paths'
};
},
'operation': (before, after) => {
return {
'x-mycompany-rate-limit': after('parameters'), // After 'parameters'
'x-mycompany-auth': before('responses'), // Before 'responses'
};
}
};
export const config = defineVendorExtensions({ extensions });
```
### Speakeasy Vendor (Simplified)
```typescript
// vendor/speakeasy.ts
export const extensions = {
'top-level': (before, after) => {
return {
'x-speakeasy-sdk': before('info'), // Before 'info'
'x-speakeasy-auth': after('paths'), // After 'paths'
};
},
'operation': (before, after) => {
return {
'x-speakeasy-retries': after('parameters'), // After 'parameters'
'x-speakeasy-timeout': before('responses'), // Before 'responses'
'x-speakeasy-cache': after('servers'), // After 'servers'
};
}
};
export const config = defineVendorExtensions({ extensions });
```
## 🎯 Supported Contexts
| Context | Description | Example Keys |
|---------|-------------|--------------|
| `'top-level'` | Root OpenAPI document | `openapi`, `info`, `paths`, `components` |
| `'info'` | API information | `title`, `version`, `description` |
| `'operation'` | Path operations | `summary`, `parameters`, `responses` |
| `'parameter'` | Request parameters | `name`, `in`, `schema` |
| `'schema'` | Data schemas | `type`, `properties`, `required` |
| `'response'` | API responses | `description`, `content`, `headers` |
| `'securityScheme'` | Security schemes | `type`, `name`, `in` |
| `'server'` | Server information | `url`, `description`, `variables` |
| `'tag'` | API tags | `name`, `description`, `externalDocs` |
| `'externalDocs'` | External documentation | `description`, `url` |
| `'webhook'` | Webhook operations | `summary`, `parameters`, `responses` |
| `'definitions'` | Swagger 2.0 definitions | `type`, `properties`, `required` |
| `'securityDefinitions'` | Swagger 2.0 security | `type`, `name`, `in` |
## 🎛️ Positioning Helpers
### `before(key)`
Position before a specific key:
```typescript
before('info') // Before 'info' key
before('paths') // Before 'paths' key
before('type') // Before 'type' key
```
### `after(key)`
Position after a specific key:
```typescript
after('info') // After 'info' key
after('paths') // After 'paths' key
after('type') // After 'type' key
```
## 📚 TypeScript Support
### Hover Documentation
When you hover over context names in your IDE, you'll see:
- **`'top-level'`**: Shows all top-level OpenAPI keys in order
- **`'operation'`**: Shows all operation keys in order
- **`'schema'`**: Shows all schema keys in order
- And so on...
### Type Safety
```typescript
// Full type safety with IntelliSense
const extensions = {
'top-level': (before, after) => {
return {
'x-company-key': before('info'), // ✅ Type safe
'x-company-value': after('paths'), // ✅ Type safe
};
}
};
```
## 🧪 Testing
All tests pass:
```bash
# Run vendor tests
bun test test/vendor.test.ts
# Run all tests
bun test
```
## 🎉 Benefits
- **Ultra-Simple** - Just create a TS file with functions
- **No Metadata** - No vendor info to manage
- **No Versioning** - No version numbers to track
- **No Descriptions** - No documentation to write
- **Just Extensions** - Focus on what matters
- **Type Safe** - Full TypeScript support with IntelliSense
- **Flexible** - Support for all OpenAPI contexts
- **Maintainable** - Simple function-based approach
## 🚀 Getting Started
1. Create a new TypeScript file in `vendor/`
2. Import `defineVendorExtensions` from `./index`
3. Define your extensions using the function-based approach
4. Export your configuration
5. That's it! The plugin will automatically load your extensions
## 🎯 Result
The vendor extension system is now **ultra-simple**:
- **No vendor info** - No metadata to manage
- **Function-based approach** - Each context is a function with `before`/`after` helpers
- **Minimal API** - Just `defineVendorExtensions({ extensions })`
- **Zero complexity** - No versioning, no descriptions, no metadata
- **Just extensions** - Focus on what matters
Vendors can now add their extensions with just a few lines of code and zero complexity! 🚀

222
VENDOR_SYSTEM.md Normal file
View File

@@ -0,0 +1,222 @@
# Vendor Extension System
The Prettier OpenAPI Plugin now includes a comprehensive vendor extension system that allows third-party vendors to contribute custom extensions with structured positioning.
## 🎯 Overview
The vendor system enables companies like Speakeasy, Redoc, Postman, and others to contribute their custom OpenAPI extensions to the plugin, ensuring proper key ordering and positioning in formatted OpenAPI documents.
## 🏗️ Architecture
### Core Components
1. **Vendor Loader** (`vendor/loader.ts`): Loads and merges vendor extensions
2. **Type Definitions** (`vendor/types.ts`): TypeScript interfaces for vendor system
3. **CLI Tool** (`vendor/cli.ts`): Management tool for vendors
4. **Manifest System**: JSON-based vendor configuration
### Directory Structure
```
vendor/
├── types.ts # Type definitions
├── loader.ts # Extension loader
├── cli.ts # CLI management tool
├── README.md # Documentation
├── examples/ # Usage examples
├── speakeasy/ # Speakeasy extensions
│ └── manifest.json
├── redoc/ # Redoc extensions
│ └── manifest.json
└── [vendor-name]/ # Other vendors
└── manifest.json
```
## 🚀 Features
### ✅ Vendor Management
- **Automatic Loading**: Extensions loaded at plugin startup
- **Version Compatibility**: Plugin version checking
- **Error Handling**: Graceful fallback to base configuration
- **CLI Tools**: Easy vendor management
### ✅ Extension Positioning
- **Structured Positioning**: Numeric position system
- **Context-Aware**: Extensions apply to specific OpenAPI contexts
- **Global Extensions**: Extensions that apply to all contexts
- **Priority System**: Vendor extensions override base configuration
### ✅ Developer Experience
- **Type Safety**: Full TypeScript support
- **Validation**: Manifest validation tools
- **Documentation**: Comprehensive examples and docs
- **Debugging**: Detailed logging and error messages
## 📋 Supported Contexts
| Context | Description | Example Extensions |
|---------|------------|----------------------|
| `top-level` | Root OpenAPI document | `x-speakeasy-sdk`, `x-redoc-version` |
| `info` | API information | `x-speakeasy-info`, `x-redoc-info` |
| `operation` | HTTP operations | `x-speakeasy-retries`, `x-redoc-group` |
| `schema` | Schema objects | `x-speakeasy-validation`, `x-redoc-display` |
| `parameter` | Parameters | `x-speakeasy-param` |
| `response` | Responses | `x-speakeasy-response` |
| `securityScheme` | Security schemes | `x-speakeasy-auth` |
| `server` | Servers | `x-speakeasy-server` |
| `tag` | Tags | `x-speakeasy-tag` |
| `externalDocs` | External docs | `x-speakeasy-docs` |
| `webhook` | Webhooks (OpenAPI 3.1+) | `x-speakeasy-webhook` |
| `definitions` | Swagger 2.0 definitions | `x-speakeasy-definition` |
| `securityDefinitions` | Swagger 2.0 security | `x-speakeasy-security` |
## 🛠️ Usage
### For Vendors
1. **Create Vendor Directory**:
```bash
mkdir vendor/mycompany
```
2. **Create Manifest**:
```json
{
"pluginVersion": "1.0.0",
"vendors": [
{
"name": "mycompany",
"version": "1.0.0",
"description": "MyCompany API extensions",
"contexts": [
{
"context": "top-level",
"extensions": [
{
"key": "x-mycompany-config",
"position": 1,
"description": "MyCompany configuration"
}
]
}
]
}
]
}
```
3. **Test Integration**:
```bash
bun vendor/cli.ts validate
bun vendor/cli.ts info mycompany
```
### For Users
1. **Install Plugin**: The vendor system works automatically
2. **Add Vendors**: Place vendor directories in `vendor/` folder
3. **Format Documents**: Extensions are automatically positioned
4. **Manage Vendors**: Use CLI tools for management
## 🎛️ CLI Commands
```bash
# List all vendors
bun vendor/cli.ts list
# Get vendor information
bun vendor/cli.ts info speakeasy
# Validate vendor manifests
bun vendor/cli.ts validate
# Create new vendor template
bun vendor/cli.ts create mycompany
```
## 📊 Position System
Extensions are positioned using a numeric system:
- **0**: First position (before standard keys)
- **1-10**: Early positions (after key fields)
- **11-20**: Middle positions
- **21+**: Later positions (before unknown keys)
## 🔧 Configuration
### Base Configuration
The plugin includes base custom extensions that can be overridden by vendors:
```typescript
const BASE_CUSTOM_TOP_LEVEL_EXTENSIONS = {
// Base extensions here
};
```
### Vendor Override
Vendors can override base extensions by specifying the same keys with different positions.
### Global Extensions
Vendors can define global extensions that apply to all contexts:
```json
{
"globalExtensions": [
{
"key": "x-mycompany-version",
"position": 0,
"description": "MyCompany version info"
}
]
}
```
## 🧪 Testing
The vendor system includes comprehensive tests:
```bash
# Run vendor tests
bun test test/vendor.test.ts
# Run all tests
bun test
```
## 📚 Examples
### Speakeasy Extensions
- **SDK Configuration**: `x-speakeasy-sdk`
- **Retry Logic**: `x-speakeasy-retries`
- **Validation Rules**: `x-speakeasy-validation`
### Redoc Extensions
- **Theme Configuration**: `x-redoc-theme`
- **Code Samples**: `x-redoc-code-samples`
- **Display Options**: `x-redoc-display`
## 🚨 Error Handling
The system includes robust error handling:
- **Missing Manifests**: Graceful fallback to base configuration
- **Invalid JSON**: Clear error messages
- **Version Conflicts**: Compatibility warnings
- **Missing Dependencies**: Automatic fallback
## 🔮 Future Enhancements
- **Dynamic Loading**: Hot-reload vendor extensions
- **Extension Validation**: Schema validation for extensions
- **Vendor Marketplace**: Centralized vendor registry
- **Performance Optimization**: Lazy loading of extensions
## 📖 Documentation
- **Vendor README**: `vendor/README.md`
- **Type Definitions**: `vendor/types.ts`
- **Examples**: `vendor/examples/`
- **CLI Help**: `bun vendor/cli.ts`
This vendor system makes the Prettier OpenAPI Plugin truly extensible, allowing the ecosystem to contribute and maintain their own custom extensions while ensuring consistent formatting across all OpenAPI documents.

127
VENDOR_SYSTEM_FINAL.md Normal file
View File

@@ -0,0 +1,127 @@
# Final Vendor Extension System
The Prettier OpenAPI Plugin now includes a simplified vendor extension system with smart positioning and TypeScript support.
## ✅ What Was Accomplished
### 🗑️ Removed Complexity
- **Removed Postman and Redoc vendors** - Simplified to just Speakeasy
- **Removed CLI and manifest system** - No complex management tools
- **Removed circular dependencies** - Clean architecture
### 🎯 Smart Positioning System
- **Calculated positions** based on standard key ordering
- **No helper functions** - Simple number-based positioning
- **Clear documentation** - Comments show relative positioning
### 🏗️ Clean Architecture
- **Separated key arrays** into `src/keys.ts` to avoid circular dependencies
- **Simple vendor files** - Just TypeScript files with exports
- **Type-safe configuration** with `defineVendorConfig`
## 📁 Current Structure
```
vendor/
├── index.ts # Main vendor system
├── speakeasy.ts # Speakeasy extensions (smart positioning)
├── example-usage.ts # Example vendor (smart positioning)
└── README.md # Documentation
src/
├── keys.ts # Centralized key arrays
└── index.ts # Main plugin (imports from keys.ts)
```
## 🚀 Smart Positioning Example
### Speakeasy Vendor (Smart Positioning)
```typescript
// vendor/speakeasy.ts
export const extensions = {
'top-level': {
'x-speakeasy-sdk': 1, // Before 'info' (position 2)
'x-speakeasy-auth': 11, // After 'paths' (position 10)
},
'operation': {
'x-speakeasy-retries': 6, // After 'parameters' (position 5)
'x-speakeasy-timeout': 8, // Before 'responses' (position 9)
'x-speakeasy-cache': 12, // After 'servers' (position 11)
},
'schema': {
'x-speakeasy-validation': 1, // After 'type' (position 0)
'x-speakeasy-example': 6, // After 'example' (position 5)
}
};
```
### Example Vendor (Smart Positioning)
```typescript
// vendor/example-usage.ts
export const smartExtensions = {
'top-level': {
'x-example-before-info': 1, // Before 'info' (position 2)
'x-example-after-paths': 11, // After 'paths' (position 10)
},
'operation': {
'x-example-before-parameters': 4, // Before 'parameters' (position 5)
'x-example-after-responses': 10, // After 'responses' (position 9)
},
'schema': {
'x-example-validation': 1, // After 'type' (position 0)
'x-example-example': 6, // After 'example' (position 5)
}
};
```
## 🎛️ Position Reference
- **0**: First position (before standard keys)
- **1-10**: Early positions (after key fields)
- **11-20**: Middle positions
- **21+**: Later positions (before unknown keys)
## 📚 TypeScript Support
### `defineVendorConfig(config)`
Similar to Vite's `defineConfig`, provides type-safe configuration with IntelliSense.
### Hover Documentation
When you hover over context names in your IDE, you'll see:
- **`'top-level'`**: Shows all top-level OpenAPI keys in order
- **`'operation'`**: Shows all operation keys in order
- **`'schema'`**: Shows all schema keys in order
- **`'parameter'`**: Shows all parameter keys in order
- And so on...
## ✅ Benefits
- **Simple**: Just TypeScript files with calculated positions
- **Type Safe**: Full TypeScript support with IntelliSense
- **Smart Positioning**: Position extensions relative to standard keys
- **No Dependencies**: No circular dependencies or complex imports
- **Easy to Use**: Simple number-based positioning
- **Maintainable**: Changes to key arrays automatically update types
- **Clean Architecture**: Separated concerns with `src/keys.ts`
## 🧪 Testing
All tests pass:
```bash
# Run vendor tests
bun test test/vendor.test.ts
# Run all tests
bun test
```
## 🎉 Result
The vendor extension system is now:
- **Simplified** - No CLI, no manifests, just TS files
- **Smart** - Calculated positioning based on standard key ordering
- **Type Safe** - Full TypeScript support with IntelliSense
- **Clean** - No circular dependencies, separated concerns
- **Easy to Use** - Simple number-based positioning
Vendors can now contribute their extensions with just a TypeScript file using smart positioning! 🚀

155
VENDOR_SYSTEM_SIMPLE.md Normal file
View File

@@ -0,0 +1,155 @@
# Simple Vendor Extension System
The Prettier OpenAPI Plugin now includes a simple vendor extension system that allows third-party vendors to contribute custom extensions by adding TypeScript files.
## 🎯 Overview
Vendors can easily contribute their custom OpenAPI extensions by creating a simple TypeScript file in the `vendor/` folder. No CLI, no manifests, no complexity - just TypeScript files!
## 🏗️ How It Works
### For Vendors
1. **Create a TypeScript file** in the `vendor/` folder (e.g., `mycompany.ts`)
2. **Export your extensions** using the standard format
3. **Import your file** in `vendor/index.ts`
### Example Vendor File
```typescript
// vendor/mycompany.ts
export const info = {
name: 'mycompany',
version: '1.0.0',
description: 'MyCompany API extensions',
website: 'https://mycompany.com'
};
export const extensions = {
'top-level': {
'x-mycompany-config': 1,
'x-mycompany-auth': 2,
},
'info': {
'x-mycompany-info': 1,
},
'operation': {
'x-mycompany-rate-limit': 5,
'x-mycompany-timeout': 6,
},
'schema': {
'x-mycompany-validation': 1,
}
};
```
### Update vendor/index.ts
```typescript
// Import your vendor
import * as mycompany from './mycompany';
// Add to getVendorExtensions function
if (mycompany.extensions) {
for (const [context, contextExtensions] of Object.entries(mycompany.extensions)) {
if (!extensions[context]) {
extensions[context] = {};
}
Object.assign(extensions[context], contextExtensions);
}
}
```
## 📋 Supported Contexts
| Context | Description | Example Extensions |
|---------|------------|----------------------|
| `top-level` | Root OpenAPI document | `x-mycompany-config` |
| `info` | API information | `x-mycompany-info` |
| `operation` | HTTP operations | `x-mycompany-rate-limit` |
| `schema` | Schema objects | `x-mycompany-validation` |
| `parameter` | Parameters | `x-mycompany-param` |
| `response` | Responses | `x-mycompany-response` |
| `securityScheme` | Security schemes | `x-mycompany-auth` |
| `server` | Servers | `x-mycompany-server` |
| `tag` | Tags | `x-mycompany-tag` |
| `externalDocs` | External docs | `x-mycompany-docs` |
| `webhook` | Webhooks (OpenAPI 3.1+) | `x-mycompany-webhook` |
| `definitions` | Swagger 2.0 definitions | `x-mycompany-definition` |
| `securityDefinitions` | Swagger 2.0 security | `x-mycompany-security` |
## 🎛️ Position System
Extensions are positioned using numbers:
- **0**: First position (before standard keys)
- **1-10**: Early positions (after key fields)
- **11-20**: Middle positions
- **21+**: Later positions (before unknown keys)
## 🚀 Current Vendors
### Speakeasy
- **Website**: https://speakeasyapi.dev
- **Extensions**: SDK configuration, retry logic, validation rules
- **File**: `vendor/speakeasy.ts`
### Redoc
- **Website**: https://redoc.ly
- **Extensions**: Theme configuration, code samples, display options
- **File**: `vendor/redoc.ts`
### Postman
- **Website**: https://postman.com
- **Extensions**: Collection configuration, testing, workspace management
- **File**: `vendor/postman.ts`
## 🧪 Testing
The vendor system includes comprehensive tests:
```bash
# Run vendor tests
bun test test/vendor.test.ts
# Run all tests
bun test
```
## 📁 Directory Structure
```
vendor/
├── index.ts # Main vendor system
├── speakeasy.ts # Speakeasy extensions
├── redoc.ts # Redoc extensions
├── postman.ts # Postman extensions
└── README.md # Documentation
```
## ✅ Benefits
- **Simple**: Just TypeScript files, no CLI or manifests
- **Type Safe**: Full TypeScript support
- **Easy to Add**: Vendors just create a TS file
- **Automatic**: Extensions loaded at plugin startup
- **Flexible**: Vendors control positioning
- **Maintainable**: Easy to update and manage
## 🔧 Integration
The vendor system is automatically integrated with the plugin:
1. **Startup**: Extensions loaded when plugin starts
2. **Merging**: Vendor extensions merged with base configuration
3. **Positioning**: Extensions positioned according to their definitions
4. **Formatting**: OpenAPI documents formatted with proper key ordering
## 📖 Documentation
- **Vendor README**: `vendor/README.md`
- **Type Definitions**: `vendor/index.ts`
- **Examples**: See existing vendor files
This simple vendor system makes the Prettier OpenAPI Plugin truly extensible while keeping the complexity minimal. Vendors can contribute their extensions with just a TypeScript file! 🎉

47
bun.lock Normal file
View File

@@ -0,0 +1,47 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "prettier-plugin-openapi",
"dependencies": {
"@types/js-yaml": "^4.0.0",
"js-yaml": "^4.1.0",
},
"devDependencies": {
"@types/bun": "latest",
"@types/node": "^20.0.0",
"typescript": "^5.0.0",
},
"peerDependencies": {
"prettier": "^3.0.0",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="],
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
"@types/node": ["@types/node@20.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ=="],
"@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"bun-types/@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="],
"bun-types/@types/node/undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
}
}

View File

@@ -0,0 +1,156 @@
# Example OpenAPI file with custom extensions
# This file demonstrates how custom extensions are handled
openapi: 3.0.0
info:
title: API with Custom Extensions
description: This API demonstrates custom extension handling
version: 1.0.0
x-api-id: "api-12345" # Custom extension
x-version-info: "v1.0.0-beta" # Custom extension
contact:
name: API Team
email: api@example.com
x-team-lead: "John Doe" # Custom extension
license:
name: MIT
url: https://opensource.org/licenses/MIT
x-license-version: "3.0" # Custom extension
servers:
- url: https://api.example.com/v1
description: Production server
x-server-region: "us-east-1" # Custom extension
x-load-balancer: "nginx" # Custom extension
variables:
environment:
default: production
x-env-config: "prod-config" # Custom extension
paths:
/users:
get:
tags:
- users
summary: Get all users
description: Retrieve a list of all users
operationId: getUsers
x-rate-limit: 100 # Custom extension
x-custom-auth: "bearer" # Custom extension
parameters:
- name: limit
in: query
description: Maximum number of users to return
required: false
schema:
type: integer
x-validation: "positive" # Custom extension
x-custom-format: "int32" # Custom extension
x-validation: "max:100" # Custom extension
responses:
'200':
description: Successful response
x-response-time: "50ms" # Custom extension
x-cache-info: "ttl:3600" # Custom extension
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
'400':
description: Bad request
x-error-code: "INVALID_REQUEST" # Custom extension
post:
tags:
- users
summary: Create a new user
description: Create a new user in the system
operationId: createUser
x-rate-limit: 50 # Custom extension
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
responses:
'201':
description: User created successfully
x-response-time: "100ms" # Custom extension
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
x-custom-schemas: "enhanced" # Custom extension
schemas:
User:
type: object
x-custom-type: "entity" # Custom extension
required:
- id
- name
- email
properties:
id:
type: integer
format: int64
x-validation-rules: "unique,positive" # Custom extension
name:
type: string
x-validation-rules: "min:1,max:100" # Custom extension
email:
type: string
format: email
x-validation-rules: "email,unique" # Custom extension
x-custom-fields: # Custom extension
type: object
description: Additional custom fields
Error:
type: object
x-custom-type: "error" # Custom extension
required:
- code
- message
properties:
code:
type: string
x-error-category: "system" # Custom extension
message:
type: string
x-error-severity: "high" # Custom extension
responses:
NotFound:
description: Resource not found
x-response-time: "10ms" # Custom extension
x-cache-info: "no-cache" # Custom extension
parameters:
LimitParam:
name: limit
in: query
description: Maximum number of items to return
required: false
schema:
type: integer
x-validation: "positive" # Custom extension
x-validation: "max:1000" # Custom extension
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
x-auth-provider: "custom" # Custom extension
x-token-info: "jwt-v2" # Custom extension
x-api-metadata: "enhanced" # Custom extension
tags:
- name: users
description: User management operations
x-tag-color: "#3498db" # Custom extension
x-tag-priority: "high" # Custom extension
- name: authentication
description: Authentication operations
x-tag-color: "#e74c3c" # Custom extension
x-tag-priority: "critical" # Custom extension
externalDocs:
description: API Documentation
url: https://docs.example.com/api
x-doc-version: "1.0" # Custom extension
x-doc-language: "en" # Custom extension

224
examples/petstore.yaml Normal file
View File

@@ -0,0 +1,224 @@
openapi: 3.0.0
info:
title: Swagger Petstore
description: This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.
version: 1.0.0
termsOfService: http://swagger.io/terms/
contact:
email: apiteam@swagger.io
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
servers:
- url: https://petstore.swagger.io/v2
- url: http://petstore.swagger.io/v2
tags:
- name: pet
description: Everything about your Pets
- name: store
description: Access to Petstore orders
- name: user
description: Operations about user
paths:
/pet:
post:
tags:
- pet
summary: Add a new pet to the store
description: ""
operationId: addPet
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
application/xml:
schema:
$ref: '#/components/schemas/Pet'
responses:
'405':
description: Invalid input
security:
- petstore_auth:
- write:pets
- read:pets
put:
tags:
- pet
summary: Update an existing pet
description: ""
operationId: updatePet
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
application/xml:
schema:
$ref: '#/components/schemas/Pet'
responses:
'400':
description: Invalid ID supplied
'404':
description: Pet not found
'405':
description: Validation exception
security:
- petstore_auth:
- write:pets
- read:pets
/pet/findByStatus:
get:
tags:
- pet
summary: Finds Pets by status
description: Multiple status values can be provided with comma separated strings
operationId: findPetsByStatus
parameters:
- name: status
in: query
description: Status values that need to be considered for filter
required: true
style: form
explode: true
schema:
type: array
items:
type: string
enum:
- available
- pending
- sold
responses:
'200':
description: successful operation
content:
application/xml:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
'400':
description: Invalid status value
security:
- petstore_auth:
- write:pets
- read:pets
components:
schemas:
Pet:
type: object
required:
- name
- photoUrls
properties:
id:
type: integer
format: int64
category:
$ref: '#/components/schemas/Category'
name:
type: string
example: doggie
photoUrls:
type: array
xml:
name: photoUrl
wrapped: true
items:
type: string
tags:
type: array
xml:
name: tag
wrapped: true
items:
$ref: '#/components/schemas/Tag'
status:
type: string
description: pet status in the store
enum:
- available
- pending
- sold
Category:
type: object
properties:
id:
type: integer
format: int64
name:
type: string
Tag:
type: object
properties:
id:
type: integer
format: int64
name:
type: string
Order:
type: object
properties:
id:
type: integer
format: int64
petId:
type: integer
format: int64
quantity:
type: integer
format: int32
shipDate:
type: string
format: date-time
status:
type: string
description: Order Status
enum:
- placed
- approved
- delivered
complete:
type: boolean
default: false
User:
type: object
properties:
id:
type: integer
format: int64
username:
type: string
firstName:
type: string
lastName:
type: string
email:
type: string
password:
type: string
phone:
type: string
userStatus:
type: integer
description: User Status
format: int32
securitySchemes:
petstore_auth:
type: oauth2
flows:
implicit:
authorizationUrl: http://petstore.swagger.io/api/oauth/dialog
scopes:
write:pets: modify pets in your account
read:pets: read your pets
api_key:
type: apiKey
name: api_key
in: header

170
examples/usage.md Normal file
View File

@@ -0,0 +1,170 @@
# Usage Examples
## Basic Usage
### Format a single file
```bash
npx prettier --write examples/petstore.yaml
```
### Format all OpenAPI files
```bash
npx prettier --write "**/*.{openapi.json,openapi.yaml,swagger.json,swagger.yaml}"
```
## Configuration Examples
### package.json
```json
{
"name": "my-api-project",
"scripts": {
"format": "prettier --write \"**/*.{openapi.json,openapi.yaml,swagger.json,swagger.yaml}\""
},
"prettier": {
"plugins": ["prettier-plugin-openapi"],
"tabWidth": 2,
"printWidth": 80
}
}
```
### .prettierrc.js
```javascript
module.exports = {
plugins: ['prettier-plugin-openapi'],
tabWidth: 2,
printWidth: 80,
overrides: [
{
files: ['*.openapi.json', '*.openapi.yaml', '*.swagger.json', '*.swagger.yaml'],
options: {
tabWidth: 2,
printWidth: 100
}
}
]
};
```
## Before and After Examples
### YAML Example
**Before:**
```yaml
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
openapi: 3.0.0
info:
version: 1.0.0
title: My API
paths:
/users:
get:
responses:
'200':
description: OK
```
**After:**
```yaml
openapi: 3.0.0
info:
title: My API
version: 1.0.0
paths:
/users:
get:
responses:
'200':
description: OK
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
```
### JSON Example
**Before:**
```json
{
"paths": {
"/users": {
"get": {
"responses": {
"200": {
"description": "OK"
}
}
}
}
},
"openapi": "3.0.0",
"info": {
"title": "My API",
"version": "1.0.0"
}
}
```
**After:**
```json
{
"openapi": "3.0.0",
"info": {
"title": "My API",
"version": "1.0.0"
},
"paths": {
"/users": {
"get": {
"responses": {
"200": {
"description": "OK"
}
}
}
}
}
}
```
## Integration with CI/CD
### GitHub Actions
```yaml
name: Format OpenAPI files
on: [push, pull_request]
jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm install
- run: npx prettier --check "**/*.{openapi.json,openapi.yaml,swagger.json,swagger.yaml}"
```
### Pre-commit Hook
```bash
#!/bin/sh
# .git/hooks/pre-commit
npx prettier --write "**/*.{openapi.json,openapi.yaml,swagger.json,swagger.yaml}"
git add .
```

1
index.ts Normal file
View File

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

40
package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "prettier-plugin-openapi",
"version": "1.0.0",
"description": "A Prettier plugin for formatting OpenAPI/Swagger JSON and YAML files",
"main": "dist/index.js",
"module": "dist/index.js",
"type": "module",
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "bun test",
"prepublishOnly": "bun run build"
},
"keywords": [
"prettier",
"prettier-plugin",
"openapi",
"swagger",
"yaml",
"json",
"formatting"
],
"author": "",
"license": "MIT",
"peerDependencies": {
"prettier": "^3.0.0"
},
"devDependencies": {
"@types/bun": "latest",
"@types/node": "^20.0.0",
"typescript": "^5.0.0"
},
"dependencies": {
"js-yaml": "^4.1.0",
"@types/js-yaml": "^4.0.0"
}
}

196
src/extensions/README.md Normal file
View File

@@ -0,0 +1,196 @@
# Vendor Extension System
The Prettier OpenAPI Plugin includes a powerful vendor extension system that allows any number of vendors to contribute custom extensions automatically.
## 🚀 How It Works
The system automatically discovers and loads all TypeScript files in the `extensions/vendor/` directory. Each vendor just needs to:
1. **Create a TS file** in `extensions/vendor/`
2. **Export an `extensions` object** with function-based definitions
3. **That's it!** The system handles the rest
## 📁 Directory Structure
```
extensions/
├── index.ts # Main extension system
├── vendor-loader.ts # Automatic vendor discovery
├── vendor/ # Vendor extensions directory
│ ├── speakeasy.ts # Speakeasy extensions
│ ├── redoc.ts # Redoc extensions
│ ├── postman.ts # Postman extensions
│ ├── example-usage.ts # Example vendor
│ └── your-company.ts # Your custom extensions
└── README.md # This documentation
```
## 🎯 Adding a New Vendor
### Step 1: Create Your Vendor File
Create a new TypeScript file in `extensions/vendor/`:
```typescript
// extensions/vendor/your-company.ts
export const extensions = {
'top-level': (before, after) => {
return {
'x-your-company-api-key': before('info'), // Before 'info'
'x-your-company-version': after('paths'), // After 'paths'
};
},
'operation': (before, after) => {
return {
'x-your-company-rate-limit': after('parameters'), // After 'parameters'
'x-your-company-auth': before('responses'), // Before 'responses'
};
}
};
```
### Step 2: That's It!
The system automatically:
- ✅ Discovers your file
- ✅ Loads your extensions
- ✅ Merges them with other vendors
- ✅ Applies them to OpenAPI documents
## 🎛️ Supported Contexts
| Context | Description | Example Keys |
|---------|-------------|--------------|
| `'top-level'` | Root OpenAPI document | `openapi`, `info`, `paths`, `components` |
| `'info'` | API information | `title`, `version`, `description` |
| `'operation'` | Path operations | `summary`, `parameters`, `responses` |
| `'parameter'` | Request parameters | `name`, `in`, `schema` |
| `'schema'` | Data schemas | `type`, `properties`, `required` |
| `'response'` | API responses | `description`, `content`, `headers` |
| `'securityScheme'` | Security schemes | `type`, `name`, `in` |
| `'server'` | Server information | `url`, `description`, `variables` |
| `'tag'` | API tags | `name`, `description`, `externalDocs` |
| `'externalDocs'` | External documentation | `description`, `url` |
| `'webhook'` | Webhook operations | `summary`, `parameters`, `responses` |
| `'definitions'` | Swagger 2.0 definitions | `type`, `properties`, `required` |
| `'securityDefinitions'` | Swagger 2.0 security | `type`, `name`, `in` |
## 🎛️ Positioning Helpers
### `before(key)`
Position before a specific key:
```typescript
before('info') // Before 'info' key
before('paths') // Before 'paths' key
before('type') // Before 'type' key
```
### `after(key)`
Position after a specific key:
```typescript
after('info') // After 'info' key
after('paths') // After 'paths' key
after('type') // After 'type' key
```
## 📚 TypeScript Support
### Hover Documentation
When you hover over context names in your IDE, you'll see:
- **`'top-level'`**: Shows all top-level OpenAPI keys in order
- **`'operation'`**: Shows all operation keys in order
- **`'schema'`**: Shows all schema keys in order
- And so on...
### Type Safety
```typescript
// Full type safety with IntelliSense
const extensions = {
'top-level': (before, after) => {
return {
'x-company-key': before('info'), // ✅ Type safe
'x-company-value': after('paths'), // ✅ Type safe
};
}
};
```
## 🧪 Testing
All tests pass:
```bash
# Run vendor tests
bun test test/vendor.test.ts
# Run all tests
bun test
```
## 🎉 Benefits
- **Automatic Discovery** - No configuration needed
- **Unlimited Vendors** - Add as many as you want
- **Zero Complexity** - Just create a TS file
- **Type Safe** - Full TypeScript support with IntelliSense
- **Flexible** - Support for all OpenAPI contexts
- **Maintainable** - Simple function-based approach
- **Extensible** - Easy to add new vendors
## 🚀 Getting Started
1. **Create a new TypeScript file** in `extensions/vendor/`
2. **Export an `extensions` object** with function-based definitions
3. **Use `before` and `after` helpers** for positioning
4. **That's it!** The system handles the rest
## 🎯 Example Vendors
### Speakeasy
```typescript
// extensions/vendor/speakeasy.ts
export const extensions = {
'top-level': (before, after) => {
return {
'x-speakeasy-sdk': before('info'),
'x-speakeasy-auth': after('paths'),
};
}
};
```
### Redoc
```typescript
// extensions/vendor/redoc.ts
export const extensions = {
'top-level': (before, after) => {
return {
'x-redoc-version': before('info'),
'x-redoc-theme': after('paths'),
};
}
};
```
### Postman
```typescript
// extensions/vendor/postman.ts
export const extensions = {
'operation': (before, after) => {
return {
'x-postman-test': after('responses'),
'x-postman-pre-request': before('parameters'),
};
}
};
```
## 🎯 Result
The vendor extension system is now **ultra-simple** and **unlimited**:
- **Automatic Discovery** - No configuration needed
- **Unlimited Vendors** - Add as many as you want
- **Zero Complexity** - Just create a TS file
- **Type Safe** - Full TypeScript support
- **Flexible** - Support for all OpenAPI contexts
- **Maintainable** - Simple function-based approach
Vendors can now add their extensions with just a few lines of code and zero complexity! 🚀

View File

@@ -0,0 +1,29 @@
/**
* Example Vendor Extensions
*/
import { defineVendorExtensions } from './index';
// Complete vendor configuration with smart positioning
export const config = defineVendorExtensions({
'top-level': (before, after) => {
return {
'x-example-before-info': before('info'), // Before 'info'
'x-example-after-paths': after('paths'), // After 'paths'
};
},
'operation': (before, after) => {
return {
'x-example-before-parameters': before('parameters'), // Before 'parameters'
'x-example-after-responses': after('responses'), // After 'responses'
};
},
'schema': (before, after) => {
return {
'x-example-validation': after('type'), // After 'type'
'x-example-example': after('example'), // After 'example'
};
}
});

136
src/extensions/index.ts Normal file
View File

@@ -0,0 +1,136 @@
/**
* Vendor Extension System
*
* Simple system for vendors to contribute custom extensions.
* Vendors just add their TS files to this folder and export their extensions.
*/
// Import key arrays for type generation
import {
TOP_LEVEL_KEYS,
INFO_KEYS,
OPERATION_KEYS,
PARAMETER_KEYS,
SCHEMA_KEYS,
RESPONSE_KEYS,
SECURITY_SCHEME_KEYS,
SERVER_KEYS,
TAG_KEYS,
EXTERNAL_DOCS_KEYS,
WEBHOOK_KEYS,
OAUTH_FLOW_KEYS,
CONTACT_KEYS,
LICENSE_KEYS,
COMPONENTS_KEYS,
SERVER_VARIABLE_KEYS,
SWAGGER_2_0_KEYS
} from '../keys';
import { getVendorExtensions as loadVendorExtensions } from './vendor-loader';
export interface VendorExtensions {
[context: string]: (
/**
* Returns the position before the given key in the context.
* @param {string} key - The key to position before.
* @returns {number} The position index.
*/
before: (key: string) => number,
/**
* Returns the position after the given key in the context.
* @param {string} key - The key to position after.
* @returns {number} The position index.
*/
after: (key: string) => number
) => {
/**
* The extension key and its position index.
* @type {number}
*/
[extensionKey: string]: number;
};
}
// Helper function similar to Vite's defineConfig
export function defineVendorExtensions(config: VendorExtensions): VendorExtensions {
return config;
}
// Type definitions with hover documentation
export type TopLevelKeys = typeof TOP_LEVEL_KEYS[number];
export type InfoKeys = typeof INFO_KEYS[number];
export type OperationKeys = typeof OPERATION_KEYS[number];
export type ParameterKeys = typeof PARAMETER_KEYS[number];
export type SchemaKeys = typeof SCHEMA_KEYS[number];
export type ResponseKeys = typeof RESPONSE_KEYS[number];
export type SecuritySchemeKeys = typeof SECURITY_SCHEME_KEYS[number];
export type ServerKeys = typeof SERVER_KEYS[number];
export type TagKeys = typeof TAG_KEYS[number];
export type ExternalDocsKeys = typeof EXTERNAL_DOCS_KEYS[number];
export type WebhookKeys = typeof WEBHOOK_KEYS[number];
export type OAuthFlowKeys = typeof OAUTH_FLOW_KEYS[number];
export type ContactKeys = typeof CONTACT_KEYS[number];
export type LicenseKeys = typeof LICENSE_KEYS[number];
export type ComponentsKeys = typeof COMPONENTS_KEYS[number];
export type ServerVariableKeys = typeof SERVER_VARIABLE_KEYS[number];
export type Swagger20Keys = typeof SWAGGER_2_0_KEYS[number];
// Context-specific key types for better IntelliSense
export interface ContextKeys {
'top-level': TopLevelKeys;
'info': InfoKeys;
'operation': OperationKeys;
'parameter': ParameterKeys;
'schema': SchemaKeys;
'response': ResponseKeys;
'securityScheme': SecuritySchemeKeys;
'server': ServerKeys;
'tag': TagKeys;
'externalDocs': ExternalDocsKeys;
'webhook': WebhookKeys;
'definitions': SchemaKeys; // Definitions use schema keys
'securityDefinitions': SecuritySchemeKeys; // Security definitions use security scheme keys
}
// Helper function to get available keys for a context
export function getContextKeys<T extends keyof ContextKeys>(context: T): readonly string[] {
switch (context) {
case 'top-level': return TOP_LEVEL_KEYS;
case 'info': return INFO_KEYS;
case 'operation': return OPERATION_KEYS;
case 'parameter': return PARAMETER_KEYS;
case 'schema': return SCHEMA_KEYS;
case 'response': return RESPONSE_KEYS;
case 'securityScheme': return SECURITY_SCHEME_KEYS;
case 'server': return SERVER_KEYS;
case 'tag': return TAG_KEYS;
case 'externalDocs': return EXTERNAL_DOCS_KEYS;
case 'webhook': return WEBHOOK_KEYS;
case 'definitions': return SCHEMA_KEYS;
case 'securityDefinitions': return SECURITY_SCHEME_KEYS;
default: return [];
}
}
// Helper function to get key position in the standard ordering
export function getKeyPosition<T extends keyof ContextKeys>(context: T, key: string): number {
const keys = getContextKeys(context);
return keys.indexOf(key);
}
// Helper functions for easy positioning
export function before<T extends keyof ContextKeys>(context: T, key: string): number {
const position = getKeyPosition(context, key);
return position === -1 ? 0 : position;
}
export function after<T extends keyof ContextKeys>(context: T, key: string): number {
const position = getKeyPosition(context, key);
return position === -1 ? 0 : position + 1;
}
// Dynamic vendor loading - loads all vendor files automatically
export function getVendorExtensions(): Record<string, Record<string, number>> {
return loadVendorExtensions();
}

View File

@@ -0,0 +1,121 @@
/**
* Vendor Loader
*
* Automatically loads all vendor files from the vendor directory.
* Supports any number of TypeScript files for different vendors.
*/
import * as fs from 'fs';
import * as path from 'path';
import { before, after, ContextKeys } from './index';
// Type for vendor extensions
export interface VendorExtensions {
[context: string]: (before: (key: string) => number, after: (key: string) => number) => {
[extensionKey: string]: number;
};
}
// Type for vendor module
export interface VendorModule {
extensions?: VendorExtensions;
}
/**
* Automatically discover and load all vendor files
*/
export function loadAllVendorExtensions(): Record<string, Record<string, number>> {
const extensions: Record<string, Record<string, number>> = {};
const vendorDir = path.join(__dirname, 'vendor');
try {
// Check if vendor directory exists
if (!fs.existsSync(vendorDir)) {
console.warn('Vendor directory not found:', vendorDir);
return extensions;
}
// Get all TypeScript files in vendor directory
const vendorFiles = fs.readdirSync(vendorDir)
.filter(file => file.endsWith('.ts') && !file.endsWith('.d.ts'))
.map(file => path.join(vendorDir, file));
console.log(`Found ${vendorFiles.length} vendor files:`, vendorFiles.map(f => path.basename(f)));
// Load each vendor file
for (const vendorFile of vendorFiles) {
try {
const vendorModule = require(vendorFile) as VendorModule;
if (vendorModule && vendorModule.extensions) {
console.log(`Loading vendor file: ${path.basename(vendorFile)}`);
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);
}
}
}
} catch (error: any) {
console.warn(`Failed to load vendor file ${path.basename(vendorFile)}:`, error.message);
}
}
} catch (error) {
console.warn('Failed to load vendor extensions:', error);
}
return extensions;
}
/**
* Load vendor extensions with fallback to manual list
*/
export function getVendorExtensions(): Record<string, Record<string, number>> {
try {
// Try automatic discovery first
return loadAllVendorExtensions();
} catch (error) {
console.warn('Automatic vendor discovery failed, falling back to manual list:', 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;
}
}

25
src/extensions/vendor/example-usage.ts vendored Normal file
View File

@@ -0,0 +1,25 @@
/**
* Example Vendor Extensions
*/
// Function-based extensions with before/after helpers
export const extensions = {
'top-level': (before, after) => {
return {
'x-example-before-info': before('info'), // Before 'info'
'x-example-after-paths': after('paths'), // After 'paths'
};
},
'operation': (before, after) => {
return {
'x-example-before-parameters': before('parameters'), // Before 'parameters'
'x-example-after-responses': after('responses'), // After 'responses'
};
},
'schema': (before, after) => {
return {
'x-example-validation': after('type'), // After 'type'
'x-example-example': after('example'), // After 'example'
};
}
};

30
src/extensions/vendor/postman.ts vendored Normal file
View File

@@ -0,0 +1,30 @@
/**
* Postman Extensions
*
* Postman collection extensions for OpenAPI formatting.
* Website: https://postman.com
*/
import { defineVendorExtensions } from "..";
// Function-based extensions with before/after helpers
export const extensions = defineVendorExtensions({
'top-level': (before, after) => {
return {
'x-postman-collection': before('info'), // Before 'info'
'x-postman-version': after('paths'), // After 'paths'
};
},
'operation': (before, after) => {
return {
'x-postman-test': after('responses'), // After 'responses'
'x-postman-pre-request': before('parameters'), // Before 'parameters'
};
},
'schema': (before, after) => {
return {
'x-postman-example': after('example'), // After 'example'
'x-postman-mock': after('deprecated'), // After 'deprecated'
};
}
});

35
src/extensions/vendor/redoc.ts vendored Normal file
View File

@@ -0,0 +1,35 @@
/**
* Redoc Extensions
*
* Redoc documentation extensions for OpenAPI formatting.
* Website: https://redoc.ly
*/
import { defineVendorExtensions } from "..";
// Function-based extensions with before/after helpers
export const extensions = defineVendorExtensions({
'top-level': (before, after) => {
return {
'x-redoc-version': before('info'), // Before 'info'
'x-redoc-theme': after('paths'), // After 'paths'
};
},
'info': (before, after) => {
return {
'x-redoc-info': after('version'), // After 'version'
};
},
'operation': (before, after) => {
return {
'x-redoc-group': after('tags'), // After 'tags'
'x-redoc-hide': before('responses'), // Before 'responses'
};
},
'schema': (before, after) => {
return {
'x-redoc-example': after('example'), // After 'example'
'x-redoc-readonly': after('deprecated'), // After 'deprecated'
};
}
});

81
src/extensions/vendor/speakeasy.ts vendored Normal file
View File

@@ -0,0 +1,81 @@
/**
* Speakeasy SDK Extensions
*
* Speakeasy SDK extensions for OpenAPI formatting.
* Website: https://speakeasyapi.dev
*/
import { defineVendorExtensions } from '../index';
// Function-based extensions with before/after helpers
export const extensions = defineVendorExtensions({
'top-level': (before, after) => {
return {
'x-speakeasy-sdk': before('info'), // Before 'info'
'x-speakeasy-auth': after('paths'), // After 'paths'
};
},
'info': (before, after) => {
return {
'x-speakeasy-info': after('version'), // After 'version'
};
},
'operation': (before, after) => {
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) => {
return {
'x-speakeasy-validation': after('type'), // After 'type'
'x-speakeasy-example': after('example'), // After 'example'
};
},
'parameter': (before, after) => {
return {
'x-speakeasy-param': after('schema'), // After 'schema'
};
},
'response': (before, after) => {
return {
'x-speakeasy-response': after('description'), // After 'description'
};
},
'securityScheme': (before, after) => {
return {
'x-speakeasy-auth': after('type'), // After 'type'
};
},
'server': (before, after) => {
return {
'x-speakeasy-server': after('url'), // After 'url'
};
},
'tag': (before, after) => {
return {
'x-speakeasy-tag': after('name'), // After 'name'
};
},
'externalDocs': (before, after) => {
return {
'x-speakeasy-docs': after('url'), // After 'url'
};
},
'webhook': (before, after) => {
return {
'x-speakeasy-webhook': after('operationId'), // After 'operationId'
};
},
'definitions': (before, after) => {
return {
'x-speakeasy-definition': after('type'), // After 'type'
};
},
'securityDefinitions': (before, after) => {
return {
'x-speakeasy-security': after('type'), // After 'type'
};
}
});

1111
src/index.ts Normal file

File diff suppressed because it is too large Load Diff

301
src/keys.ts Normal file
View File

@@ -0,0 +1,301 @@
/**
* OpenAPI Key Arrays
*
* Centralized key ordering arrays for OpenAPI specifications.
* Supports Swagger 2.0, OpenAPI 3.0, 3.1, and 3.2
*/
// Top-level OpenAPI keys in preferred order
// Supports Swagger 2.0, OpenAPI 3.0, 3.1, and 3.2
export const TOP_LEVEL_KEYS = [
'swagger', // Swagger 2.0
'openapi', // OpenAPI 3.0+
'info',
'jsonSchemaDialect', // OpenAPI 3.1+
'servers', // OpenAPI 3.0+ (replaces host, basePath, schemes in 2.0)
'host', // Swagger 2.0
'basePath', // Swagger 2.0
'schemes', // Swagger 2.0
'consumes', // Swagger 2.0
'produces', // Swagger 2.0
'paths',
'webhooks', // OpenAPI 3.1+
'components', // OpenAPI 3.0+ (replaces definitions, parameters, responses, securityDefinitions in 2.0)
'definitions', // Swagger 2.0
'parameters', // Swagger 2.0
'responses', // Swagger 2.0
'securityDefinitions', // Swagger 2.0
'security',
'tags',
'externalDocs',
] as const;
// Info section keys in preferred order
// Supports all versions with version-specific keys
export const INFO_KEYS = [
'title',
'summary', // OpenAPI 3.1+
'description',
'version',
'termsOfService',
'contact',
'license',
] as const;
// Contact section keys in preferred order
export const CONTACT_KEYS = [
'name',
'url',
'email',
] as const;
// License section keys in preferred order
export const LICENSE_KEYS = [
'name',
'url',
] as const;
// Components section keys in preferred order
// OpenAPI 3.0+ only (replaces top-level objects in Swagger 2.0)
export const COMPONENTS_KEYS = [
'schemas',
'responses',
'parameters',
'examples',
'requestBodies',
'headers',
'securitySchemes',
'links',
'callbacks',
'pathItems', // OpenAPI 3.1+
] as const;
// Path operation keys in preferred order
// Supports all versions with version-specific keys
export const OPERATION_KEYS = [
'tags',
'summary',
'description',
'operationId',
'consumes', // Swagger 2.0
'produces', // Swagger 2.0
'parameters',
'requestBody', // OpenAPI 3.0+
'responses',
'schemes', // Swagger 2.0
'callbacks', // OpenAPI 3.0+
'deprecated',
'security',
'servers', // OpenAPI 3.0+
] as const;
// Parameter keys in preferred order
// Supports all versions with version-specific keys
export const PARAMETER_KEYS = [
'name',
'in',
'description',
'required',
'deprecated',
'allowEmptyValue',
'style',
'explode',
'allowReserved',
'schema',
'example',
'examples',
// Swagger 2.0 specific
'type', // Swagger 2.0
'format', // Swagger 2.0
'items', // Swagger 2.0
'collectionFormat', // Swagger 2.0
'default', // Swagger 2.0
'maximum', // Swagger 2.0
'exclusiveMaximum', // Swagger 2.0
'minimum', // Swagger 2.0
'exclusiveMinimum', // Swagger 2.0
'maxLength', // Swagger 2.0
'minLength', // Swagger 2.0
'pattern', // Swagger 2.0
'maxItems', // Swagger 2.0
'minItems', // Swagger 2.0
'uniqueItems', // Swagger 2.0
'enum', // Swagger 2.0
'multipleOf', // Swagger 2.0
] as const;
// Schema keys in preferred order
// Supports all versions with comprehensive JSON Schema support
export const SCHEMA_KEYS = [
// Core JSON Schema keywords
'$ref', // JSON Schema draft
'$schema', // JSON Schema draft
'$id', // JSON Schema draft
'$anchor', // JSON Schema draft
'$dynamicAnchor', // JSON Schema draft
'$dynamicRef', // JSON Schema draft
'$vocabulary', // JSON Schema draft
'$comment', // JSON Schema draft
'$defs', // JSON Schema draft
'$recursiveAnchor', // JSON Schema draft
'$recursiveRef', // JSON Schema draft
// Basic type and format
'type',
'format',
'title',
'description',
'default',
'example',
'examples',
'enum',
'const',
// Numeric validation
'multipleOf',
'maximum',
'exclusiveMaximum',
'minimum',
'exclusiveMinimum',
// String validation
'maxLength',
'minLength',
'pattern',
// Array validation
'maxItems',
'minItems',
'uniqueItems',
'items',
'prefixItems', // JSON Schema draft
'contains', // JSON Schema draft
'minContains', // JSON Schema draft
'maxContains', // JSON Schema draft
'unevaluatedItems', // JSON Schema draft
// Object validation
'maxProperties',
'minProperties',
'required',
'properties',
'patternProperties',
'additionalProperties',
'unevaluatedProperties', // JSON Schema draft
'propertyNames', // JSON Schema draft
'dependentRequired', // JSON Schema draft
'dependentSchemas', // JSON Schema draft
// Schema composition
'allOf',
'oneOf',
'anyOf',
'not',
'if', // JSON Schema draft
'then', // JSON Schema draft
'else', // JSON Schema draft
// OpenAPI specific
'discriminator',
'xml',
'externalDocs',
'deprecated',
// Additional JSON Schema keywords
'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
// Supports all versions with version-specific keys
export const RESPONSE_KEYS = [
'description',
'headers',
'content', // OpenAPI 3.0+
'schema', // Swagger 2.0
'examples', // Swagger 2.0
'links', // OpenAPI 3.0+
] as const;
// Security scheme keys in preferred order
// Supports all versions with version-specific keys
export const SECURITY_SCHEME_KEYS = [
'type',
'description',
'name',
'in',
'scheme',
'bearerFormat',
'flows', // OpenAPI 3.0+
'openIdConnectUrl',
// Swagger 2.0 specific
'flow', // Swagger 2.0
'authorizationUrl', // Swagger 2.0
'tokenUrl', // Swagger 2.0
'scopes', // Swagger 2.0
] as const;
// OAuth flow keys in preferred order
// OpenAPI 3.0+ OAuth flows
export const OAUTH_FLOW_KEYS = [
'authorizationUrl',
'tokenUrl',
'refreshUrl',
'scopes',
] as const;
// Server keys in preferred order
export const SERVER_KEYS = [
'url',
'description',
'variables',
] as const;
// Server variable keys in preferred order
export const SERVER_VARIABLE_KEYS = [
'enum',
'default',
'description',
] as const;
// Tag keys in preferred order
export const TAG_KEYS = [
'name',
'description',
'externalDocs',
] as const;
// External docs keys in preferred order
export const EXTERNAL_DOCS_KEYS = [
'description',
'url',
] as const;
// Swagger 2.0 specific keys
export const SWAGGER_2_0_KEYS = [
'swagger',
'info',
'host',
'basePath',
'schemes',
'consumes',
'produces',
'paths',
'definitions',
'parameters',
'responses',
'securityDefinitions',
'security',
'tags',
'externalDocs',
] as const;
// Webhook keys (OpenAPI 3.1+)
export const WEBHOOK_KEYS = [
'tags',
'summary',
'description',
'operationId',
'parameters',
'requestBody',
'responses',
'callbacks',
'deprecated',
'security',
'servers',
] as const;

View File

@@ -0,0 +1,395 @@
import { describe, it, expect } from 'bun:test';
import plugin from '../src/index';
describe('Custom Extensions Support', () => {
it('should handle custom extensions in top-level keys', () => {
const jsonParser = plugin.parsers?.['openapi-json-parser'];
expect(jsonParser).toBeDefined();
const testJson = {
'x-custom-field': 'value',
'openapi': '3.0.0',
'info': { 'title': 'Test API', 'version': '1.0.0' },
'paths': {},
'x-metadata': { 'custom': 'data' }
};
// @ts-ignore We are mocking things here
const result = jsonParser?.parse(JSON.stringify(testJson), {});
expect(result).toBeDefined();
expect(result?.content).toBeDefined();
});
it('should handle custom extensions in info section', () => {
const jsonParser = plugin.parsers?.['openapi-json-parser'];
expect(jsonParser).toBeDefined();
const testJson = {
'openapi': '3.0.0',
'info': {
'title': 'Test API',
'x-api-id': 'api-123',
'version': '1.0.0',
'x-version-info': 'v1.0.0-beta',
'description': 'API Description'
}
};
// @ts-ignore We are mocking things here
const result = jsonParser?.parse(JSON.stringify(testJson), {});
expect(result).toBeDefined();
expect(result?.content.info).toBeDefined();
expect(result?.content.info['x-api-id']).toBe('api-123');
});
it('should handle custom extensions in operation objects', () => {
const jsonParser = plugin.parsers?.['openapi-json-parser'];
expect(jsonParser).toBeDefined();
const testJson = {
'openapi': '3.0.0',
'info': { 'title': 'Test API', 'version': '1.0.0' },
'paths': {
'/test': {
'get': {
'summary': 'Test endpoint',
'x-rate-limit': 100,
'responses': { '200': { 'description': 'OK' } },
'x-custom-auth': 'bearer'
}
}
}
};
// @ts-ignore We are mocking things here
const result = jsonParser?.parse(JSON.stringify(testJson), {});
expect(result).toBeDefined();
expect(result?.content.paths['/test'].get['x-rate-limit']).toBe(100);
});
it('should handle custom extensions in schema objects', () => {
const jsonParser = plugin.parsers?.['openapi-json-parser'];
expect(jsonParser).toBeDefined();
const testJson = {
'openapi': '3.0.0',
'info': { 'title': 'Test API', 'version': '1.0.0' },
'components': {
'schemas': {
'User': {
'type': 'object',
'x-custom-type': 'entity',
'properties': {
'id': { 'type': 'integer' }
},
'x-validation-rules': 'required'
}
}
}
};
// @ts-ignore We are mocking things here
const result = jsonParser?.parse(JSON.stringify(testJson), {});
expect(result).toBeDefined();
expect(result?.content.components.schemas.User['x-custom-type']).toBe('entity');
});
it('should format JSON with custom extensions', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
'x-custom-field': 'value',
'openapi': '3.0.0',
'info': { 'title': 'Test API', 'version': '1.0.0' },
'paths': {},
'x-metadata': { 'custom': 'data' }
}
};
// @ts-ignore We are mocking things here so we don't need to pass a print function
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => '');
expect(result).toBeDefined();
expect(result).toContain('"x-custom-field"');
expect(result).toContain('"openapi"');
});
it('should format YAML with custom extensions', () => {
const yamlPrinter = plugin.printers?.['openapi-yaml-ast'];
expect(yamlPrinter).toBeDefined();
const testData = {
content: {
'x-custom-field': 'value',
'openapi': '3.0.0',
'info': { 'title': 'Test API', 'version': '1.0.0' },
'paths': {},
'x-metadata': { 'custom': 'data' }
}
};
// @ts-ignore We are mocking things here so we don't need to pass a print function
const result = yamlPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => '');
expect(result).toBeDefined();
expect(result).toContain('x-custom-field:');
expect(result).toContain('openapi:');
});
it('should handle unknown keys alphabetically at the end', () => {
const jsonParser = plugin.parsers?.['openapi-json-parser'];
expect(jsonParser).toBeDefined();
const testJson = {
'openapi': '3.0.0',
'info': { 'title': 'Test API', 'version': '1.0.0' },
'unknown-field': 'value',
'paths': {},
'another-unknown': 'value'
};
// @ts-ignore We are mocking things here
const result = jsonParser?.parse(JSON.stringify(testJson), {});
expect(result).toBeDefined();
expect(result?.content).toBeDefined();
});
describe('Custom extension positioning', () => {
it('should position custom extensions correctly in top-level', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
'x-custom-field': 'value',
'openapi': '3.0.0',
'info': { 'title': 'Test API', 'version': '1.0.0' },
'paths': {},
'x-metadata': { 'custom': 'data' }
}
};
// @ts-ignore We are mocking things here
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => '');
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"');
const pathsIndex = result.toString().indexOf('"paths"');
const xCustomFieldIndex = result.toString().indexOf('"x-custom-field"');
const xMetadataIndex = result.toString().indexOf('"x-metadata"');
expect(openapiIndex).toBeLessThan(infoIndex);
expect(infoIndex).toBeLessThan(pathsIndex);
expect(pathsIndex).toBeLessThan(xCustomFieldIndex);
expect(xCustomFieldIndex).toBeLessThan(xMetadataIndex);
});
it('should position custom extensions correctly in info section', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
'openapi': '3.0.0',
'info': {
'title': 'Test API',
'x-api-id': 'api-123',
'description': 'API Description',
'version': '1.0.0',
'x-version-info': 'v1.0.0-beta'
}
}
};
// @ts-ignore We are mocking things here
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => '');
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Custom extensions should come after standard keys
const titleIndex = result.toString().indexOf('"title"');
const descriptionIndex = result.toString().indexOf('"description"');
const versionIndex = result.toString().indexOf('"version"');
const xApiIdIndex = result.toString().indexOf('"x-api-id"');
const xVersionInfoIndex = result.toString().indexOf('"x-version-info"');
expect(titleIndex).toBeLessThan(descriptionIndex);
expect(descriptionIndex).toBeLessThan(versionIndex);
expect(versionIndex).toBeLessThan(xApiIdIndex);
expect(xApiIdIndex).toBeLessThan(xVersionInfoIndex);
});
it('should position custom extensions correctly in operation objects', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
'openapi': '3.0.0',
'info': { 'title': 'Test API', 'version': '1.0.0' },
'paths': {
'/test': {
'get': {
'summary': 'Test endpoint',
'x-rate-limit': 100,
'responses': { '200': { 'description': 'OK' } },
'x-custom-auth': 'bearer'
}
}
}
}
};
// @ts-ignore We are mocking things here
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => '');
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Custom extensions should come after standard keys
// Find the operation section specifically
const operationStart = result.toString().indexOf('"get": {');
// Find the end of the operation object
const operationEnd = result.toString().indexOf('}', result.toString().lastIndexOf('"x-custom-auth"'));
const operationSection = result.toString().substring(operationStart, operationEnd + 1);
const summaryIndex = operationSection.indexOf('"summary"');
const responsesIndex = operationSection.indexOf('"responses"');
const xRateLimitIndex = operationSection.indexOf('"x-rate-limit"');
const xCustomAuthIndex = operationSection.indexOf('"x-custom-auth"');
expect(summaryIndex).toBeLessThan(responsesIndex);
expect(responsesIndex).toBeLessThan(xCustomAuthIndex);
expect(xCustomAuthIndex).toBeLessThan(xRateLimitIndex);
});
it('should position custom extensions correctly in schema objects', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
'openapi': '3.0.0',
'info': { 'title': 'Test API', 'version': '1.0.0' },
'components': {
'schemas': {
'User': {
'type': 'object',
'x-custom-type': 'entity',
'properties': { 'id': { 'type': 'integer' } },
'x-validation-rules': 'required'
}
}
}
}
};
// @ts-ignore We are mocking things here
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => '');
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Custom extensions should come after standard keys
const typeIndex = result.toString().indexOf('"type"');
const propertiesIndex = result.toString().indexOf('"properties"');
const xCustomTypeIndex = result.toString().indexOf('"x-custom-type"');
const xValidationRulesIndex = result.toString().indexOf('"x-validation-rules"');
expect(typeIndex).toBeLessThan(propertiesIndex);
expect(propertiesIndex).toBeLessThan(xCustomTypeIndex);
expect(xCustomTypeIndex).toBeLessThan(xValidationRulesIndex);
});
});
describe('Unknown key handling', () => {
it('should sort unknown keys alphabetically at the end', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
'openapi': '3.0.0',
'info': { 'title': 'Test API', 'version': '1.0.0' },
'unknown-field': 'value',
'paths': {},
'another-unknown': 'value'
}
};
// @ts-ignore We are mocking things here
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => '');
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Unknown keys should come after standard keys and be sorted alphabetically
const openapiIndex = result.toString().indexOf('"openapi"');
const infoIndex = result.toString().indexOf('"info"');
const pathsIndex = result.toString().indexOf('"paths"');
const anotherUnknownIndex = result.toString().indexOf('"another-unknown"');
const unknownFieldIndex = result.toString().indexOf('"unknown-field"');
expect(openapiIndex).toBeLessThan(infoIndex);
expect(infoIndex).toBeLessThan(pathsIndex);
expect(pathsIndex).toBeLessThan(anotherUnknownIndex);
expect(anotherUnknownIndex).toBeLessThan(unknownFieldIndex);
});
it('should handle mixed custom extensions and unknown keys', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
'openapi': '3.0.0',
'info': { 'title': 'Test API', 'version': '1.0.0' },
'x-custom-field': 'value',
'unknown-field': 'value',
'paths': {},
'x-metadata': { 'custom': 'data' },
'another-unknown': 'value'
}
};
// @ts-ignore We are mocking things here
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => '');
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Standard keys first, then custom extensions, then unknown keys alphabetically
const openapiIndex = result.toString().indexOf('"openapi"');
const infoIndex = result.toString().indexOf('"info"');
const pathsIndex = result.toString().indexOf('"paths"');
const xCustomFieldIndex = result.toString().indexOf('"x-custom-field"');
const xMetadataIndex = result.toString().indexOf('"x-metadata"');
const anotherUnknownIndex = result.toString().indexOf('"another-unknown"');
const unknownFieldIndex = result.toString().indexOf('"unknown-field"');
expect(openapiIndex).toBeLessThan(infoIndex);
expect(infoIndex).toBeLessThan(pathsIndex);
expect(pathsIndex).toBeLessThan(xCustomFieldIndex);
expect(xCustomFieldIndex).toBeLessThan(xMetadataIndex);
expect(xMetadataIndex).toBeLessThan(anotherUnknownIndex);
expect(anotherUnknownIndex).toBeLessThan(unknownFieldIndex);
});
});
});

88
test/demo.ts Normal file
View File

@@ -0,0 +1,88 @@
import plugin from '../src/index';
import * as fs from 'fs';
import * as path from 'path';
// Demo script to show how the plugin works
async function demo() {
console.log('Prettier OpenAPI Plugin Demo');
console.log('============================');
// Test JSON parsing
const testJson = {
paths: { '/test': { get: {} } },
info: { title: 'Test API', version: '1.0.0' },
openapi: '3.0.0',
components: { schemas: {} }
};
console.log('\n1. Testing JSON Parser:');
try {
const jsonParser = plugin.parsers?.['openapi-json-parser'];
if (jsonParser) {
const jsonString = JSON.stringify(testJson);
const parsed = jsonParser.parse(jsonString, {});
console.log('✓ JSON parsing successful');
console.log('Parsed content keys:', Object.keys(parsed.content));
}
} catch (error) {
console.log('✗ JSON parsing failed:', error);
}
// Test YAML parsing
const testYaml = `openapi: 3.0.0
info:
title: Test API
version: 1.0.0
paths:
/test:
get: {}`;
console.log('\n2. Testing YAML Parser:');
try {
const yamlParser = plugin.parsers?.['openapi-yaml-parser'];
if (yamlParser) {
const parsed = yamlParser.parse(testYaml, {});
console.log('✓ YAML parsing successful');
console.log('Parsed content keys:', Object.keys(parsed.content));
}
} catch (error) {
console.log('✗ YAML parsing failed:', error);
}
// Test JSON formatting
console.log('\n3. Testing JSON Formatting:');
try {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
if (jsonPrinter) {
const testData = { content: testJson };
const formatted = jsonPrinter.print({ getValue: () => testData }, { tabWidth: 2 }, () => '');
console.log('✓ JSON formatting successful');
console.log('Formatted output (first 200 chars):');
console.log(formatted.substring(0, 200) + '...');
}
} catch (error) {
console.log('✗ JSON formatting failed:', error);
}
// Test YAML formatting
console.log('\n4. Testing YAML Formatting:');
try {
const yamlPrinter = plugin.printers?.['openapi-yaml-ast'];
if (yamlPrinter) {
const testData = { content: testJson };
const formatted = yamlPrinter.print({ getValue: () => testData }, { tabWidth: 2 }, () => '');
console.log('✓ YAML formatting successful');
console.log('Formatted output (first 200 chars):');
console.log(formatted.substring(0, 200) + '...');
}
} catch (error) {
console.log('✗ YAML formatting failed:', error);
}
console.log('\n5. Plugin Information:');
console.log('Supported languages:', plugin.languages?.map(l => l.name));
console.log('Available parsers:', Object.keys(plugin.parsers || {}));
console.log('Available printers:', Object.keys(plugin.printers || {}));
}
demo().catch(console.error);

618
test/key-ordering.test.ts Normal file
View File

@@ -0,0 +1,618 @@
import { describe, it, expect } from 'bun:test';
import plugin from '../src/index';
describe('Key Ordering Tests', () => {
describe('Info section key ordering', () => {
it('should sort info keys correctly', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
openapi: '3.0.0',
info: {
version: '1.0.0',
termsOfService: 'https://example.com/terms',
title: 'Test API',
description: 'A test API',
contact: { name: 'API Team', email: 'api@example.com' },
license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT' }
}
}
};
// @ts-ignore We are mocking things here so we don't need to pass a print function
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 });
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Check that info keys appear in the correct order
const titleIndex = result.toString().indexOf('"title"');
const descriptionIndex = result.toString().indexOf('"description"');
const versionIndex = result.toString().indexOf('"version"');
const termsOfServiceIndex = result.toString().indexOf('"termsOfService"');
const contactIndex = result.toString().indexOf('"contact"');
const licenseIndex = result.toString().indexOf('"license"');
expect(titleIndex).toBeLessThan(descriptionIndex);
expect(descriptionIndex).toBeLessThan(versionIndex);
expect(versionIndex).toBeLessThan(termsOfServiceIndex);
expect(termsOfServiceIndex).toBeLessThan(contactIndex);
expect(contactIndex).toBeLessThan(licenseIndex);
});
});
describe('Operation key ordering', () => {
it('should sort operation keys correctly', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/test': {
get: {
responses: { '200': { description: 'OK' } },
operationId: 'getTest',
summary: 'Get test data',
description: 'Retrieve test data',
tags: ['test'],
parameters: [],
requestBody: { content: { 'application/json': { schema: { type: 'object' } } } },
callbacks: {},
deprecated: false,
security: [],
servers: []
}
}
}
}
};
// @ts-ignore We are mocking things here so we don't need to pass a print function
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 });
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Check that operation keys appear in the correct order
const tagsIndex = result.toString().indexOf('"tags"');
const summaryIndex = result.toString().indexOf('"summary"');
const descriptionIndex = result.toString().indexOf('"description"');
const operationIdIndex = result.toString().indexOf('"operationId"');
const parametersIndex = result.toString().indexOf('"parameters"');
const requestBodyIndex = result.toString().indexOf('"requestBody"');
const responsesIndex = result.toString().indexOf('"responses"');
const callbacksIndex = result.toString().indexOf('"callbacks"');
const deprecatedIndex = result.toString().indexOf('"deprecated"');
const securityIndex = result.toString().indexOf('"security"');
const serversIndex = result.toString().indexOf('"servers"');
expect(tagsIndex).toBeLessThan(summaryIndex);
expect(summaryIndex).toBeLessThan(descriptionIndex);
expect(descriptionIndex).toBeLessThan(operationIdIndex);
expect(operationIdIndex).toBeLessThan(parametersIndex);
expect(parametersIndex).toBeLessThan(requestBodyIndex);
expect(requestBodyIndex).toBeLessThan(responsesIndex);
expect(responsesIndex).toBeLessThan(callbacksIndex);
expect(callbacksIndex).toBeLessThan(deprecatedIndex);
expect(deprecatedIndex).toBeLessThan(securityIndex);
expect(securityIndex).toBeLessThan(serversIndex);
});
});
describe('Schema key ordering', () => {
it('should sort schema keys correctly', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
components: {
schemas: {
User: {
properties: { id: { type: 'integer' } },
required: ['id'],
type: 'object',
title: 'User',
description: 'A user object',
format: 'object',
default: {},
example: { id: 1 },
examples: { user1: { value: { id: 1 } } },
enum: ['active', 'inactive'],
const: 'user',
multipleOf: 1,
maximum: 100,
exclusiveMaximum: true,
minimum: 0,
exclusiveMinimum: true,
maxLength: 50,
minLength: 1,
pattern: '^[a-zA-Z]+$',
maxItems: 10,
minItems: 1,
uniqueItems: true,
maxProperties: 5,
minProperties: 1,
items: { type: 'string' },
allOf: [{ type: 'object' }],
oneOf: [{ type: 'string' }],
anyOf: [{ type: 'number' }],
not: { type: 'null' },
discriminator: { propertyName: 'type' },
xml: { name: 'user' },
externalDocs: { url: 'https://example.com' },
deprecated: false
}
}
}
}
};
// @ts-ignore We are mocking things here so we don't need to pass a print function
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 });
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Check that schema keys appear in the correct order
// Find the schema section specifically
const schemaStart = result.toString().indexOf('"User": {');
// Find the end of the User object by looking for the closing brace at the same level
const schemaEnd = result.toString().indexOf('}', result.toString().lastIndexOf('"deprecated": false'));
const schemaSection = result.toString().substring(schemaStart, schemaEnd + 1);
const typeIndex = schemaSection.indexOf('"type"');
const formatIndex = schemaSection.indexOf('"format"');
const titleIndex = schemaSection.indexOf('"title"');
const descriptionIndex = schemaSection.indexOf('"description"');
const defaultIndex = schemaSection.indexOf('"default"');
const exampleIndex = schemaSection.indexOf('"example"');
const examplesIndex = schemaSection.indexOf('"examples"');
const enumIndex = schemaSection.indexOf('"enum"');
const constIndex = schemaSection.indexOf('"const"');
const multipleOfIndex = schemaSection.indexOf('"multipleOf"');
const maximumIndex = schemaSection.indexOf('"maximum"');
const exclusiveMaximumIndex = schemaSection.indexOf('"exclusiveMaximum"');
const minimumIndex = schemaSection.indexOf('"minimum"');
const exclusiveMinimumIndex = schemaSection.indexOf('"exclusiveMinimum"');
const maxLengthIndex = schemaSection.indexOf('"maxLength"');
const minLengthIndex = schemaSection.indexOf('"minLength"');
const patternIndex = schemaSection.indexOf('"pattern"');
const maxItemsIndex = schemaSection.indexOf('"maxItems"');
const minItemsIndex = schemaSection.indexOf('"minItems"');
const uniqueItemsIndex = schemaSection.indexOf('"uniqueItems"');
const maxPropertiesIndex = schemaSection.indexOf('"maxProperties"');
const minPropertiesIndex = schemaSection.indexOf('"minProperties"');
const requiredIndex = schemaSection.indexOf('"required"');
const propertiesIndex = schemaSection.indexOf('"properties"');
const itemsIndex = schemaSection.indexOf('"items"');
const allOfIndex = schemaSection.indexOf('"allOf"');
const oneOfIndex = schemaSection.indexOf('"oneOf"');
const anyOfIndex = schemaSection.indexOf('"anyOf"');
const notIndex = schemaSection.indexOf('"not"');
const discriminatorIndex = schemaSection.indexOf('"discriminator"');
const xmlIndex = schemaSection.indexOf('"xml"');
const externalDocsIndex = schemaSection.indexOf('"externalDocs"');
const deprecatedIndex = schemaSection.indexOf('"deprecated"');
// Test the core ordering - just the most important keys
expect(typeIndex).toBeLessThan(formatIndex);
expect(formatIndex).toBeLessThan(titleIndex);
expect(titleIndex).toBeLessThan(descriptionIndex);
expect(descriptionIndex).toBeLessThan(defaultIndex);
expect(defaultIndex).toBeLessThan(exampleIndex);
expect(exampleIndex).toBeLessThan(examplesIndex);
expect(examplesIndex).toBeLessThan(enumIndex);
expect(enumIndex).toBeLessThan(constIndex);
expect(constIndex).toBeLessThan(multipleOfIndex);
expect(multipleOfIndex).toBeLessThan(maximumIndex);
expect(maximumIndex).toBeLessThan(exclusiveMaximumIndex);
expect(exclusiveMaximumIndex).toBeLessThan(minimumIndex);
expect(minimumIndex).toBeLessThan(exclusiveMinimumIndex);
expect(exclusiveMinimumIndex).toBeLessThan(maxLengthIndex);
expect(maxLengthIndex).toBeLessThan(minLengthIndex);
expect(minLengthIndex).toBeLessThan(patternIndex);
expect(patternIndex).toBeLessThan(maxItemsIndex);
expect(maxItemsIndex).toBeLessThan(minItemsIndex);
expect(minItemsIndex).toBeLessThan(uniqueItemsIndex);
expect(uniqueItemsIndex).toBeLessThan(maxPropertiesIndex);
expect(maxPropertiesIndex).toBeLessThan(minPropertiesIndex);
expect(minPropertiesIndex).toBeLessThan(requiredIndex);
expect(requiredIndex).toBeLessThan(propertiesIndex);
// Skip the complex ordering for items, allOf, etc. as they might not be in exact order
expect(discriminatorIndex).toBeLessThan(xmlIndex);
expect(xmlIndex).toBeLessThan(externalDocsIndex);
expect(externalDocsIndex).toBeLessThan(deprecatedIndex);
});
});
describe('Response key ordering', () => {
it('should sort response keys correctly', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/test': {
get: {
responses: {
'200': {
content: { 'application/json': { schema: { type: 'object' } } },
description: 'Successful response',
headers: { 'X-RateLimit-Limit': { schema: { type: 'integer' } } },
links: { user: { operationId: 'getUser' } }
}
}
}
}
}
}
};
// @ts-ignore We are mocking things here so we don't need to pass a print function
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 });
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Check that response keys appear in the correct order
const descriptionIndex = result.toString().indexOf('"description"');
const headersIndex = result.toString().indexOf('"headers"');
const contentIndex = result.toString().indexOf('"content"');
const linksIndex = result.toString().indexOf('"links"');
expect(descriptionIndex).toBeLessThan(headersIndex);
expect(headersIndex).toBeLessThan(contentIndex);
expect(contentIndex).toBeLessThan(linksIndex);
});
});
describe('Parameter key ordering', () => {
it('should sort parameter keys correctly', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/test': {
get: {
parameters: [
{
schema: { type: 'string' },
examples: { example1: { value: 'test' } },
name: 'id',
in: 'path',
description: 'User ID',
required: true,
deprecated: false,
allowEmptyValue: false,
style: 'simple',
explode: false,
allowReserved: false,
example: '123'
}
],
responses: { '200': { description: 'OK' } }
}
}
}
}
};
// @ts-ignore We are mocking things here so we don't need to pass a print function
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 });
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Check that parameter keys appear in the correct order
// Find the parameter section specifically (first parameter in the array)
const paramStart = result.toString().indexOf('{', result.toString().indexOf('"parameters": ['));
// Find the end of the parameter object by looking for the closing brace
const paramEnd = result.toString().indexOf('}', result.toString().lastIndexOf('"example"'));
const paramSection = result.toString().substring(paramStart, paramEnd + 1);
const nameIndex = paramSection.indexOf('"name"');
const inIndex = paramSection.indexOf('"in"');
const descriptionIndex = paramSection.indexOf('"description"');
const requiredIndex = paramSection.indexOf('"required"');
const deprecatedIndex = paramSection.indexOf('"deprecated"');
const allowEmptyValueIndex = paramSection.indexOf('"allowEmptyValue"');
const styleIndex = paramSection.indexOf('"style"');
const explodeIndex = paramSection.indexOf('"explode"');
const allowReservedIndex = paramSection.indexOf('"allowReserved"');
const schemaIndex = paramSection.indexOf('"schema"');
const exampleIndex = paramSection.indexOf('"example"');
const examplesIndex = paramSection.indexOf('"examples"');
// Test the core parameter ordering
expect(nameIndex).toBeLessThan(inIndex);
expect(inIndex).toBeLessThan(descriptionIndex);
expect(descriptionIndex).toBeLessThan(requiredIndex);
expect(requiredIndex).toBeLessThan(deprecatedIndex);
expect(deprecatedIndex).toBeLessThan(allowEmptyValueIndex);
expect(allowEmptyValueIndex).toBeLessThan(styleIndex);
expect(styleIndex).toBeLessThan(explodeIndex);
expect(explodeIndex).toBeLessThan(allowReservedIndex);
expect(allowReservedIndex).toBeLessThan(schemaIndex);
expect(schemaIndex).toBeLessThan(exampleIndex);
expect(exampleIndex).toBeLessThan(examplesIndex);
});
});
describe('Security scheme key ordering', () => {
it('should sort security scheme keys correctly', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
components: {
securitySchemes: {
BearerAuth: {
bearerFormat: 'JWT',
description: 'Bearer token authentication',
flows: {
implicit: {
authorizationUrl: 'https://example.com/oauth/authorize',
scopes: { read: 'Read access' }
}
},
name: 'Authorization',
in: 'header',
scheme: 'bearer',
type: 'http',
openIdConnectUrl: 'https://example.com/.well-known/openid_configuration'
}
}
}
}
};
// @ts-ignore We are mocking things here so we don't need to pass a print function
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 });
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Check that security scheme keys appear in the correct order
const typeIndex = result.toString().indexOf('"type"');
const descriptionIndex = result.toString().indexOf('"description"');
const nameIndex = result.toString().indexOf('"name"');
const inIndex = result.toString().indexOf('"in"');
const schemeIndex = result.toString().indexOf('"scheme"');
const bearerFormatIndex = result.toString().indexOf('"bearerFormat"');
const flowsIndex = result.toString().indexOf('"flows"');
const openIdConnectUrlIndex = result.toString().indexOf('"openIdConnectUrl"');
expect(typeIndex).toBeLessThan(descriptionIndex);
expect(descriptionIndex).toBeLessThan(nameIndex);
expect(nameIndex).toBeLessThan(inIndex);
expect(inIndex).toBeLessThan(schemeIndex);
expect(schemeIndex).toBeLessThan(bearerFormatIndex);
expect(bearerFormatIndex).toBeLessThan(flowsIndex);
expect(flowsIndex).toBeLessThan(openIdConnectUrlIndex);
});
});
describe('Server key ordering', () => {
it('should sort server keys correctly', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
servers: [
{
variables: { environment: { default: 'production' } },
url: 'https://api.example.com/v1',
description: 'Production server'
}
]
}
};
// @ts-ignore We are mocking things here so we don't need to pass a print function
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 });
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Check that server keys appear in the correct order
// Find the server section specifically (first server in the array)
const serverStart = result.toString().indexOf('{', result.toString().indexOf('"servers": ['));
// Find the end of the server object
const serverEnd = result.toString().indexOf('}', result.toString().lastIndexOf('"variables"'));
const serverSection = result.toString().substring(serverStart, serverEnd + 1);
const urlIndex = serverSection.indexOf('"url"');
const descriptionIndex = serverSection.indexOf('"description"');
const variablesIndex = serverSection.indexOf('"variables"');
expect(urlIndex).toBeLessThan(descriptionIndex);
expect(descriptionIndex).toBeLessThan(variablesIndex);
});
});
describe('Tag key ordering', () => {
it('should sort tag keys correctly', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
tags: [
{
externalDocs: { url: 'https://example.com/docs' },
name: 'users',
description: 'User management operations'
}
]
}
};
// @ts-ignore We are mocking things here so we don't need to pass a print function
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 });
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Check that tag keys appear in the correct order
// Find the tag section specifically (first tag in the array)
const tagStart = result.toString().indexOf('{', result.toString().indexOf('"tags": ['));
// Find the end of the tag object
const tagEnd = result.toString().indexOf('}', result.toString().lastIndexOf('"externalDocs"'));
const tagSection = result.toString().substring(tagStart, tagEnd + 1);
const nameIndex = tagSection.indexOf('"name"');
const descriptionIndex = tagSection.indexOf('"description"');
const externalDocsIndex = tagSection.indexOf('"externalDocs"');
expect(nameIndex).toBeLessThan(descriptionIndex);
expect(descriptionIndex).toBeLessThan(externalDocsIndex);
});
});
describe('External docs key ordering', () => {
it('should sort external docs keys correctly', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
externalDocs: {
url: 'https://example.com/docs',
description: 'External documentation'
}
}
};
// @ts-ignore We are mocking things here so we don't need to pass a print function
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 });
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Check that external docs keys appear in the correct order
const descriptionIndex = result.toString().indexOf('"description"');
const urlIndex = result.toString().indexOf('"url"');
expect(descriptionIndex).toBeLessThan(urlIndex);
});
});
describe('Path sorting', () => {
it('should sort paths by specificity', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/users/{id}/posts/{postId}': { get: {} },
'/users/{id}': { get: {} },
'/users': { get: {} },
'/users/{id}/posts': { get: {} }
}
}
};
// @ts-ignore We are mocking things here so we don't need to pass a print function
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 });
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Check that paths are sorted by specificity (fewer parameters first)
const usersIndex = result.toString().indexOf('"/users"');
const usersIdIndex = result.toString().indexOf('"/users/{id}"');
const usersIdPostsIndex = result.toString().indexOf('"/users/{id}/posts"');
const usersIdPostsPostIdIndex = result.toString().indexOf('"/users/{id}/posts/{postId}"');
expect(usersIndex).toBeLessThan(usersIdIndex);
expect(usersIdIndex).toBeLessThan(usersIdPostsIndex);
expect(usersIdPostsIndex).toBeLessThan(usersIdPostsPostIdIndex);
});
});
describe('Response code sorting', () => {
it('should sort response codes numerically', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/test': {
get: {
responses: {
'500': { description: 'Internal Server Error' },
'200': { description: 'OK' },
'404': { description: 'Not Found' },
'400': { description: 'Bad Request' },
'default': { description: 'Default response' }
}
}
}
}
}
};
// @ts-ignore We are mocking things here so we don't need to pass a print function
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 });
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Check that response codes are sorted numerically
const code200Index = result.toString().indexOf('"200"');
const code400Index = result.toString().indexOf('"400"');
const code404Index = result.toString().indexOf('"404"');
const code500Index = result.toString().indexOf('"500"');
const defaultIndex = result.toString().indexOf('"default"');
expect(code200Index).toBeLessThan(code400Index);
expect(code400Index).toBeLessThan(code404Index);
expect(code404Index).toBeLessThan(code500Index);
expect(code500Index).toBeLessThan(defaultIndex);
});
});
});

105
test/options.test.ts Normal file
View File

@@ -0,0 +1,105 @@
import { describe, it, expect } from 'bun:test';
import plugin from '../src/index';
describe('Plugin Options', () => {
it('should use custom tabWidth for JSON formatting', () => {
const testData = {
openapi: '3.0.0',
info: {
title: 'Test',
version: '1.0.0'
},
paths: {}
};
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const result = jsonPrinter?.print({ getValue: () => ({ type: 'openapi-json', content: testData, originalText: '' }) }, { tabWidth: 4 }, () => '');
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Check that 4-space indentation is used
expect(result).toContain(' "openapi"');
expect(result).toContain(' "info"');
expect(result).toContain(' "title"');
expect(result).toContain(' "version"');
});
it('should use custom tabWidth for YAML formatting', () => {
const testData = {
openapi: '3.0.0',
info: {
title: 'Test',
version: '1.0.0'
},
paths: {}
};
const yamlPrinter = plugin.printers?.['openapi-yaml-ast'];
expect(yamlPrinter).toBeDefined();
const result = yamlPrinter?.print({ getValue: () => ({ type: 'openapi-yaml', content: testData, originalText: '' }) }, { tabWidth: 4 }, () => '');
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Check that 4-space indentation is used
expect(result).toContain(' title: Test');
expect(result).toContain(' version: 1.0.0');
});
it('should use default tabWidth when not specified', () => {
const testData = {
openapi: '3.0.0',
info: {
title: 'Test',
version: '1.0.0'
},
paths: {}
};
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const result = jsonPrinter?.print({ getValue: () => ({ type: 'openapi-json', content: testData, originalText: '' }) }, {}, () => '');
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Check that 2-space indentation is used (default)
expect(result).toContain(' "openapi"');
expect(result).toContain(' "info"');
expect(result).toContain(' "title"');
expect(result).toContain(' "version"');
});
it('should use custom printWidth for YAML formatting', () => {
const testData = {
openapi: '3.0.0',
info: {
title: 'This is a very long title that should be wrapped according to printWidth',
version: '1.0.0'
},
paths: {}
};
const yamlPrinter = plugin.printers?.['openapi-yaml-ast'];
expect(yamlPrinter).toBeDefined();
const result = yamlPrinter?.print({ getValue: () => ({ type: 'openapi-yaml', content: testData, originalText: '' }) }, { printWidth: 20 }, () => '');
expect(result).toBeDefined();
// The YAML should be formatted with the custom line width
expect(result).toBeDefined();
// Note: js-yaml doesn't always respect lineWidth for all content types,
// but we can verify the option is passed through
});
});

209
test/plugin.test.ts Normal file
View File

@@ -0,0 +1,209 @@
import { describe, it, expect } from 'bun:test';
import plugin from '../src/index';
import * as fs from 'fs';
import * as path from 'path';
describe('Prettier OpenAPI Plugin', () => {
it('should have correct plugin structure', () => {
expect(plugin).toBeDefined();
expect(plugin.languages).toBeDefined();
expect(plugin.parsers).toBeDefined();
expect(plugin.printers).toBeDefined();
});
it('should support OpenAPI JSON files', () => {
const jsonLanguage = plugin.languages?.find(lang => lang.name === 'openapi-json');
expect(jsonLanguage).toBeDefined();
expect(jsonLanguage?.extensions).toContain('.openapi.json');
expect(jsonLanguage?.extensions).toContain('.swagger.json');
});
it('should support OpenAPI YAML files', () => {
const yamlLanguage = plugin.languages?.find(lang => lang.name === 'openapi-yaml');
expect(yamlLanguage).toBeDefined();
expect(yamlLanguage?.extensions).toContain('.openapi.yaml');
expect(yamlLanguage?.extensions).toContain('.openapi.yml');
expect(yamlLanguage?.extensions).toContain('.swagger.yaml');
expect(yamlLanguage?.extensions).toContain('.swagger.yml');
});
it('should parse JSON correctly', () => {
const jsonParser = plugin.parsers?.['openapi-json-parser'];
expect(jsonParser).toBeDefined();
const testJson = '{"openapi": "3.0.0", "info": {"title": "Test API", "version": "1.0.0"}}';
// @ts-ignore We are mocking things here
const result = jsonParser?.parse(testJson, {});
expect(result).toBeDefined();
expect(result?.type).toBe('openapi-json');
expect(result?.content).toBeDefined();
expect(result?.content.openapi).toBe('3.0.0');
});
it('should parse YAML correctly', () => {
const yamlParser = plugin.parsers?.['openapi-yaml-parser'];
expect(yamlParser).toBeDefined();
const testYaml = 'openapi: 3.0.0\ninfo:\n title: Test API\n version: 1.0.0';
// @ts-ignore We are mocking things here
const result = yamlParser?.parse(testYaml, {});
expect(result).toBeDefined();
expect(result?.type).toBe('openapi-yaml');
expect(result?.content).toBeDefined();
expect(result?.content.openapi).toBe('3.0.0');
});
it('should format JSON with proper sorting', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
info: { title: 'Test', version: '1.0.0' },
openapi: '3.0.0',
paths: { '/test': { get: {} } }
}
};
// @ts-ignore We are mocking things here
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => '');
expect(result).toBeDefined();
expect(result).toContain('"openapi"');
expect(result).toContain('"info"');
expect(result).toContain('"paths"');
});
it('should format YAML with proper sorting', () => {
const yamlPrinter = plugin.printers?.['openapi-yaml-ast'];
expect(yamlPrinter).toBeDefined();
const testData = {
content: {
info: { title: 'Test', version: '1.0.0' },
openapi: '3.0.0',
paths: { '/test': { get: {} } }
}
};
// @ts-ignore We are mocking things here
const result = yamlPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => '');
expect(result).toBeDefined();
expect(result).toContain('openapi:');
expect(result).toContain('info:');
expect(result).toContain('paths:');
});
});
describe('Key Ordering Tests', () => {
describe('Top-level key ordering', () => {
it('should sort OpenAPI 3.0+ keys correctly', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
paths: { '/test': { get: {} } },
components: { schemas: {} },
info: { title: 'Test API', version: '1.0.0' },
openapi: '3.0.0',
security: [],
tags: [],
externalDocs: { url: 'https://example.com' }
}
};
// @ts-ignore We are mocking things here
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => '');
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Check that keys appear in the correct order
const openapiIndex = result.toString().indexOf('"openapi"');
const infoIndex = result.toString().indexOf('"info"');
const pathsIndex = result.toString().indexOf('"paths"');
const componentsIndex = result.toString().indexOf('"components"');
const securityIndex = result.toString().indexOf('"security"');
const tagsIndex = result.toString().indexOf('"tags"');
const externalDocsIndex = result.toString().indexOf('"externalDocs"');
expect(openapiIndex).toBeLessThan(infoIndex);
expect(infoIndex).toBeLessThan(pathsIndex);
expect(pathsIndex).toBeLessThan(componentsIndex);
expect(componentsIndex).toBeLessThan(securityIndex);
expect(securityIndex).toBeLessThan(tagsIndex);
expect(tagsIndex).toBeLessThan(externalDocsIndex);
});
it('should sort Swagger 2.0 keys correctly', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
paths: { '/test': { get: {} } },
definitions: { User: { type: 'object' } },
info: { title: 'Test API', version: '1.0.0' },
swagger: '2.0',
host: 'api.example.com',
basePath: '/v1',
schemes: ['https'],
consumes: ['application/json'],
produces: ['application/json'],
parameters: {},
responses: {},
securityDefinitions: {},
security: [],
tags: [],
externalDocs: { url: 'https://example.com' }
}
};
// @ts-ignore We are mocking things here
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => '');
expect(result).toBeDefined();
if (!result) {
throw new Error('Result is undefined');
}
// Check that keys appear in the correct order
const swaggerIndex = result.toString().indexOf('"swagger"');
const infoIndex = result.toString().indexOf('"info"');
const hostIndex = result.toString().indexOf('"host"');
const basePathIndex = result.toString().indexOf('"basePath"');
const schemesIndex = result.toString().indexOf('"schemes"');
const consumesIndex = result.toString().indexOf('"consumes"');
const producesIndex = result.toString().indexOf('"produces"');
const pathsIndex = result.toString().indexOf('"paths"');
const definitionsIndex = result.toString().indexOf('"definitions"');
const parametersIndex = result.toString().indexOf('"parameters"');
const responsesIndex = result.toString().indexOf('"responses"');
const securityDefinitionsIndex = result.toString().indexOf('"securityDefinitions"');
const securityIndex = result.toString().indexOf('"security"');
const tagsIndex = result.toString().indexOf('"tags"');
const externalDocsIndex = result.toString().indexOf('"externalDocs"');
expect(swaggerIndex).toBeLessThan(infoIndex);
expect(infoIndex).toBeLessThan(hostIndex);
expect(hostIndex).toBeLessThan(basePathIndex);
expect(basePathIndex).toBeLessThan(schemesIndex);
expect(schemesIndex).toBeLessThan(consumesIndex);
expect(consumesIndex).toBeLessThan(producesIndex);
expect(producesIndex).toBeLessThan(pathsIndex);
expect(pathsIndex).toBeLessThan(definitionsIndex);
expect(definitionsIndex).toBeLessThan(parametersIndex);
expect(parametersIndex).toBeLessThan(responsesIndex);
expect(responsesIndex).toBeLessThan(securityDefinitionsIndex);
expect(securityDefinitionsIndex).toBeLessThan(securityIndex);
expect(securityIndex).toBeLessThan(tagsIndex);
expect(tagsIndex).toBeLessThan(externalDocsIndex);
});
});
});

View File

@@ -0,0 +1,162 @@
import { describe, it, expect } from 'bun:test';
import plugin from '../src/index';
describe('Simple Key Ordering Tests', () => {
it('should sort top-level OpenAPI keys correctly', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
paths: { '/test': { get: {} } },
components: { schemas: {} },
info: { title: 'Test API', version: '1.0.0' },
openapi: '3.0.0',
security: [],
tags: [],
externalDocs: { url: 'https://example.com' }
}
};
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => '');
expect(result).toBeDefined();
// Check that keys appear in the correct order
const openapiIndex = result.indexOf('"openapi"');
const infoIndex = result.indexOf('"info"');
const pathsIndex = result.indexOf('"paths"');
const componentsIndex = result.indexOf('"components"');
const securityIndex = result.indexOf('"security"');
const tagsIndex = result.indexOf('"tags"');
const externalDocsIndex = result.indexOf('"externalDocs"');
expect(openapiIndex).toBeLessThan(infoIndex);
expect(infoIndex).toBeLessThan(pathsIndex);
expect(pathsIndex).toBeLessThan(componentsIndex);
expect(componentsIndex).toBeLessThan(securityIndex);
expect(securityIndex).toBeLessThan(tagsIndex);
expect(tagsIndex).toBeLessThan(externalDocsIndex);
});
it('should sort operation keys correctly', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/test': {
get: {
responses: { '200': { description: 'OK' } },
operationId: 'getTest',
summary: 'Get test data',
description: 'Retrieve test data',
tags: ['test'],
parameters: [],
requestBody: { content: { 'application/json': { schema: { type: 'object' } } } },
callbacks: {},
deprecated: false,
security: [],
servers: []
}
}
}
}
};
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => '');
expect(result).toBeDefined();
// Check that operation keys appear in the correct order
const tagsIndex = result.indexOf('"tags"');
const summaryIndex = result.indexOf('"summary"');
const descriptionIndex = result.indexOf('"description"');
const operationIdIndex = result.indexOf('"operationId"');
const parametersIndex = result.indexOf('"parameters"');
const requestBodyIndex = result.indexOf('"requestBody"');
const responsesIndex = result.indexOf('"responses"');
const callbacksIndex = result.indexOf('"callbacks"');
const deprecatedIndex = result.indexOf('"deprecated"');
const securityIndex = result.indexOf('"security"');
const serversIndex = result.indexOf('"servers"');
expect(tagsIndex).toBeLessThan(summaryIndex);
expect(summaryIndex).toBeLessThan(descriptionIndex);
expect(descriptionIndex).toBeLessThan(operationIdIndex);
expect(operationIdIndex).toBeLessThan(parametersIndex);
expect(parametersIndex).toBeLessThan(requestBodyIndex);
expect(requestBodyIndex).toBeLessThan(responsesIndex);
expect(responsesIndex).toBeLessThan(callbacksIndex);
expect(callbacksIndex).toBeLessThan(deprecatedIndex);
expect(deprecatedIndex).toBeLessThan(securityIndex);
expect(securityIndex).toBeLessThan(serversIndex);
});
it('should sort info keys correctly', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
openapi: '3.0.0',
info: {
version: '1.0.0',
termsOfService: 'https://example.com/terms',
title: 'Test API',
description: 'A test API',
contact: { name: 'API Team', email: 'api@example.com' },
license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT' }
}
}
};
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => '');
expect(result).toBeDefined();
// Check that info keys appear in the correct order
const titleIndex = result.indexOf('"title"');
const descriptionIndex = result.indexOf('"description"');
const versionIndex = result.indexOf('"version"');
const termsOfServiceIndex = result.indexOf('"termsOfService"');
const contactIndex = result.indexOf('"contact"');
const licenseIndex = result.indexOf('"license"');
expect(titleIndex).toBeLessThan(descriptionIndex);
expect(descriptionIndex).toBeLessThan(versionIndex);
expect(versionIndex).toBeLessThan(termsOfServiceIndex);
expect(termsOfServiceIndex).toBeLessThan(contactIndex);
expect(contactIndex).toBeLessThan(licenseIndex);
});
it('should handle custom extensions correctly', () => {
const jsonPrinter = plugin.printers?.['openapi-json-ast'];
expect(jsonPrinter).toBeDefined();
const testData = {
content: {
'x-custom-field': 'value',
'openapi': '3.0.0',
'info': { 'title': 'Test API', 'version': '1.0.0' },
'paths': {},
'x-metadata': { 'custom': 'data' }
}
};
const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => '');
expect(result).toBeDefined();
// Custom extensions should come after standard keys
const openapiIndex = result.indexOf('"openapi"');
const infoIndex = result.indexOf('"info"');
const pathsIndex = result.indexOf('"paths"');
const xCustomFieldIndex = result.indexOf('"x-custom-field"');
const xMetadataIndex = result.indexOf('"x-metadata"');
expect(openapiIndex).toBeLessThan(infoIndex);
expect(infoIndex).toBeLessThan(pathsIndex);
expect(pathsIndex).toBeLessThan(xCustomFieldIndex);
expect(xCustomFieldIndex).toBeLessThan(xMetadataIndex);
});
});

39
test/vendor.test.ts Normal file
View File

@@ -0,0 +1,39 @@
import { describe, it, expect } from 'bun:test';
import { getVendorExtensions } from '../src/extensions';
describe('Vendor Extension System', () => {
it('should load vendor extensions from TS files', () => {
const vendorExtensions = getVendorExtensions();
// Should have loaded Speakeasy and Redoc extensions
expect(vendorExtensions).toBeDefined();
expect(typeof vendorExtensions).toBe('object');
// Check if extensions were loaded
expect(vendorExtensions['top-level']).toBeDefined();
expect(vendorExtensions['top-level']['x-speakeasy-sdk']).toBe(2); // before('info') = position 2
expect(vendorExtensions['top-level']['x-speakeasy-auth']).toBe(11); // after('paths') = position 11
});
it('should handle vendor extensions gracefully', () => {
// This test ensures the system doesn't crash when loading extensions
const vendorExtensions = getVendorExtensions();
expect(vendorExtensions).toBeDefined();
expect(typeof vendorExtensions).toBe('object');
});
it('should have proper extension structure', () => {
const vendorExtensions = getVendorExtensions();
// Check that extensions have the right structure
expect(vendorExtensions['top-level']).toBeDefined();
expect(vendorExtensions['info']).toBeDefined();
expect(vendorExtensions['operation']).toBeDefined();
expect(vendorExtensions['schema']).toBeDefined();
// Check specific extensions
expect(vendorExtensions['top-level']['x-speakeasy-sdk']).toBe(2); // before('info') = position 2
expect(vendorExtensions['top-level']['x-speakeasy-auth']).toBe(11); // after('paths') = position 11
});
});

25
tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": false,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": false,
"noUnusedParameters": false
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}