openapi-remote-codegen 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -0
- package/dist/config.d.ts +46 -0
- package/dist/config.js +45 -0
- package/dist/generators/api-client.d.ts +6 -0
- package/dist/generators/api-client.js +72 -0
- package/dist/generators/remote-functions.d.ts +3 -0
- package/dist/generators/remote-functions.js +439 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +88 -0
- package/dist/parser.d.ts +3 -0
- package/dist/parser.js +236 -0
- package/dist/public.d.ts +6 -0
- package/dist/public.js +5 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.js +1 -0
- package/dist/utils/client-mapping.d.ts +6 -0
- package/dist/utils/client-mapping.js +9 -0
- package/dist/utils/naming.d.ts +26 -0
- package/dist/utils/naming.js +89 -0
- package/package.json +37 -0
- package/src/__tests__/client-mapping.test.ts +38 -0
- package/src/__tests__/config.test.ts +59 -0
- package/src/__tests__/naming.test.ts +68 -0
- package/src/__tests__/parser.test.ts +576 -0
- package/src/__tests__/remote-functions.test.ts +315 -0
- package/src/config.ts +95 -0
- package/src/generators/api-client.ts +82 -0
- package/src/generators/remote-functions.ts +521 -0
- package/src/index.ts +105 -0
- package/src/parser.ts +303 -0
- package/src/public.ts +7 -0
- package/src/types.ts +48 -0
- package/src/utils/client-mapping.ts +9 -0
- package/src/utils/naming.ts +99 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# openapi-remote-codegen
|
|
2
|
+
|
|
3
|
+
TypeScript CLI and library that generates type-safe SvelteKit remote functions from OpenAPI specs annotated with `x-remote-*` extensions.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -D openapi-remote-codegen
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Zero-Config Usage
|
|
12
|
+
|
|
13
|
+
If your OpenAPI spec lives at `./openapi.json`, just run:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx openapi-remote-codegen
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The generator reads the spec, finds operations annotated with `x-remote-type`, and writes generated files to `./src/lib/api/generated/`.
|
|
20
|
+
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
Create a `remote-codegen.config.ts` (or `.js` / `.mjs`) in your project root:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { defineConfig } from 'openapi-remote-codegen/config';
|
|
27
|
+
|
|
28
|
+
export default defineConfig({
|
|
29
|
+
openApiPath: './openapi.json',
|
|
30
|
+
outputDir: './src/lib',
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Config Reference
|
|
35
|
+
|
|
36
|
+
| Option | Type | Default | Description |
|
|
37
|
+
|--------|------|---------|-------------|
|
|
38
|
+
| `openApiPath` | `string` | `'./openapi.json'` | Path to the OpenAPI spec JSON file |
|
|
39
|
+
| `outputDir` | `string` | `'./src/lib'` | Base output directory for generated files |
|
|
40
|
+
| `remoteFunctionsOutput` | `string` | `'api/generated'` | Subdirectory within `outputDir` for remote function files |
|
|
41
|
+
| `apiClientOutput` | `string` | `'api/api-client.generated.ts'` | Path within `outputDir` for the ApiClient wrapper |
|
|
42
|
+
| `clientAccess` | `string` | `'getRequestEvent().locals.apiClient'` | Expression to access the API client in generated functions |
|
|
43
|
+
| `nswagClientPath` | `string` | `'./generated/api-client'` | Path to the NSwag-generated client module |
|
|
44
|
+
| `imports` | `ImportPaths` | See below | Module paths used in generated `import` statements |
|
|
45
|
+
| `errorHandling` | `ErrorHandling` | See below | Templates for generated `catch` blocks |
|
|
46
|
+
|
|
47
|
+
### `imports`
|
|
48
|
+
|
|
49
|
+
| Key | Default | Description |
|
|
50
|
+
|-----|---------|-------------|
|
|
51
|
+
| `server` | `'$app/server'` | Module providing `query`, `command`, `form`, `getRequestEvent` |
|
|
52
|
+
| `kit` | `'@sveltejs/kit'` | Module providing `error`, `redirect` |
|
|
53
|
+
| `schemas` | `'$lib/api/generated/schemas'` | Module providing Zod schemas |
|
|
54
|
+
| `apiTypes` | `'$api'` | Module providing API types and enums |
|
|
55
|
+
| `zod` | `'zod'` | Zod module |
|
|
56
|
+
|
|
57
|
+
### `errorHandling`
|
|
58
|
+
|
|
59
|
+
| Key | Default | Description |
|
|
60
|
+
|-----|---------|-------------|
|
|
61
|
+
| `on401` | Redirect to `/auth/login` | Code to execute on 401 (has access to `url`) |
|
|
62
|
+
| `on403` | `error(403, 'Forbidden')` | Code to execute on 403 |
|
|
63
|
+
| `on500` | `error(500, 'Failed to ...')` | Function taking a human-readable name, returns code for 500 |
|
|
64
|
+
|
|
65
|
+
## CLI Flags
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npx openapi-remote-codegen [options]
|
|
69
|
+
|
|
70
|
+
Options:
|
|
71
|
+
--config <path> Path to config file (skips auto-discovery)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Config File Discovery
|
|
75
|
+
|
|
76
|
+
When `--config` is not provided, the CLI looks for these files in the current directory (first match wins):
|
|
77
|
+
|
|
78
|
+
1. `remote-codegen.config.ts`
|
|
79
|
+
2. `remote-codegen.config.js`
|
|
80
|
+
3. `remote-codegen.config.mjs`
|
|
81
|
+
|
|
82
|
+
If none are found, all defaults apply.
|
|
83
|
+
|
|
84
|
+
## Generated Output
|
|
85
|
+
|
|
86
|
+
The generator produces one file per OpenAPI tag (e.g., `foods.generated.remote.ts`) plus an `index.ts` barrel export and an `api-client.generated.ts` wrapper. Stale generated files are automatically cleaned up.
|
|
87
|
+
|
|
88
|
+
### Example: Query
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// foods.generated.remote.ts
|
|
92
|
+
import { query, getRequestEvent } from '$app/server';
|
|
93
|
+
import { error, redirect } from '@sveltejs/kit';
|
|
94
|
+
|
|
95
|
+
export const getFavorites = query(async () => {
|
|
96
|
+
try {
|
|
97
|
+
const apiClient = getRequestEvent().locals.apiClient;
|
|
98
|
+
return await apiClient.foodsV4.getFavorites();
|
|
99
|
+
} catch (err) {
|
|
100
|
+
const status = (err as any)?.status;
|
|
101
|
+
if (status === 401) { /* redirect to login */ }
|
|
102
|
+
if (status === 403) throw error(403, 'Forbidden');
|
|
103
|
+
throw error(500, 'Failed to get favorites');
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Example: Command with Invalidation
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
export const createFood = command(async (data: CreateFoodDto) => {
|
|
112
|
+
try {
|
|
113
|
+
const apiClient = getRequestEvent().locals.apiClient;
|
|
114
|
+
return await apiClient.foodsV4.createFood(data);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
// ... error handling
|
|
117
|
+
}
|
|
118
|
+
}, { invalidates: [getFavorites] });
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Programmatic API
|
|
122
|
+
|
|
123
|
+
The package also exports its core pipeline for integration into build tools or custom workflows:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import {
|
|
127
|
+
parseOpenApiSpec,
|
|
128
|
+
generateRemoteFunctions,
|
|
129
|
+
generateApiClient,
|
|
130
|
+
resolveConfig,
|
|
131
|
+
defineConfig,
|
|
132
|
+
} from 'openapi-remote-codegen';
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { readFileSync } from 'fs';
|
|
137
|
+
import { resolveConfig, parseOpenApiSpec, generateRemoteFunctions } from 'openapi-remote-codegen';
|
|
138
|
+
|
|
139
|
+
const config = resolveConfig({ openApiPath: './my-spec.json' });
|
|
140
|
+
const spec = JSON.parse(readFileSync(config.openApiPath, 'utf-8'));
|
|
141
|
+
const parsed = parseOpenApiSpec(spec);
|
|
142
|
+
const files = generateRemoteFunctions(parsed, config);
|
|
143
|
+
|
|
144
|
+
for (const [fileName, content] of files) {
|
|
145
|
+
console.log(fileName, content.length);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Exported Types
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import type {
|
|
153
|
+
GeneratorConfig,
|
|
154
|
+
UserConfig,
|
|
155
|
+
ImportPaths,
|
|
156
|
+
ErrorHandling,
|
|
157
|
+
ParsedSpec,
|
|
158
|
+
OperationInfo,
|
|
159
|
+
ParameterInfo,
|
|
160
|
+
RemoteType,
|
|
161
|
+
} from 'openapi-remote-codegen';
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## How It Works
|
|
165
|
+
|
|
166
|
+
1. **Parse** -- The OpenAPI JSON spec is read and scanned for operations containing `x-remote-type` extension data.
|
|
167
|
+
2. **Classify** -- Each annotated operation is classified as a `query`, `command`, or `form` and its parameters, request body schema, and response type are extracted.
|
|
168
|
+
3. **Generate** -- Operations are grouped by tag. For each tag, a `.generated.remote.ts` file is emitted with typed wrapper functions. An `ApiClient` wrapper and barrel `index.ts` are also generated.
|
|
169
|
+
4. **Write** -- Files are written to the configured output directory. Previously generated files that no longer correspond to a tag are removed.
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface ImportPaths {
|
|
2
|
+
/** Module providing query, command, form, getRequestEvent. Default: '$app/server' */
|
|
3
|
+
server: string;
|
|
4
|
+
/** Module providing error, redirect. Default: '@sveltejs/kit' */
|
|
5
|
+
kit: string;
|
|
6
|
+
/** Module providing Zod schemas. Default: '$lib/api/generated/schemas' */
|
|
7
|
+
schemas: string;
|
|
8
|
+
/** Module providing API types/enums. Default: '$api' */
|
|
9
|
+
apiTypes: string;
|
|
10
|
+
/** Zod module. Default: 'zod' */
|
|
11
|
+
zod: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ErrorHandling {
|
|
14
|
+
/** Code to execute on 401. Has access to `url` (current URL). Default: redirect to /auth/login */
|
|
15
|
+
on401: string;
|
|
16
|
+
/** Code to execute on 403. Default: error(403, 'Forbidden') */
|
|
17
|
+
on403: string;
|
|
18
|
+
/** Function that takes a human-readable function name and returns code for 500. */
|
|
19
|
+
on500: (functionName: string) => string;
|
|
20
|
+
}
|
|
21
|
+
export interface GeneratorConfig {
|
|
22
|
+
/** Path to the OpenAPI spec JSON file. Default: './openapi.json' */
|
|
23
|
+
openApiPath: string;
|
|
24
|
+
/** Base output directory. Default: './src/lib' */
|
|
25
|
+
outputDir: string;
|
|
26
|
+
/** Subdirectory within outputDir for remote function files. Default: 'api/generated' */
|
|
27
|
+
remoteFunctionsOutput: string;
|
|
28
|
+
/** Path within outputDir for the ApiClient wrapper. Default: 'api/api-client.generated.ts' */
|
|
29
|
+
apiClientOutput: string;
|
|
30
|
+
/** Import paths used in generated code. */
|
|
31
|
+
imports: ImportPaths;
|
|
32
|
+
/** Expression to access the API client in generated functions. Default: 'getRequestEvent().locals.apiClient' */
|
|
33
|
+
clientAccess: string;
|
|
34
|
+
/** Error handling templates for generated catch blocks. */
|
|
35
|
+
errorHandling: ErrorHandling;
|
|
36
|
+
/** Path to the NSwag-generated client module (used in ApiClient imports). Default: './generated/api-client' */
|
|
37
|
+
nswagClientPath: string;
|
|
38
|
+
}
|
|
39
|
+
export type UserConfig = Partial<Omit<GeneratorConfig, 'imports' | 'errorHandling'>> & {
|
|
40
|
+
imports?: Partial<ImportPaths>;
|
|
41
|
+
errorHandling?: Partial<ErrorHandling>;
|
|
42
|
+
};
|
|
43
|
+
/** Type-helper for config files. Returns the input as-is. */
|
|
44
|
+
export declare function defineConfig(config: UserConfig): UserConfig;
|
|
45
|
+
/** Merge user config with defaults to produce a fully resolved config. */
|
|
46
|
+
export declare function resolveConfig(user: UserConfig): GeneratorConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const DEFAULT_IMPORTS = {
|
|
2
|
+
server: '$app/server',
|
|
3
|
+
kit: '@sveltejs/kit',
|
|
4
|
+
schemas: '$lib/api/generated/schemas',
|
|
5
|
+
apiTypes: '$api',
|
|
6
|
+
zod: 'zod',
|
|
7
|
+
};
|
|
8
|
+
const DEFAULT_ERROR_HANDLING = {
|
|
9
|
+
on401: 'const { url } = getRequestEvent(); throw redirect(302, `/auth/login?returnUrl=${encodeURIComponent(url.pathname + url.search)}`)',
|
|
10
|
+
on403: "throw error(403, 'Forbidden')",
|
|
11
|
+
on500: (functionName) => `throw error(500, 'Failed to ${functionName}')`,
|
|
12
|
+
};
|
|
13
|
+
const DEFAULTS = {
|
|
14
|
+
openApiPath: './openapi.json',
|
|
15
|
+
outputDir: './src/lib',
|
|
16
|
+
remoteFunctionsOutput: 'api/generated',
|
|
17
|
+
apiClientOutput: 'api/api-client.generated.ts',
|
|
18
|
+
imports: DEFAULT_IMPORTS,
|
|
19
|
+
clientAccess: 'getRequestEvent().locals.apiClient',
|
|
20
|
+
errorHandling: DEFAULT_ERROR_HANDLING,
|
|
21
|
+
nswagClientPath: './generated/api-client',
|
|
22
|
+
};
|
|
23
|
+
/** Type-helper for config files. Returns the input as-is. */
|
|
24
|
+
export function defineConfig(config) {
|
|
25
|
+
return config;
|
|
26
|
+
}
|
|
27
|
+
/** Merge user config with defaults to produce a fully resolved config. */
|
|
28
|
+
export function resolveConfig(user) {
|
|
29
|
+
return {
|
|
30
|
+
openApiPath: user.openApiPath ?? DEFAULTS.openApiPath,
|
|
31
|
+
outputDir: user.outputDir ?? DEFAULTS.outputDir,
|
|
32
|
+
remoteFunctionsOutput: user.remoteFunctionsOutput ?? DEFAULTS.remoteFunctionsOutput,
|
|
33
|
+
apiClientOutput: user.apiClientOutput ?? DEFAULTS.apiClientOutput,
|
|
34
|
+
imports: {
|
|
35
|
+
...DEFAULTS.imports,
|
|
36
|
+
...user.imports,
|
|
37
|
+
},
|
|
38
|
+
clientAccess: user.clientAccess ?? DEFAULTS.clientAccess,
|
|
39
|
+
errorHandling: {
|
|
40
|
+
...DEFAULTS.errorHandling,
|
|
41
|
+
...user.errorHandling,
|
|
42
|
+
},
|
|
43
|
+
nswagClientPath: user.nswagClientPath ?? DEFAULTS.nswagClientPath,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { getClientPropertyName } from '../utils/client-mapping.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generate the ApiClient wrapper class.
|
|
4
|
+
*/
|
|
5
|
+
export function generateApiClient(spec, config) {
|
|
6
|
+
const tagInfo = new Map();
|
|
7
|
+
for (const pathItem of Object.values(spec.paths ?? {})) {
|
|
8
|
+
if (!pathItem)
|
|
9
|
+
continue;
|
|
10
|
+
for (const method of ['get', 'post', 'put', 'patch', 'delete']) {
|
|
11
|
+
const operation = pathItem[method];
|
|
12
|
+
if (operation?.tags?.[0] && operation.operationId) {
|
|
13
|
+
const tag = operation.tags[0];
|
|
14
|
+
if (!tagInfo.has(tag)) {
|
|
15
|
+
const prefix = operation.operationId.split('_')[0];
|
|
16
|
+
tagInfo.set(tag, {
|
|
17
|
+
prefix,
|
|
18
|
+
clientProperty: operation['x-client-property'],
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Deduplicate by className (NSwag merges tags like "Treatments" and "V4 Treatments"
|
|
25
|
+
// into a single TreatmentsClient)
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
const clients = Array.from(tagInfo.entries())
|
|
28
|
+
.map(([tag, info]) => ({
|
|
29
|
+
className: `${info.prefix}Client`,
|
|
30
|
+
propertyName: info.clientProperty ?? getClientPropertyName(tag),
|
|
31
|
+
}))
|
|
32
|
+
.filter(c => {
|
|
33
|
+
if (seen.has(c.className))
|
|
34
|
+
return false;
|
|
35
|
+
seen.add(c.className);
|
|
36
|
+
return true;
|
|
37
|
+
})
|
|
38
|
+
.sort((a, b) => a.propertyName.localeCompare(b.propertyName));
|
|
39
|
+
const imports = clients.map(c => c.className).join(',\n ');
|
|
40
|
+
const properties = clients.map(c => ` public readonly ${c.propertyName}: ${c.className};`).join('\n');
|
|
41
|
+
const initializers = clients.map(c => ` this.${c.propertyName} = new ${c.className}(apiBaseUrl, http);`).join('\n');
|
|
42
|
+
return `// AUTO-GENERATED - DO NOT EDIT
|
|
43
|
+
// Generated by openapi-remote-codegen
|
|
44
|
+
// Source: openapi.json
|
|
45
|
+
//
|
|
46
|
+
import {
|
|
47
|
+
${imports}
|
|
48
|
+
} from "${config.nswagClientPath}";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* API client wrapper.
|
|
52
|
+
* Provides typed access to all backend endpoints.
|
|
53
|
+
*/
|
|
54
|
+
export class ApiClient {
|
|
55
|
+
public readonly baseUrl: string;
|
|
56
|
+
${properties}
|
|
57
|
+
|
|
58
|
+
constructor(
|
|
59
|
+
baseUrl: string,
|
|
60
|
+
http?: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> }
|
|
61
|
+
) {
|
|
62
|
+
const apiBaseUrl = baseUrl;
|
|
63
|
+
this.baseUrl = apiBaseUrl;
|
|
64
|
+
|
|
65
|
+
${initializers}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Export the generated client types for use in components
|
|
70
|
+
export * from "${config.nswagClientPath}";
|
|
71
|
+
`;
|
|
72
|
+
}
|