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 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
@@ -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,6 @@
1
+ import type { OpenAPIV3 } from 'openapi-types';
2
+ import type { GeneratorConfig } from '../config.js';
3
+ /**
4
+ * Generate the ApiClient wrapper class.
5
+ */
6
+ export declare function generateApiClient(spec: OpenAPIV3.Document, config: GeneratorConfig): string;
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import type { GeneratorConfig } from '../config.js';
2
+ import type { ParsedSpec } from '../types.js';
3
+ export declare function generateRemoteFunctions(parsed: ParsedSpec, config: GeneratorConfig): Map<string, string>;