@haddowg/json-api-codegen 1.0.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/LICENSE +21 -0
- package/README.md +170 -0
- package/dist/cli.d.mts +14 -0
- package/dist/cli.d.mts.map +1 -0
- package/dist/cli.mjs +66 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index-C0becRSc.d.mts +471 -0
- package/dist/index-C0becRSc.d.mts.map +1 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +2 -0
- package/dist/src-B24cYyjM.mjs +1278 -0
- package/dist/src-B24cYyjM.mjs.map +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gregory Haddow
|
|
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
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# @haddowg/json-api-codegen
|
|
2
|
+
|
|
3
|
+
> **Part of the [jsonapi.rest](https://jsonapi.rest) suite** — a complete, spec-compliant
|
|
4
|
+
> JSON:API 1.1 stack: a framework-agnostic PHP [core](https://github.com/haddowg/json-api), a
|
|
5
|
+
> [Symfony bundle](https://github.com/haddowg/json-api-symfony), a
|
|
6
|
+
> [Laravel package](https://github.com/haddowg/json-api-laravel), and this **typed TypeScript
|
|
7
|
+
> client**, bound together by one conformance-tested OpenAPI 3.1 contract.
|
|
8
|
+
> Full documentation: **[haddowg.github.io/json-api-ts](https://haddowg.github.io/json-api-ts/)**.
|
|
9
|
+
|
|
10
|
+
The CLI (and programmatic API) that reads the
|
|
11
|
+
[`haddowg/json-api-symfony`](https://github.com/haddowg/json-api-symfony) OpenAPI 3.1 document and
|
|
12
|
+
emits a typed client: a runtime descriptor, per-type attribute interfaces, and a descriptor-bound
|
|
13
|
+
`createClient`. Optionally reads the bundle's JSON Schemas to emit a per-type schema map for the
|
|
14
|
+
[client's](../client) opt-in validation seam.
|
|
15
|
+
|
|
16
|
+
The OpenAPI document is fully self-describing for the runtime's needs — type identity
|
|
17
|
+
(`properties.type.const`), relationship cardinality and related types, the allowed `?include`
|
|
18
|
+
paths (a bounded, pre-expanded enum per endpoint), the create client-id policy, and paginator
|
|
19
|
+
kinds. The codegen only reads the spec; all real work is generic runtime machinery in
|
|
20
|
+
`@haddowg/json-api-client`.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
A dev-only dependency — the **generated output** has no runtime tie back to the codegen (it imports
|
|
25
|
+
`@haddowg/json-api-client`).
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pnpm add -D @haddowg/json-api-codegen
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Two bin names are installed: `json-api-codegen` and the short alias `japi`.
|
|
32
|
+
|
|
33
|
+
## CLI
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
json-api-codegen --input <url|file> --output <file> [--server <name>] [--schemas <url|file>] [--check]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| Flag | Required | Description |
|
|
40
|
+
| ----------- | -------- | ------------------------------------------------------------------------------------- |
|
|
41
|
+
| `--input` | yes | OpenAPI document source — an http(s) URL or a local file path (JSON or YAML). |
|
|
42
|
+
| `--output` | yes | Output file for the generated client (e.g. `src/api/music.gen.ts`). |
|
|
43
|
+
| `--server` | no | Server name to target. Metadata-only — the served document is already per-server. |
|
|
44
|
+
| `--schemas` | no | JSON Schema source (URL or path) for the validation seam. Omit to skip schema output. |
|
|
45
|
+
| `--check` | no | Drift gate — regenerate in memory and exit non-zero if the committed output differs. |
|
|
46
|
+
|
|
47
|
+
Example — generate against a served API, with the validation schemas:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
json-api-codegen \
|
|
51
|
+
--input https://music.example/docs.json \
|
|
52
|
+
--output src/api/music.gen.ts \
|
|
53
|
+
--schemas https://music.example/schemas.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This writes:
|
|
57
|
+
|
|
58
|
+
- `src/api/music.gen.ts` — the typed client (descriptor + types + bound `createClient`);
|
|
59
|
+
- `src/api/music.schemas.gen.ts` — the per-type JSON Schema map (only with `--schemas`).
|
|
60
|
+
|
|
61
|
+
The schema artifact path is derived from `--output`: a `.gen.ts` suffix becomes `.schemas.gen.ts`,
|
|
62
|
+
a plain `.ts` becomes `.schemas.ts`.
|
|
63
|
+
|
|
64
|
+
> **Commit the generated files.** Like `openapi-typescript` / `graphql-codegen`, the output is one
|
|
65
|
+
> small, reviewable, diffable, versioned module in your repo — not a published package. Regenerate
|
|
66
|
+
> when the API changes.
|
|
67
|
+
|
|
68
|
+
### Provenance stamp
|
|
69
|
+
|
|
70
|
+
Every generated artifact carries a provenance record in its header — the source spec identifier
|
|
71
|
+
(`<info.title> <info.version>`), the first declared server URL, and a stable 16-hex content hash of
|
|
72
|
+
the source document (the schema artifact carries a `Schema hash` of the schema bundle). It is
|
|
73
|
+
**timestamp-free and deterministic**: regenerating from the same spec yields byte-identical output.
|
|
74
|
+
The hash tells a reviewer which spec a committed client was generated from, and lets the drift gate
|
|
75
|
+
below compare regenerated output byte-for-byte.
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
/**
|
|
79
|
+
* AUTO-GENERATED by @haddowg/json-api-codegen — do not edit by hand.
|
|
80
|
+
* Regenerate from the source OpenAPI document instead.
|
|
81
|
+
*
|
|
82
|
+
* Source spec: Music Catalog API 1.0.0
|
|
83
|
+
* Server: https://music.example
|
|
84
|
+
* Spec hash: 40e273dff0411f68
|
|
85
|
+
*/
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Drift check (`--check`)
|
|
89
|
+
|
|
90
|
+
`--check` regenerates in memory and compares against the committed `--output` (and its schema
|
|
91
|
+
sibling when `--schemas` is set) **without writing anything**. It prints a per-artifact `ok`/`DRIFT`
|
|
92
|
+
line and exits non-zero if any committed file is missing or out of date. Wire it into CI — pinning
|
|
93
|
+
the spec to a committed fixture — so a stale generated client fails the build:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
json-api-codegen \
|
|
97
|
+
--input spec/music-catalog.openapi.json \
|
|
98
|
+
--output src/api/music.gen.ts \
|
|
99
|
+
--schemas spec/music-catalog.schemas.json \
|
|
100
|
+
--check
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Multiple servers
|
|
104
|
+
|
|
105
|
+
The bundle serves one OpenAPI document per server (e.g. `default` and `admin` differ in their
|
|
106
|
+
type set and path prefix). Generate one client per server by pointing `--input` at each server's
|
|
107
|
+
document — one typed client per server.
|
|
108
|
+
|
|
109
|
+
## What it generates
|
|
110
|
+
|
|
111
|
+
The output module exports:
|
|
112
|
+
|
|
113
|
+
- `resourceMap` — the runtime descriptor (`as const`); what the [query](../query) package's
|
|
114
|
+
normalization needs.
|
|
115
|
+
- `ResourceMap` — `typeof resourceMap`.
|
|
116
|
+
- `createClient(options)` — the descriptor-bound factory (the descriptor and the server's `atomic`
|
|
117
|
+
capability are baked in; you supply only [`ClientOptions`](../client)).
|
|
118
|
+
- `Attributes` / `WriteAttributes` — the per-type attribute maps (read vs create/update).
|
|
119
|
+
- Per-type `…Attributes` / `…CreateAttributes` / `…UpdateAttributes` interfaces, enum types
|
|
120
|
+
(e.g. `AlbumStatus`), and action input/output types.
|
|
121
|
+
|
|
122
|
+
With `--schemas`, the sibling module exports `schemas` — the per-type JSON Schema 2020-12 map.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { createClient, type ResourceMap, resourceMap } from './api/music.gen'
|
|
126
|
+
import { schemas } from './api/music.schemas.gen'
|
|
127
|
+
|
|
128
|
+
const client = createClient({ baseUrl: 'https://music.example' })
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Verb / relation collisions
|
|
132
|
+
|
|
133
|
+
If a type has a relation named like a reserved verb on the resource handle (`get`, `update`,
|
|
134
|
+
`delete`, `rel`, `actions`, …), the codegen emits a **build-time warning** and that relation routes
|
|
135
|
+
through `.rel('name')` in the fluent client; the common case stays clean.
|
|
136
|
+
|
|
137
|
+
## Programmatic API
|
|
138
|
+
|
|
139
|
+
The same generation, callable from a script (`japi.config.ts`, a build step, etc.):
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { generate, type CodegenConfig } from '@haddowg/json-api-codegen'
|
|
143
|
+
|
|
144
|
+
const config: CodegenConfig = {
|
|
145
|
+
input: 'https://music.example/docs.json',
|
|
146
|
+
output: 'src/api/music.gen.ts',
|
|
147
|
+
schemas: 'https://music.example/schemas.json', // optional
|
|
148
|
+
server: 'default', // optional, metadata-only
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await generate(config) // writes the output file(s); returns the client source string
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The drift gate is also programmatic — `check(config)` regenerates in memory and returns
|
|
155
|
+
`{ ok, artifacts }` (per-artifact `{ path, upToDate }`) without writing:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { check } from '@haddowg/json-api-codegen'
|
|
159
|
+
|
|
160
|
+
const result = await check({ ...config, check: true })
|
|
161
|
+
if (!result.ok) process.exit(1)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Lower-level building blocks are also exported for advanced use: `readDocument` / `readSchemas`,
|
|
165
|
+
`buildDescriptor`, `emit`, `emitSchemas`, `detectVerbCollisions`, `schemasOutputPath`, and the
|
|
166
|
+
provenance helpers `deriveProvenance` / `provenanceLines` / `hashJson`.
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { r as CodegenConfig } from "./index-C0becRSc.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/cli.d.ts
|
|
4
|
+
declare const USAGE = "Usage: json-api-codegen --input <url|file> --output <file> [--server <name>] [--schemas <url|file>] [--check]";
|
|
5
|
+
/**
|
|
6
|
+
* Thin CLI wrapper. Real arg parsing + `japi.config.ts` discovery land with the
|
|
7
|
+
* generator itself; this stub establishes the bin entry and the shape of invocation.
|
|
8
|
+
* `--check` is a boolean flag (drift gate) with no value.
|
|
9
|
+
*/
|
|
10
|
+
declare function parseArgs(argv: string[]): Partial<CodegenConfig>;
|
|
11
|
+
declare function run(argv: string[]): Promise<number>;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { USAGE, parseArgs, run };
|
|
14
|
+
//# sourceMappingURL=cli.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.mts","names":[],"sources":["../src/cli.ts"],"mappings":";;;cAIa,KAAA;AAAb;;;;AAAkB;AAAlB,iBAQgB,SAAA,CAAU,IAAA,aAAiB,OAAO,CAAC,aAAA;AAAA,iBA6B7B,GAAA,CAAI,IAAA,aAAiB,OAAO"}
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as generate, t as check } from "./src-B24cYyjM.mjs";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
//#region src/cli.ts
|
|
5
|
+
const USAGE = "Usage: json-api-codegen --input <url|file> --output <file> [--server <name>] [--schemas <url|file>] [--check]";
|
|
6
|
+
/**
|
|
7
|
+
* Thin CLI wrapper. Real arg parsing + `japi.config.ts` discovery land with the
|
|
8
|
+
* generator itself; this stub establishes the bin entry and the shape of invocation.
|
|
9
|
+
* `--check` is a boolean flag (drift gate) with no value.
|
|
10
|
+
*/
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
const config = {};
|
|
13
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14
|
+
const arg = argv[i];
|
|
15
|
+
if (arg === "--check") {
|
|
16
|
+
config.check = true;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const next = argv[i + 1];
|
|
20
|
+
if (next === void 0) continue;
|
|
21
|
+
if (arg === "--input") {
|
|
22
|
+
config.input = next;
|
|
23
|
+
i++;
|
|
24
|
+
} else if (arg === "--output") {
|
|
25
|
+
config.output = next;
|
|
26
|
+
i++;
|
|
27
|
+
} else if (arg === "--server") {
|
|
28
|
+
config.server = next;
|
|
29
|
+
i++;
|
|
30
|
+
} else if (arg === "--schemas") {
|
|
31
|
+
config.schemas = next;
|
|
32
|
+
i++;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return config;
|
|
36
|
+
}
|
|
37
|
+
async function run(argv) {
|
|
38
|
+
const config = parseArgs(argv);
|
|
39
|
+
if (!config.input || !config.output) {
|
|
40
|
+
console.error(USAGE);
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
if (config.check) {
|
|
44
|
+
const result = await check(config);
|
|
45
|
+
for (const { path, upToDate } of result.artifacts) console.error(`${upToDate ? "ok " : "DRIFT"} ${path}`);
|
|
46
|
+
if (!result.ok) {
|
|
47
|
+
console.error("Generated client is out of date with the source spec. Regenerate (drop --check) and commit the result.");
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
await generate(config);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
async function main() {
|
|
56
|
+
process.exitCode = await run(process.argv.slice(2));
|
|
57
|
+
}
|
|
58
|
+
const entry = process.argv[1];
|
|
59
|
+
if (entry !== void 0 && import.meta.url === pathToFileURL(entry).href) main().catch((error) => {
|
|
60
|
+
console.error(error instanceof Error ? error.message : error);
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
});
|
|
63
|
+
//#endregion
|
|
64
|
+
export { USAGE, parseArgs, run };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=cli.mjs.map
|
package/dist/cli.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { pathToFileURL } from 'node:url'\nimport { check, generate, type CodegenConfig } from './index'\n\nexport const USAGE =\n 'Usage: json-api-codegen --input <url|file> --output <file> [--server <name>] [--schemas <url|file>] [--check]'\n\n/**\n * Thin CLI wrapper. Real arg parsing + `japi.config.ts` discovery land with the\n * generator itself; this stub establishes the bin entry and the shape of invocation.\n * `--check` is a boolean flag (drift gate) with no value.\n */\nexport function parseArgs(argv: string[]): Partial<CodegenConfig> {\n const config: Partial<CodegenConfig> = {}\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i]\n if (arg === '--check') {\n config.check = true\n continue\n }\n const next = argv[i + 1]\n if (next === undefined) {\n continue\n }\n if (arg === '--input') {\n config.input = next\n i++\n } else if (arg === '--output') {\n config.output = next\n i++\n } else if (arg === '--server') {\n config.server = next\n i++\n } else if (arg === '--schemas') {\n config.schemas = next\n i++\n }\n }\n return config\n}\n\nexport async function run(argv: string[]): Promise<number> {\n const config = parseArgs(argv)\n if (!config.input || !config.output) {\n console.error(USAGE)\n return 1\n }\n if (config.check) {\n const result = await check(config as CodegenConfig)\n for (const { path, upToDate } of result.artifacts) {\n console.error(`${upToDate ? 'ok ' : 'DRIFT'} ${path}`)\n }\n if (!result.ok) {\n console.error(\n 'Generated client is out of date with the source spec. Regenerate (drop --check) and commit the result.',\n )\n return 1\n }\n return 0\n }\n await generate(config as CodegenConfig)\n return 0\n}\n\nasync function main(): Promise<void> {\n process.exitCode = await run(process.argv.slice(2))\n}\n\n// Only run when invoked as the bin, not when imported (e.g. by tests).\nconst entry = process.argv[1]\nif (entry !== undefined && import.meta.url === pathToFileURL(entry).href) {\n main().catch((error: unknown) => {\n console.error(error instanceof Error ? error.message : error)\n process.exitCode = 1\n })\n}\n"],"mappings":";;;;AAIA,MAAa,QACX;;;;;;AAOF,SAAgB,UAAU,MAAwC;CAChE,MAAM,SAAiC,CAAC;CACxC,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;EACjB,IAAI,QAAQ,WAAW;GACrB,OAAO,QAAQ;GACf;EACF;EACA,MAAM,OAAO,KAAK,IAAI;EACtB,IAAI,SAAS,KAAA,GACX;EAEF,IAAI,QAAQ,WAAW;GACrB,OAAO,QAAQ;GACf;EACF,OAAO,IAAI,QAAQ,YAAY;GAC7B,OAAO,SAAS;GAChB;EACF,OAAO,IAAI,QAAQ,YAAY;GAC7B,OAAO,SAAS;GAChB;EACF,OAAO,IAAI,QAAQ,aAAa;GAC9B,OAAO,UAAU;GACjB;EACF;CACF;CACA,OAAO;AACT;AAEA,eAAsB,IAAI,MAAiC;CACzD,MAAM,SAAS,UAAU,IAAI;CAC7B,IAAI,CAAC,OAAO,SAAS,CAAC,OAAO,QAAQ;EACnC,QAAQ,MAAM,KAAK;EACnB,OAAO;CACT;CACA,IAAI,OAAO,OAAO;EAChB,MAAM,SAAS,MAAM,MAAM,MAAuB;EAClD,KAAK,MAAM,EAAE,MAAM,cAAc,OAAO,WACtC,QAAQ,MAAM,GAAG,WAAW,SAAS,QAAQ,GAAG,MAAM;EAExD,IAAI,CAAC,OAAO,IAAI;GACd,QAAQ,MACN,wGACF;GACA,OAAO;EACT;EACA,OAAO;CACT;CACA,MAAM,SAAS,MAAuB;CACtC,OAAO;AACT;AAEA,eAAe,OAAsB;CACnC,QAAQ,WAAW,MAAM,IAAI,QAAQ,KAAK,MAAM,CAAC,CAAC;AACpD;AAGA,MAAM,QAAQ,QAAQ,KAAK;AAC3B,IAAI,UAAU,KAAA,KAAa,OAAO,KAAK,QAAQ,cAAc,KAAK,CAAC,CAAC,MAClE,KAAK,CAAC,CAAC,OAAO,UAAmB;CAC/B,QAAQ,MAAM,iBAAiB,QAAQ,MAAM,UAAU,KAAK;CAC5D,QAAQ,WAAW;AACrB,CAAC"}
|