@xemahq/api-client-generator 0.2.0 → 0.3.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/dist/bin/xema-openapi.js +2 -2
- package/dist/bin/xema-openapi.js.map +1 -1
- package/dist/lib/client-package.d.ts.map +1 -1
- package/dist/lib/client-package.js +9 -4
- package/dist/lib/client-package.js.map +1 -1
- package/dist/lib/default-stubs.d.ts.map +1 -1
- package/dist/lib/default-stubs.js +35 -10
- package/dist/lib/default-stubs.js.map +1 -1
- package/dist/lib/extract-openapi.d.ts +2 -2
- package/dist/lib/extract-openapi.d.ts.map +1 -1
- package/dist/lib/extract-openapi.js +16 -7
- package/dist/lib/extract-openapi.js.map +1 -1
- package/dist/lib/generate-clients.d.ts +1 -1
- package/dist/lib/generate-clients.d.ts.map +1 -1
- package/dist/lib/generate-clients.js +1 -1
- package/dist/lib/generate-clients.js.map +1 -1
- package/dist/lib/overrides.d.ts.map +1 -1
- package/dist/lib/overrides.js +10 -2
- package/dist/lib/overrides.js.map +1 -1
- package/dist/lib/repo-post-process.d.ts.map +1 -1
- package/dist/lib/repo-post-process.js +2 -1
- package/dist/lib/repo-post-process.js.map +1 -1
- package/dist/lib/seed-openapi-env.d.ts.map +1 -1
- package/dist/lib/seed-openapi-env.js +0 -20
- package/dist/lib/seed-openapi-env.js.map +1 -1
- package/dist/lib/service-config.d.ts +1 -1
- package/dist/lib/service-config.d.ts.map +1 -1
- package/dist/lib/service-config.js +2 -2
- package/dist/lib/service-config.js.map +1 -1
- package/dist/lib/service-nest.d.ts.map +1 -1
- package/dist/lib/service-nest.js.map +1 -1
- package/package.json +5 -3
- package/src/bin/xema-client-generate.ts +11 -0
- package/src/bin/xema-openapi.ts +51 -0
- package/src/index.ts +9 -0
- package/src/lib/client-package.ts +215 -0
- package/src/lib/client-readme.ts +124 -0
- package/src/lib/custom-fetch-template.ts +200 -0
- package/src/lib/default-stubs.ts +422 -0
- package/src/lib/extract-openapi.ts +240 -0
- package/src/lib/generate-clients.ts +240 -0
- package/src/lib/overrides.ts +81 -0
- package/src/lib/repo-post-process.ts +106 -0
- package/src/lib/seed-openapi-env.ts +137 -0
- package/src/lib/service-config.ts +170 -0
- package/src/lib/service-nest.ts +58 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { ApiSurface } from '@xemahq/xema-decorators';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The per-service residue of the central generator: a `xema.api` block in the
|
|
8
|
+
* service's own `package.json`. Everything else (spec paths, client dirs,
|
|
9
|
+
* client package names, mutator path, orval mode) is derived from this plus
|
|
10
|
+
* the repo layout — there is no `orval.config.ts`, no extract script.
|
|
11
|
+
*
|
|
12
|
+
* ```jsonc
|
|
13
|
+
* // <service>/package.json
|
|
14
|
+
* { "name": "backlog-api", "version": "0.1.0",
|
|
15
|
+
* "xema": { "api": { "serviceId": "backlog-api", "biomeId": "backlog",
|
|
16
|
+
* "title": "Backlog API", "description": "…" } } }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export interface XemaApiConfig {
|
|
20
|
+
readonly serviceId: string;
|
|
21
|
+
readonly biomeId: string;
|
|
22
|
+
readonly title: string;
|
|
23
|
+
readonly description?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ResolvedServiceContext {
|
|
27
|
+
/** Absolute path to the service directory (process cwd). */
|
|
28
|
+
readonly serviceDir: string;
|
|
29
|
+
/** Absolute path to the repository root (holds pnpm-workspace.yaml). */
|
|
30
|
+
readonly repoRoot: string;
|
|
31
|
+
/** The declared `xema.api` block. */
|
|
32
|
+
readonly api: XemaApiConfig;
|
|
33
|
+
/** The service `package.json` version — stamped into every surface client. */
|
|
34
|
+
readonly version: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Walk up from `start` to the directory holding `pnpm-workspace.yaml`. */
|
|
38
|
+
export function findRepoRoot(start: string): string {
|
|
39
|
+
let dir = resolve(start);
|
|
40
|
+
while (dir !== dirname(dir)) {
|
|
41
|
+
if (existsSync(join(dir, 'pnpm-workspace.yaml'))) {
|
|
42
|
+
return dir;
|
|
43
|
+
}
|
|
44
|
+
dir = dirname(dir);
|
|
45
|
+
}
|
|
46
|
+
throw new Error(
|
|
47
|
+
`api-client-generator: could not locate a repo root (pnpm-workspace.yaml) ` +
|
|
48
|
+
`walking up from ${start}.`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Read and validate the service's `package.json#xema.api` block + version.
|
|
54
|
+
* Fail-fast on a missing/malformed block — the generator never guesses a
|
|
55
|
+
* serviceId or biomeId.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveServiceContext(serviceDir: string): ResolvedServiceContext {
|
|
58
|
+
const pkgPath = join(serviceDir, 'package.json');
|
|
59
|
+
if (!existsSync(pkgPath)) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`api-client-generator: no package.json found at ${pkgPath}. Run the ` +
|
|
62
|
+
`generator from a service directory.`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as {
|
|
66
|
+
version?: unknown;
|
|
67
|
+
xema?: { api?: Partial<XemaApiConfig> } | null;
|
|
68
|
+
};
|
|
69
|
+
const api = pkg.xema?.api;
|
|
70
|
+
if (!api || typeof api !== 'object') {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`api-client-generator: ${pkgPath} is missing the required \`xema.api\` ` +
|
|
73
|
+
`block. Add: { "xema": { "api": { "serviceId": "<id>", "biomeId": ` +
|
|
74
|
+
`"<biome>", "title": "<title>" } } }.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const { serviceId, biomeId, title, description } = api;
|
|
78
|
+
for (const [field, value] of Object.entries({ serviceId, biomeId, title })) {
|
|
79
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`api-client-generator: \`xema.api.${field}\` is required and must be a ` +
|
|
82
|
+
`non-empty string in ${pkgPath}.`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (typeof pkg.version !== 'string' || pkg.version.trim() === '') {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`api-client-generator: ${pkgPath} is missing a \`version\` — every ` +
|
|
89
|
+
`generated surface client shares the service version.`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
serviceDir: resolve(serviceDir),
|
|
94
|
+
repoRoot: findRepoRoot(serviceDir),
|
|
95
|
+
api: {
|
|
96
|
+
serviceId: serviceId as string,
|
|
97
|
+
biomeId: biomeId as string,
|
|
98
|
+
title: title as string,
|
|
99
|
+
...(typeof description === 'string' ? { description } : {}),
|
|
100
|
+
},
|
|
101
|
+
version: pkg.version,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Short package name (without the `@xemahq/` scope) for a surface client.
|
|
107
|
+
*
|
|
108
|
+
* - public: `<serviceId>-client` (unchanged from today, e.g.
|
|
109
|
+
* `backlog-api` → `backlog-api-client`)
|
|
110
|
+
* - internal: surface segment inserted before a trailing `-api`, matching the
|
|
111
|
+
* agreed shape `llm-registry-api` → `llm-registry-internal-api-client`;
|
|
112
|
+
* services without a `-api` suffix fall back to
|
|
113
|
+
* `<serviceId>-internal-client`.
|
|
114
|
+
* - operator: same rule with `operator`.
|
|
115
|
+
*
|
|
116
|
+
* Deterministic for every serviceId — no name guessing, pure string rule.
|
|
117
|
+
*/
|
|
118
|
+
export function surfaceClientShortName(
|
|
119
|
+
serviceId: string,
|
|
120
|
+
surface: ApiSurface,
|
|
121
|
+
): string {
|
|
122
|
+
if (surface === ApiSurface.Public) {
|
|
123
|
+
return `${serviceId}-client`;
|
|
124
|
+
}
|
|
125
|
+
const segment = surface; // 'internal' | 'operator'
|
|
126
|
+
if (serviceId.endsWith('-api')) {
|
|
127
|
+
const base = serviceId.slice(0, -'-api'.length);
|
|
128
|
+
return `${base}-${segment}-api-client`;
|
|
129
|
+
}
|
|
130
|
+
return `${serviceId}-${segment}-client`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Scoped npm package name for a surface client. */
|
|
134
|
+
export function surfaceClientPackageName(
|
|
135
|
+
serviceId: string,
|
|
136
|
+
surface: ApiSurface,
|
|
137
|
+
): string {
|
|
138
|
+
return `@xemahq/${surfaceClientShortName(serviceId, surface)}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Absolute client package directory under `<repoRoot>/packages/clients/`. */
|
|
142
|
+
export function surfaceClientDir(
|
|
143
|
+
ctx: ResolvedServiceContext,
|
|
144
|
+
surface: ApiSurface,
|
|
145
|
+
): string {
|
|
146
|
+
const dirName = surfaceClientShortName(ctx.api.serviceId, surface).replace(
|
|
147
|
+
/-client$/,
|
|
148
|
+
'',
|
|
149
|
+
);
|
|
150
|
+
return join(ctx.repoRoot, 'packages', 'clients', dirName);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Absolute path of a surface's OpenAPI document at the service root. */
|
|
154
|
+
export function surfaceSpecPath(
|
|
155
|
+
ctx: ResolvedServiceContext,
|
|
156
|
+
surface: ApiSurface,
|
|
157
|
+
/**
|
|
158
|
+
* Where to write instead of the service directory.
|
|
159
|
+
*
|
|
160
|
+
* Exists so a drift GATE can extract without touching the working tree. Its
|
|
161
|
+
* sibling `check-client-spec-drift.mjs` states the requirement plainly: a
|
|
162
|
+
* check that regenerates in place and reads `git status` cannot be run by an
|
|
163
|
+
* author mid-change without destroying their work, so nobody runs it. The
|
|
164
|
+
* same has to be true of code→spec drift, and that is only possible if the
|
|
165
|
+
* extractor can be pointed elsewhere.
|
|
166
|
+
*/
|
|
167
|
+
outDir?: string,
|
|
168
|
+
): string {
|
|
169
|
+
return join(outDir ?? ctx.serviceDir, `openapi.${surface}.json`);
|
|
170
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the NestJS runtime classes from the SERVICE's own `node_modules`,
|
|
3
|
+
* not the generator's.
|
|
4
|
+
*
|
|
5
|
+
* The generator is symlinked into each service as a workspace package; a static
|
|
6
|
+
* `import … from '@nestjs/core'` would resolve to the generator's own copy of
|
|
7
|
+
* Nest, producing a dual-package hazard — the `DiscoveryService` token built
|
|
8
|
+
* into the app (service's Nest) would differ from the token we pass to
|
|
9
|
+
* `app.get(...)` (generator's Nest), and DI resolution fails. Resolving these
|
|
10
|
+
* from `serviceDir` guarantees one Nest instance end-to-end.
|
|
11
|
+
*
|
|
12
|
+
* (The decorator-metadata scan in `@xemahq/xema-decorators` is NOT subject to
|
|
13
|
+
* this: it reads via the global `Reflect` polyfill and compares string keys,
|
|
14
|
+
* so the generator's copy reads metadata stamped by the service's copy fine.)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { INestApplication, Type } from '@nestjs/common';
|
|
18
|
+
import type {
|
|
19
|
+
DiscoveryService,
|
|
20
|
+
MetadataScanner,
|
|
21
|
+
ModulesContainer,
|
|
22
|
+
} from '@nestjs/core';
|
|
23
|
+
import type { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
|
24
|
+
import type { Test } from '@nestjs/testing';
|
|
25
|
+
|
|
26
|
+
export interface ServiceNest {
|
|
27
|
+
readonly Test: typeof Test;
|
|
28
|
+
readonly DocumentBuilder: typeof DocumentBuilder;
|
|
29
|
+
readonly SwaggerModule: typeof SwaggerModule;
|
|
30
|
+
readonly DiscoveryService: Type<DiscoveryService>;
|
|
31
|
+
readonly MetadataScanner: Type<MetadataScanner>;
|
|
32
|
+
readonly ModulesContainer: Type<ModulesContainer>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function requireFrom(
|
|
36
|
+
serviceDir: string,
|
|
37
|
+
request: string,
|
|
38
|
+
): Record<string, unknown> {
|
|
39
|
+
const resolved = require.resolve(request, { paths: [serviceDir] });
|
|
40
|
+
|
|
41
|
+
return require(resolved) as Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function loadServiceNest(serviceDir: string): ServiceNest {
|
|
45
|
+
const core = requireFrom(serviceDir, '@nestjs/core');
|
|
46
|
+
const testing = requireFrom(serviceDir, '@nestjs/testing');
|
|
47
|
+
const swagger = requireFrom(serviceDir, '@nestjs/swagger');
|
|
48
|
+
return {
|
|
49
|
+
Test: testing.Test as typeof Test,
|
|
50
|
+
DocumentBuilder: swagger.DocumentBuilder as typeof DocumentBuilder,
|
|
51
|
+
SwaggerModule: swagger.SwaggerModule as typeof SwaggerModule,
|
|
52
|
+
DiscoveryService: core.DiscoveryService as Type<DiscoveryService>,
|
|
53
|
+
MetadataScanner: core.MetadataScanner as Type<MetadataScanner>,
|
|
54
|
+
ModulesContainer: core.ModulesContainer as Type<ModulesContainer>,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type { INestApplication };
|