@shipworthy/openapi-generator 0.0.0-stage → 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shipworthy AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,297 @@
1
- # Temporary Holding Version
1
+ # @shipworthy/openapi-generator
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Load, bundle, and parse an OpenAPI 3.x document, then run one generator plugin on the result. `generate` takes a document (a file path, a URL, file URL, JSON/YAML text, `Buffer`, Node `Readable`, or plain object). It resolves external `$ref`s, upgrades OpenAPI 3.0 to 3.1, decodes the document into the `ParsedService` model from `@shipworthy/openapi-transformer`, loads a plugin by module specifier and export name, and returns whatever the plugin's `generate` returns. Use this package to drive an existing `@shipworthy/*` generator from code, such as `@shipworthy/openapi-typescript` (export `typescript`) or `@shipworthy/openapi-zod` (export `zod`), or to write your own generator against the `Generator` interface. The `openapi` CLI (`@shipworthy/openapi-cli`) is built on this package. If you only want generated files on disk, use the CLI.
4
+
5
+ The API is built on [Effect](https://effect.website) 4. `generate` returns an `Effect`, not a `Promise`.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ bun add @shipworthy/openapi-generator effect @effect/platform-node
11
+ ```
12
+
13
+ Requirements:
14
+
15
+ - **`effect` `^4.0.0`.** This is a peer dependency, so install it yourself. Every package then shares one copy of `effect`, which you use to build, run, and provide services to the returned `Effect`.
16
+ - **`@effect/platform-node` `^4.0.0`**, only because the examples below import `NodeServices` from it. `@shipworthy/openapi-parser` already depends on it, but your own code needs it in your `package.json` to import it.
17
+ - **A platform layer.** `generate` requires the `FileSystem`, `Path`, and `HttpClient` services. Provide them with `NodeServices.layer` from `@effect/platform-node` or `BunServices.layer` from `@effect/platform-bun`, plus `FetchHttpClient.layer` from `effect/http`. `HttpClient` is required even when the source is inline, because it's part of the effect's type.
18
+ - **ESM only.** The package is `"type": "module"` and ships compiled ESM JavaScript with `.d.ts` declarations. It has no CommonJS build, and it needs no TypeScript loader or bundler.
19
+ - **Node.js `>=22.19.0` or Bun.** The loader uses Node APIs (`Buffer`, `node:stream`) through its `@shipworthy/openapi-parser` dependency. It doesn't run in browsers.
20
+ - **The plugin package.** Each plugin is loaded with dynamic `import()` when `generate` runs. Install it too, for example `@shipworthy/openapi-typescript`.
21
+
22
+ ## Quick start
23
+
24
+ Write a generator plugin:
25
+
26
+ ```ts
27
+ // operation-ids.ts
28
+ import type { Generator } from "@shipworthy/openapi-generator";
29
+
30
+ export const operationIds: Generator<string[]> = {
31
+ id: "operation-ids",
32
+ generate: (model) => model.operations.map((op) => `${op.method.toUpperCase()} ${op.path}`),
33
+ };
34
+ ```
35
+
36
+ Then run it on an OpenAPI document:
37
+
38
+ ```yaml
39
+ # openapi.yaml
40
+ openapi: 3.0.3
41
+ info: { title: Pets, version: "1.0.0" }
42
+ paths:
43
+ /pets:
44
+ get:
45
+ operationId: listPets
46
+ responses:
47
+ "200": { description: OK }
48
+ ```
49
+
50
+ ```ts
51
+ // main.ts
52
+ import { NodeServices } from "@effect/platform-node";
53
+ import { generate } from "@shipworthy/openapi-generator";
54
+ import { Effect, Layer } from "effect";
55
+ import { FetchHttpClient } from "effect/http";
56
+
57
+ const program = generate({
58
+ // Pass a local plugin as an absolute file URL.
59
+ specifier: new URL("./operation-ids.ts", import.meta.url).href,
60
+ exportName: "operationIds",
61
+ // A path string is resolved against process.cwd() (see "Sources").
62
+ source: "./openapi.yaml",
63
+ }).pipe(
64
+ Effect.tap((output) => Effect.log(output)), // ["GET /pets"]
65
+ Effect.catchTags({
66
+ ResolveError: (error) => Effect.logError(`Invalid document: ${error.message}`),
67
+ TransformError: (error) => Effect.logError(`Generation failed: ${error.message}`),
68
+ MissingPlugin: (error) => Effect.logError(error.message),
69
+ InvalidPlugin: (error) => Effect.logError(error.message),
70
+ }),
71
+ Effect.provide(Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer)),
72
+ );
73
+
74
+ Effect.runPromise(program);
75
+ ```
76
+
77
+ On Bun, swap `NodeServices` for `BunServices` from `@effect/platform-bun`.
78
+
79
+ ## API
80
+
81
+ The package has a single entry point, `@shipworthy/openapi-generator`. It exports:
82
+
83
+ | Export | Kind | Purpose |
84
+ | ------------------------------------------------------------------ | ------------------------------ | ------------------------------------------------------------------------ |
85
+ | `generate` | function returning an `Effect` | Runs the full pipeline: load, bundle, decode, import the plugin, render. |
86
+ | `GenerateOptions` | type | Options for `generate`. |
87
+ | `GenerateError` | type | Union of every error `generate` can fail with. |
88
+ | `Generator<A, E>` | type | The interface a plugin export implements. |
89
+ | `importPlugin` | function returning an `Effect` | Loads and checks a plugin without running it. |
90
+ | `ModuleLoader` | `Context.Reference` | The service that imports plugin modules. You can replace it. |
91
+ | `ResolveError`, `TransformError`, `MissingPlugin`, `InvalidPlugin` | tagged error classes | Typed failures. |
92
+
93
+ ### `generate(options)`
94
+
95
+ ```ts
96
+ function generate(
97
+ options: GenerateOptions,
98
+ ): Effect.Effect<unknown, GenerateError, FileSystem.FileSystem | Path.Path | HttpClient.HttpClient>;
99
+
100
+ interface GenerateOptions {
101
+ readonly specifier: string;
102
+ readonly exportName: string;
103
+ readonly source: unknown;
104
+ readonly cwd?: URL | string;
105
+ readonly pluginOptions?: Readonly<Record<string, unknown>>;
106
+ }
107
+ ```
108
+
109
+ | Option | Required | Default | Description |
110
+ | --------------- | -------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
111
+ | `specifier` | yes | — | Module to import the plugin from: a package name such as `"@shipworthy/openapi-typescript"`, or an absolute `file://` URL string. Passed to `ModuleLoader.load`. |
112
+ | `exportName` | yes | — | Named export of that module that holds the `Generator`, for example `"typescript"` or `"zod"`. |
113
+ | `source` | yes | — | The OpenAPI document. See [Sources](#sources). |
114
+ | `cwd` | no | `process.cwd()` | Base location for a relative path `source` and for relative `$ref`s when `source` is inline (text, `Buffer`, stream, or object). A string is resolved as a directory path. A `URL` is used as is. File and URL sources resolve refs against their own location. |
115
+ | `pluginOptions` | no | `{}` | Passed unchanged as the second argument of the plugin's `generate`. Each plugin defines its own keys. |
116
+
117
+ `generate` runs these steps in order:
118
+
119
+ 1. **Load and bundle** the source with `@shipworthy/openapi-parser`. This parses JSON or YAML, inlines external `$ref`s, and upgrades OpenAPI 3.0 to 3.1. Any failure here becomes a `ResolveError`.
120
+ 2. **Decode** the bundled document into a `ParsedService` with `@shipworthy/openapi-transformer`'s `decode`. A failure becomes a `TransformError`.
121
+ 3. **Import the plugin** with `importPlugin(specifier, exportName)`. This can fail with `MissingPlugin` or `InvalidPlugin`. The plugin is imported only after steps 1 and 2 succeed, so an invalid document never loads plugin code.
122
+ 4. **Render** by calling `plugin.generate(model, pluginOptions ?? {})`. If the plugin returns a plain value, the effect succeeds with it. If it returns an `Effect`, that effect runs and its success value is the result. A synchronous throw, or a failure in the returned `Effect`, becomes a `TransformError` whose `cause` is the original error.
123
+
124
+ **Returns** `unknown`, whatever the plugin produced. Narrow or decode it yourself. For example, `@shipworthy/openapi-typescript`'s `typescript` plugin returns an array of TypeScript AST nodes.
125
+
126
+ **Errors:** `GenerateError = ResolveError | TransformError | MissingPlugin | InvalidPlugin`.
127
+
128
+ The source object you pass is not mutated. Object input is also not serialized, so values such as `bigint` extensions reach the model unchanged.
129
+
130
+ #### Example: running a published plugin with options
131
+
132
+ ```ts
133
+ import { NodeServices } from "@effect/platform-node";
134
+ import { generate } from "@shipworthy/openapi-generator";
135
+ import { Effect, Layer } from "effect";
136
+ import { FetchHttpClient } from "effect/http";
137
+
138
+ const nodes = await Effect.runPromise(
139
+ generate({
140
+ specifier: "@shipworthy/openapi-typescript",
141
+ exportName: "typescript",
142
+ source: new URL("https://example.com/openapi.yaml"),
143
+ pluginOptions: {/* options defined by @shipworthy/openapi-typescript */},
144
+ }).pipe(Effect.provide(Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer))),
145
+ );
146
+ ```
147
+
148
+ ### Sources
149
+
150
+ `source` accepts:
151
+
152
+ | Value | Treatment |
153
+ | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
154
+ | `URL` with `file:` protocol | Read from disk with `FileSystem`. |
155
+ | `URL` with `http:`/`https:` protocol | Fetched with `HttpClient`. A non-2xx status fails. |
156
+ | String starting with `http://`, `https://`, or `file://` | Converted to a `URL` and handled as above. |
157
+ | File path string (see below) | Resolved against `cwd` and read from disk with `FileSystem`. Relative `$ref`s resolve against the file. |
158
+ | Any other string | Parsed as document text. Text starting with `{` or `[` is tried as JSON first, with a YAML fallback. Anything else is parsed as YAML. A leading BOM is stripped. |
159
+ | `Buffer` | Decoded as UTF-8 text. |
160
+ | Node `Readable` stream | Read to the end as text. |
161
+ | Plain object | Used directly as the document. |
162
+
163
+ A string is a file path when, after trimming, it is a single line and either is absolute according to Effect's `Path` service on the current platform, starts with `./`, `../`, `.\`, or `..\`, or ends in `.json`, `.yaml`, or `.yml`, contains no `: `, and doesn't start with `{` or `[`. So `"./openapi.yaml"`, `"/srv/openapi.json"`, and `"specs/openapi.yml"` are read from disk, while inline JSON and YAML documents are still parsed as text. A missing file fails with a `ResolveError` that carries the `FileSystem` error, such as `NotFound: FileSystem.readFile (/abs/openapi.yaml)`.
164
+
165
+ ### `Generator<A, E>`
166
+
167
+ ```ts
168
+ interface Generator<A, E = never> {
169
+ readonly id: string;
170
+ generate(
171
+ model: ParsedService,
172
+ options: Readonly<Record<string, unknown>>,
173
+ ): A | Effect.Effect<A, E>;
174
+ }
175
+ ```
176
+
177
+ A plugin is a named export that implements this interface. `model` is the decoded `ParsedService` from `@shipworthy/openapi-transformer`, which includes `operations`, `paths`, `webhooks`, `components`, `servers`, `securitySchemes`, and `info` fields such as `name` and `version`. `options` is `GenerateOptions.pluginOptions`. `generate` can return a value or an `Effect`, synchronously or not.
178
+
179
+ ```ts
180
+ import type { Generator } from "@shipworthy/openapi-generator";
181
+ import type { ParsedService } from "@shipworthy/openapi-transformer";
182
+ import { Effect } from "effect";
183
+
184
+ class RouteError extends Error {}
185
+
186
+ export const routes: Generator<string, RouteError> = {
187
+ id: "routes",
188
+ generate: (model: ParsedService, options) =>
189
+ Effect.gen(function* () {
190
+ if (model.operations.length === 0) return yield* Effect.fail(new RouteError("No operations"));
191
+ const prefix = typeof options.prefix === "string" ? options.prefix : "";
192
+ return model.operations.map((op) => `${prefix}${op.path}`).join("\n");
193
+ }),
194
+ };
195
+ ```
196
+
197
+ Type `model` with `ParsedService` and the other `Parsed*` types from `@shipworthy/openapi-transformer`, not with an ad-hoc document type.
198
+
199
+ ### `importPlugin(specifier, exportName)`
200
+
201
+ ```ts
202
+ function importPlugin(
203
+ specifier: string,
204
+ exportName: string,
205
+ ): Effect.Effect<Generator<unknown>, MissingPlugin | InvalidPlugin>;
206
+ ```
207
+
208
+ Loads `specifier` through `ModuleLoader` and returns `module[exportName]`. `generate` calls it for you. Call it directly to check that a plugin is installed and valid without running the pipeline.
209
+
210
+ - If the module can't be loaded (the import rejects), it fails with `MissingPlugin`. The message is `Install <specifier> to load <exportName>.` and `cause` is the import error.
211
+ - If the export is missing, doesn't have a function `generate`, or doesn't have a string `id`, it fails with `InvalidPlugin`. The message is `Module <specifier> does not export <exportName>.`
212
+
213
+ ### `ModuleLoader`
214
+
215
+ ```ts
216
+ const ModuleLoader: Context.Reference<{ readonly load: (specifier: string) => Promise<unknown> }>;
217
+ ```
218
+
219
+ The service that imports plugin modules. By default it calls `import(specifier)` from inside this package, so bare specifiers resolve from this package's install location. Override it when plugins must resolve from your project, or to inject a module in tests:
220
+
221
+ ```ts
222
+ import { generate, ModuleLoader } from "@shipworthy/openapi-generator";
223
+ import { Effect } from "effect";
224
+
225
+ // Resolve plugin specifiers from the calling module (this is what the openapi CLI does).
226
+ generate(options).pipe(
227
+ Effect.provideService(ModuleLoader, { load: (specifier) => import(specifier) }),
228
+ );
229
+
230
+ // Inject an in-memory plugin in tests.
231
+ const plugin = { id: "fixture", generate: (model: unknown) => model };
232
+ generate({ specifier: "fixture", exportName: "plugin", source: document }).pipe(
233
+ Effect.provideService(ModuleLoader, { load: () => Promise.resolve({ plugin }) }),
234
+ );
235
+ ```
236
+
237
+ ### Errors
238
+
239
+ All errors are `Schema.TaggedError` classes. Each one has a `_tag` and a `message`. Handle them with `Effect.catchTag` or `Effect.catchTags`.
240
+
241
+ | Class (`_tag`) | Fields | When |
242
+ | ---------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
243
+ | `ResolveError` | `message`, optional `cause` | The source couldn't be loaded, parsed, bundled, or validated. Examples: an empty source, unreadable file, failed fetch, invalid JSON/YAML, a non-object document, Swagger 2.0, an OpenAPI version outside 3.x, an unresolvable `$ref`, or a schema violation such as a missing `info.version`. |
244
+ | `TransformError` | `message`, `cause` | The bundled document couldn't be decoded into `ParsedService`, or the plugin threw or failed. `cause` is the original error. |
245
+ | `MissingPlugin` | `specifier`, `exportName`, `message`, `cause` | `ModuleLoader.load(specifier)` rejected. |
246
+ | `InvalidPlugin` | `specifier`, `exportName`, `message` | The module loaded, but `exportName` isn't a valid `Generator`. |
247
+
248
+ Example messages:
249
+
250
+ ```text
251
+ ResolveError Unsupported Swagger version: 2.x. Use OpenAPI 3.x instead.
252
+ ResolveError Expected an OpenAPI document object. Got string
253
+ MissingPlugin Install @shipworthy/openapi-zod to load zod.
254
+ InvalidPlugin Module example-generator does not export generate.
255
+ ```
256
+
257
+ Defects, such as an `Effect.die` inside a plugin's effect, aren't converted to `TransformError`. They propagate as defects.
258
+
259
+ ## Supported OpenAPI versions
260
+
261
+ - **OpenAPI 3.1.x:** used as is.
262
+ - **OpenAPI 3.0.x:** upgraded to 3.1 before decoding. The model's `openapi` is `"3.1.0"`. For example, `{ type: "string", nullable: true }` becomes `{ type: ["string", "null"] }`.
263
+ - **Swagger 2.0 and OpenAPI 4+:** rejected with `ResolveError`.
264
+
265
+ ## Related packages
266
+
267
+ | Package | Relation |
268
+ | --------------------------------- | --------------------------------------------------------------------------------------------------------- |
269
+ | `@shipworthy/openapi-cli` | The `openapi generate …` commands, which call `generate`. |
270
+ | `@shipworthy/openapi-parser` | Loads and bundles the source (step 1). |
271
+ | `@shipworthy/openapi-transformer` | Defines `ParsedService` and decodes into it (step 2). Import model types from it when you write a plugin. |
272
+ | `@shipworthy/openapi-typescript` | Plugin, export `typescript`. |
273
+ | `@shipworthy/openapi-zod` | Plugin, export `zod`. |
274
+
275
+ ## Troubleshooting
276
+
277
+ - **Type error or runtime failure about missing `FileSystem`, `Path`, or `HttpClient` services**: Provide `Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer)`, or use `BunServices.layer` on Bun.
278
+ - **`ResolveError: Expected an OpenAPI document object. Got string`**: You passed a file path string. Pass a `URL` instead.
279
+ - **`MissingPlugin` for a plugin that is installed**: The default loader resolves specifiers from this package's location. Pass an absolute `file://` URL, or provide `ModuleLoader` with `{ load: (s) => import(s) }` from your own module.
280
+ - **`InvalidPlugin`**: Check `exportName`. It must be a named export, not `default`, and the object must have both `id` and a `generate` function.
281
+ - **Relative `$ref`s fail for inline sources**: Set `cwd` to the directory that the refs are relative to.
282
+
283
+ ## Key facts (for AI agents)
284
+
285
+ - Entry point: `import { generate, importPlugin, ModuleLoader, ResolveError, TransformError, MissingPlugin, InvalidPlugin, type Generator, type GenerateOptions, type GenerateError } from "@shipworthy/openapi-generator"`.
286
+ - `generate({ specifier, exportName, source, cwd?, pluginOptions? })` returns `Effect<unknown, GenerateError, FileSystem | Path | HttpClient>`. It's not a Promise. Run it with `Effect.runPromise` after providing `NodeServices.layer` (or `BunServices.layer`) and `FetchHttpClient.layer`.
287
+ - Requires `effect` `^4.0.0` (peer dependency). ESM only, compiled JavaScript. Runs on Node.js `>=22.19.0` or Bun.
288
+ - Pipeline: bundle (`ResolveError`), then decode to `ParsedService` (`TransformError`), then import the plugin (`MissingPlugin` / `InvalidPlugin`), then `plugin.generate(model, pluginOptions ?? {})` (plugin failures become `TransformError`).
289
+ - File-path strings are parsed as YAML text. Pass `URL` objects for files. Strings starting with `http(s)://` or `file://` are fetched or read.
290
+ - OpenAPI 3.0 is upgraded to 3.1. Swagger 2.0 is rejected.
291
+ - A plugin is a named export `{ id: string, generate(model: ParsedService, options) => A | Effect<A, E> }`.
292
+ - Override `ModuleLoader` with `Effect.provideService(ModuleLoader, { load })` to control plugin resolution or inject test plugins.
293
+ - The return value is `unknown`. Validate or cast it to the plugin's output type.
294
+
295
+ ## License
296
+
297
+ MIT. See [LICENSE](./LICENSE).
@@ -0,0 +1,29 @@
1
+ import { Schema } from "effect";
2
+ declare const ResolveError_base: Schema.Class<ResolveError, Schema.TaggedStruct<"ResolveError", {
3
+ readonly message: Schema.String;
4
+ readonly cause: Schema.optionalKey<Schema.Defect>;
5
+ }>, import("effect/Cause").YieldableError>;
6
+ export declare class ResolveError extends ResolveError_base {
7
+ }
8
+ declare const TransformError_base: Schema.Class<TransformError, Schema.TaggedStruct<"TransformError", {
9
+ readonly message: Schema.String;
10
+ readonly cause: Schema.Defect;
11
+ }>, import("effect/Cause").YieldableError>;
12
+ export declare class TransformError extends TransformError_base {
13
+ }
14
+ declare const MissingPlugin_base: Schema.Class<MissingPlugin, Schema.TaggedStruct<"MissingPlugin", {
15
+ readonly specifier: Schema.String;
16
+ readonly exportName: Schema.String;
17
+ readonly message: Schema.String;
18
+ readonly cause: Schema.Defect;
19
+ }>, import("effect/Cause").YieldableError>;
20
+ export declare class MissingPlugin extends MissingPlugin_base {
21
+ }
22
+ declare const InvalidPlugin_base: Schema.Class<InvalidPlugin, Schema.TaggedStruct<"InvalidPlugin", {
23
+ readonly specifier: Schema.String;
24
+ readonly exportName: Schema.String;
25
+ readonly message: Schema.String;
26
+ }>, import("effect/Cause").YieldableError>;
27
+ export declare class InvalidPlugin extends InvalidPlugin_base {
28
+ }
29
+ export {};
package/dist/errors.js ADDED
@@ -0,0 +1,24 @@
1
+ import { Schema } from "effect";
2
+ export class ResolveError extends Schema.TaggedError()("ResolveError", {
3
+ message: Schema.String,
4
+ cause: Schema.optionalKey(Schema.Defect()),
5
+ }) {
6
+ }
7
+ export class TransformError extends Schema.TaggedError()("TransformError", {
8
+ message: Schema.String,
9
+ cause: Schema.Defect(),
10
+ }) {
11
+ }
12
+ export class MissingPlugin extends Schema.TaggedError()("MissingPlugin", {
13
+ specifier: Schema.String,
14
+ exportName: Schema.String,
15
+ message: Schema.String,
16
+ cause: Schema.Defect(),
17
+ }) {
18
+ }
19
+ export class InvalidPlugin extends Schema.TaggedError()("InvalidPlugin", {
20
+ specifier: Schema.String,
21
+ exportName: Schema.String,
22
+ message: Schema.String,
23
+ }) {
24
+ }
@@ -0,0 +1,14 @@
1
+ import { Effect, FileSystem, Path } from "effect";
2
+ import { HttpClient } from "effect/http";
3
+ import { ResolveError, TransformError, type InvalidPlugin, type MissingPlugin } from "./errors.ts";
4
+ export interface GenerateOptions {
5
+ readonly specifier: string;
6
+ readonly exportName: string;
7
+ readonly source: unknown;
8
+ readonly cwd?: URL | string;
9
+ readonly pluginOptions?: Readonly<Record<string, unknown>>;
10
+ }
11
+ type GenerateServices = FileSystem.FileSystem | Path.Path | HttpClient.HttpClient;
12
+ export declare const generate: (options: GenerateOptions) => Effect.Effect<unknown, GenerateError, GenerateServices>;
13
+ export type GenerateError = ResolveError | TransformError | MissingPlugin | InvalidPlugin;
14
+ export {};
@@ -0,0 +1,29 @@
1
+ import { bundleDocument } from "@shipworthy/openapi-parser";
2
+ import { decode } from "@shipworthy/openapi-transformer";
3
+ import { Effect, Predicate } from "effect";
4
+ import { ResolveError, TransformError } from "./errors.js";
5
+ import { importPlugin } from "./plugins.js";
6
+ export const generate = Effect.fn("generate")(function* (options) {
7
+ const document = yield* bundleDocument(options.source, { cwd: options.cwd }).pipe(Effect.mapError((cause) => new ResolveError({ message: cause.message, cause })));
8
+ const model = yield* decode(document).pipe(Effect.mapError((cause) => new TransformError({ message: cause.message, cause })));
9
+ const plugin = yield* importPlugin(options.specifier, options.exportName);
10
+ return yield* render(plugin, model, options.pluginOptions ?? {});
11
+ });
12
+ function failureMessage(cause) {
13
+ return Predicate.isError(cause) ? cause.message : String(cause);
14
+ }
15
+ function pluginFailure(cause) {
16
+ return new TransformError({ message: failureMessage(cause), cause });
17
+ }
18
+ function isPluginEffect(value) {
19
+ return Effect.isEffect(value);
20
+ }
21
+ const render = Effect.fnUntraced(function* (plugin, model, options) {
22
+ const value = yield* Effect.try({
23
+ try: () => plugin.generate(model, options),
24
+ catch: pluginFailure,
25
+ });
26
+ if (!isPluginEffect(value))
27
+ return value;
28
+ return yield* Effect.mapError(value, pluginFailure);
29
+ });
@@ -0,0 +1,6 @@
1
+ import type { ParsedService } from "@shipworthy/openapi-transformer";
2
+ import type { Effect } from "effect";
3
+ export interface Generator<A, E = never> {
4
+ readonly id: string;
5
+ generate(model: ParsedService, options: Readonly<Record<string, unknown>>): A | Effect.Effect<A, E>;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ export { InvalidPlugin, MissingPlugin, ResolveError, TransformError } from "./errors.ts";
2
+ export { generate, type GenerateError, type GenerateOptions } from "./generate.ts";
3
+ export { type Generator } from "./generator.ts";
4
+ export { importPlugin, ModuleLoader } from "./plugins.ts";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { InvalidPlugin, MissingPlugin, ResolveError, TransformError } from "./errors.js";
2
+ export { generate } from "./generate.js";
3
+ export { importPlugin, ModuleLoader } from "./plugins.js";
@@ -0,0 +1,7 @@
1
+ import type { Generator } from "./generator.ts";
2
+ import { Context, Effect } from "effect";
3
+ import { InvalidPlugin, MissingPlugin } from "./errors.ts";
4
+ export declare const ModuleLoader: Context.Reference<{
5
+ readonly load: (specifier: string) => Promise<unknown>;
6
+ }>;
7
+ export declare const importPlugin: (specifier: string, exportName: string) => Effect.Effect<Generator<unknown, never>, InvalidPlugin | MissingPlugin, never>;
@@ -0,0 +1,51 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { Context, Effect, Predicate, Schema } from "effect";
10
+ import { InvalidPlugin, MissingPlugin } from "./errors.js";
11
+ export const ModuleLoader = Context.Reference("@shipworthy/openapi-generator/plugins/ModuleLoader", {
12
+ defaultValue: () => ({ load: (specifier) => import(__rewriteRelativeImportExtension(specifier)) }),
13
+ });
14
+ const generatorShape = Schema.Struct({
15
+ id: Schema.String,
16
+ generate: Schema.declare((value) => Predicate.isFunction(value)),
17
+ });
18
+ // Keep plugin identity and prototype methods while validating its callable interface.
19
+ const generatorSchema = Schema.declare(Schema.is(generatorShape));
20
+ const moduleSchema = Schema.Record(Schema.String, Schema.Unknown);
21
+ /** Whether `cause` reports `specifier`'s own package as missing, not one of its imports. */
22
+ function isPackageNotFound(cause, specifier) {
23
+ if (!Predicate.isError(cause) || !Predicate.hasProperty(cause, "code"))
24
+ return false;
25
+ if (cause.code !== "ERR_MODULE_NOT_FOUND" && cause.code !== "MODULE_NOT_FOUND")
26
+ return false;
27
+ const [scope = "", name = ""] = specifier.split("/");
28
+ const packageName = scope.startsWith("@") ? `${scope}/${name}` : scope;
29
+ return cause.message.includes(`'${packageName}'`);
30
+ }
31
+ export const importPlugin = Effect.fn("importPlugin")(function* (specifier, exportName) {
32
+ const loader = yield* ModuleLoader;
33
+ const loaded = yield* Effect.tryPromise({
34
+ try: () => loader.load(specifier),
35
+ catch: (cause) => new MissingPlugin({
36
+ specifier,
37
+ exportName,
38
+ message: isPackageNotFound(cause, specifier)
39
+ ? `Install ${specifier} to load ${exportName}.`
40
+ : `Could not load ${specifier}: ${Predicate.isError(cause) ? cause.message : String(cause)}`,
41
+ cause,
42
+ }),
43
+ });
44
+ const invalid = () => new InvalidPlugin({
45
+ specifier,
46
+ exportName,
47
+ message: `Module ${specifier} does not export ${exportName}.`,
48
+ });
49
+ const module = yield* Schema.decodeUnknownEffect(moduleSchema)(loaded).pipe(Effect.mapError(invalid));
50
+ return yield* Schema.decodeUnknownEffect(generatorSchema)(module[exportName]).pipe(Effect.mapError(invalid));
51
+ });
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@shipworthy/openapi-generator",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1",
4
+ "files": [
5
+ "dist"
6
+ ],
7
+ "dependencies": {
8
+ "@shipworthy/openapi-parser": "0.0.1",
9
+ "@shipworthy/openapi-transformer": "0.0.1"
10
+ },
11
+ "peerDependencies": {
12
+ "effect": "^4.0.0"
13
+ },
14
+ "description": "Load, resolve, and parse an OpenAPI document, then run one generator plugin.",
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/ShipWorthyAI/openapi.git",
19
+ "directory": "packages/openapi-generator"
20
+ },
21
+ "type": "module",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "default": "./dist/index.js"
26
+ },
27
+ "./package.json": "./package.json"
28
+ },
29
+ "scripts": {
30
+ "test": "vitest run",
31
+ "build": "rm -rf dist && tsc -p tsconfig.build.json"
32
+ },
33
+ "devDependencies": {
34
+ "@effect/vitest": "4.0.0",
35
+ "@shipworthy/typescript-config": "0.0.1",
36
+ "@types/node": "^24.10.3",
37
+ "effect": "^4.0.0",
38
+ "typescript": "^7.0.2",
39
+ "vitest": "5.0.3"
40
+ },
41
+ "engines": {
42
+ "node": ">=22.19.0"
43
+ },
44
+ "main": "./dist/index.js",
45
+ "types": "./dist/index.d.ts",
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }