@workos/oagen-emitters 0.12.1 → 0.12.3
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/.github/workflows/ci.yml +1 -1
- package/.github/workflows/lint-pr-title.yml +1 -1
- package/.github/workflows/lint.yml +1 -1
- package/.github/workflows/release-please.yml +2 -2
- package/.github/workflows/release.yml +1 -1
- package/.node-version +1 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +14 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{plugin-CmfzawTp.mjs → plugin-D2N2ZT5W.mjs} +2566 -1493
- package/dist/plugin-D2N2ZT5W.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +6 -6
- package/renovate.json +46 -6
- package/src/node/client.ts +19 -32
- package/src/node/enums.ts +67 -30
- package/src/node/errors.ts +2 -8
- package/src/node/field-plan.ts +188 -52
- package/src/node/fixtures.ts +11 -33
- package/src/node/index.ts +354 -20
- package/src/node/live-surface.ts +378 -0
- package/src/node/models.ts +547 -351
- package/src/node/naming.ts +122 -25
- package/src/node/node-overrides.ts +77 -0
- package/src/node/options.ts +41 -0
- package/src/node/path-expression.ts +11 -4
- package/src/node/resources.ts +473 -48
- package/src/node/sdk-errors.ts +0 -16
- package/src/node/tests.ts +152 -93
- package/src/node/type-map.ts +40 -18
- package/src/node/utils.ts +89 -102
- package/src/node/wrappers.ts +0 -20
- package/test/node/client.test.ts +106 -1201
- package/test/node/enums.test.ts +59 -130
- package/test/node/errors.test.ts +2 -3
- package/test/node/live-surface.test.ts +240 -0
- package/test/node/models.test.ts +396 -765
- package/test/node/naming.test.ts +69 -234
- package/test/node/resources.test.ts +435 -2025
- package/test/node/tests.test.ts +214 -0
- package/test/node/type-map.test.ts +49 -54
- package/test/node/utils.test.ts +29 -80
- package/dist/plugin-CmfzawTp.mjs.map +0 -1
- package/test/node/serializers.test.ts +0 -444
package/src/node/index.ts
CHANGED
|
@@ -12,58 +12,394 @@ import * as fs from 'node:fs';
|
|
|
12
12
|
import * as path from 'node:path';
|
|
13
13
|
|
|
14
14
|
import { generateModelsAndSerializers } from './models.js';
|
|
15
|
-
import { generateEnums } from './enums.js';
|
|
16
|
-
import { generateResources } from './resources.js';
|
|
15
|
+
import { generateEnums as generateEnumFiles } from './enums.js';
|
|
16
|
+
import { generateResources, resolveResourceClassName, resolveResourceDir } from './resources.js';
|
|
17
17
|
import { generateClient } from './client.js';
|
|
18
|
-
import {
|
|
18
|
+
import { generateTests as generateTestFiles } from './tests.js';
|
|
19
|
+
import { buildLiveSurface, emptyLiveSurface, setActiveLiveSurface, type LiveSurface } from './live-surface.js';
|
|
20
|
+
import {
|
|
21
|
+
setBaselineSerializedNames,
|
|
22
|
+
setBaselineInterfaceNames,
|
|
23
|
+
setAdoptedModelNames,
|
|
24
|
+
resolveInterfaceName,
|
|
25
|
+
} from './naming.js';
|
|
26
|
+
import { withNodeOperationOverrides } from './node-overrides.js';
|
|
27
|
+
import { isNodeOwnedService, nodeOptions } from './options.js';
|
|
28
|
+
import { setInlineEnumUnions, setDomainNameResolver } from './type-map.js';
|
|
29
|
+
import { groupByMount } from '../shared/resolved-ops.js';
|
|
30
|
+
import { assignModelsToServices, createServiceDirResolver } from './utils.js';
|
|
31
|
+
import { fileName } from './naming.js';
|
|
19
32
|
|
|
20
|
-
|
|
21
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Cache live-surface per ctx — every emitter method receives the same ctx in
|
|
35
|
+
* one oagen run, so we walk the target SDK once and reuse it.
|
|
36
|
+
*/
|
|
37
|
+
const surfaceCache = new WeakMap<EmitterContext, LiveSurface>();
|
|
22
38
|
|
|
23
|
-
|
|
24
|
-
|
|
39
|
+
function getSurface(ctx: EmitterContext): LiveSurface {
|
|
40
|
+
let surface = surfaceCache.get(ctx);
|
|
41
|
+
if (surface) return surface;
|
|
42
|
+
// Prefer --output (where we are writing) over --target (rare for node).
|
|
43
|
+
// The literal command `--output <SDK>` makes outputDir the SDK root.
|
|
44
|
+
const root = ctx.outputDir ?? ctx.targetDir;
|
|
45
|
+
surface = root ? buildLiveSurface(root) : emptyLiveSurface();
|
|
46
|
+
if (root) markPriorManifestAutogen(surface, root, ctx.priorTargetManifestPaths);
|
|
47
|
+
surfaceCache.set(ctx, surface);
|
|
48
|
+
setActiveLiveSurface(surface);
|
|
49
|
+
|
|
50
|
+
// Wire the type-map's resolver so field-type references (`schema: X`) and
|
|
51
|
+
// import statements (`import type { X }`) agree on the same name. The
|
|
52
|
+
// structural baseline match in `resolveInterfaceName` can map an IR name
|
|
53
|
+
// to a different live-SDK name (e.g. `AuditLogSchemaJson` → baseline
|
|
54
|
+
// `AuditLogSchemaResponse` via `overlayLookup.modelNameByIR`).
|
|
55
|
+
setDomainNameResolver((irName: string) => resolveInterfaceName(irName, ctx));
|
|
56
|
+
|
|
57
|
+
// Tell `naming.wireInterfaceName` which `Serialized*` interfaces exist in
|
|
58
|
+
// the live SDK so generated imports use the legacy name where applicable.
|
|
59
|
+
// Source from the api-surface JSON (richer) when present, else fall back
|
|
60
|
+
// to the disk walk.
|
|
61
|
+
const serialized = new Set<string>();
|
|
62
|
+
const ifaces = ctx.apiSurface?.interfaces;
|
|
63
|
+
if (ifaces) {
|
|
64
|
+
for (const name of Object.keys(ifaces)) {
|
|
65
|
+
if (name.startsWith('Serialized')) serialized.add(name);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const name of surface.interfaces.keys()) {
|
|
69
|
+
if (name.startsWith('Serialized')) serialized.add(name);
|
|
70
|
+
}
|
|
71
|
+
setBaselineSerializedNames(serialized);
|
|
72
|
+
|
|
73
|
+
// Full set of baseline interface names — used by `wireInterfaceName` to
|
|
74
|
+
// detect when a `Response`-suffixed name has no `*Wire` companion in
|
|
75
|
+
// the live SDK and is therefore the single-form wire interface.
|
|
76
|
+
const allInterfaces = new Set<string>();
|
|
77
|
+
if (ifaces) {
|
|
78
|
+
for (const name of Object.keys(ifaces)) allInterfaces.add(name);
|
|
79
|
+
}
|
|
80
|
+
for (const name of surface.interfaces.keys()) allInterfaces.add(name);
|
|
81
|
+
setBaselineInterfaceNames(allInterfaces);
|
|
82
|
+
|
|
83
|
+
// Inline-enum optimization is intentionally disabled. workos-node emits the
|
|
84
|
+
// dual `const X = {...} as const; type X = ...` pattern so callers can use
|
|
85
|
+
// members at runtime (e.g. `GenerateLinkIntent.SSO`). Inlining the type
|
|
86
|
+
// would drop the enum's file but leave value references in hand-written
|
|
87
|
+
// test files dangling — see admin-portal.spec.ts referencing `.SSO`.
|
|
88
|
+
// Pass an empty map; type-map will fall back to emitting the symbol name.
|
|
89
|
+
setInlineEnumUnions(new Map());
|
|
90
|
+
setAdoptedModelNames(computeAdoptedModelNames(ctx, surface));
|
|
91
|
+
return surface;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Apply the live-surface filter to a batch of generated files.
|
|
96
|
+
*
|
|
97
|
+
* Three signals decide what happens to each `GeneratedFile`:
|
|
98
|
+
*
|
|
99
|
+
* 1. `@oagen-ignore-file` marker (`surface.protectedFiles`) — the user has
|
|
100
|
+
* taken ownership of the file, never write on top of it.
|
|
101
|
+
* 2. `auto-generated by oagen` header (`surface.autogenFiles`) — the file
|
|
102
|
+
* was produced by a prior generation. Spec changes (e.g. parameter
|
|
103
|
+
* renames) must propagate, so we leave the file in the output and let
|
|
104
|
+
* the engine's AST merger update it. No skip flags.
|
|
105
|
+
* 3. File on disk without either marker — treat as hand-written; drop from
|
|
106
|
+
* the output to avoid the engine prepending a header on `skipIfExists`
|
|
107
|
+
* files (see writeFiles:~4360 in @workos/oagen) or merging unrequested
|
|
108
|
+
* changes into it.
|
|
109
|
+
* 4. Brand-new path in an existing SDK — drop it. For a live SDK we treat
|
|
110
|
+
* the git-tracked baseline as the managed surface; generating new files
|
|
111
|
+
* is what caused the workos-node file explosion.
|
|
112
|
+
*
|
|
113
|
+
* `integrateTarget: false` files (smoke-manifest.json etc.) are also dropped:
|
|
114
|
+
* with no `--target` step they would otherwise land as untracked cruft.
|
|
115
|
+
*
|
|
116
|
+
* Note: pairing this with `--no-prune` is required for stable behavior — see
|
|
117
|
+
* `scripts/sdk-generate.sh` in the spec repo, which enables it for `--lang node`.
|
|
118
|
+
*/
|
|
119
|
+
/**
|
|
120
|
+
* `*.spec.ts`, `*.test.ts`, and JSON fixtures under `fixtures/` are owned by
|
|
121
|
+
* the test author after first emission. The emitter scaffolds them on a
|
|
122
|
+
* brand-new resource, but subsequent regenerations must leave them alone —
|
|
123
|
+
* assertions get hand-tuned, fixture data gets stabilized, and re-emission
|
|
124
|
+
* would point them at sibling serializers/interfaces that may have shape-
|
|
125
|
+
* shifted under the user's feet.
|
|
126
|
+
*/
|
|
127
|
+
function isUserOwnedAfterFirstEmit(relPath: string): boolean {
|
|
128
|
+
if (relPath.endsWith('.spec.ts') || relPath.endsWith('.test.ts')) return true;
|
|
129
|
+
if (/\/fixtures\/[^/]+\.json$/.test(relPath)) return true;
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface LiveSurfacePolicy {
|
|
134
|
+
managedPaths: Set<string>;
|
|
135
|
+
hasExistingSdk: boolean;
|
|
136
|
+
adoptedServiceDirs: Set<string>;
|
|
137
|
+
ownedServiceDirs: Set<string>;
|
|
138
|
+
oagenOwnedDirs: Set<string>;
|
|
139
|
+
regenerateOwnedTests: boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function managedPathsFor(ctx: EmitterContext, surface: LiveSurface): Set<string> {
|
|
143
|
+
const managedPaths = new Set(surface.trackedFiles.size > 0 ? surface.trackedFiles : surface.files);
|
|
144
|
+
for (const relPath of ctx.priorTargetManifestPaths ?? []) {
|
|
145
|
+
if (relPath.startsWith('src/')) managedPaths.add(relPath);
|
|
146
|
+
}
|
|
147
|
+
return managedPaths;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function markPriorManifestAutogen(
|
|
151
|
+
surface: LiveSurface,
|
|
152
|
+
root: string,
|
|
153
|
+
priorManifestPaths: Set<string> | undefined,
|
|
154
|
+
): void {
|
|
155
|
+
if (!priorManifestPaths) return;
|
|
156
|
+
|
|
157
|
+
for (const relPath of priorManifestPaths) {
|
|
158
|
+
if (!relPath.startsWith('src/')) continue;
|
|
159
|
+
if (!surface.files.has(relPath)) continue;
|
|
160
|
+
if (surface.protectedFiles.has(relPath)) continue;
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const text = fs.readFileSync(path.join(root, relPath), 'utf8');
|
|
164
|
+
if (/auto-generated by oagen/i.test(text.slice(0, 400))) {
|
|
165
|
+
surface.autogenFiles.add(relPath);
|
|
166
|
+
extractManifestFunctions(text, relPath, surface);
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
// File disappeared between surface walk and read; ignore it.
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const MANIFEST_FUNCTION_RE = /^\s*export\s+(?:async\s+)?function\s+([a-zA-Z_$][\w$]*)/gm;
|
|
175
|
+
const MANIFEST_CONST_FN_RE =
|
|
176
|
+
/^\s*export\s+const\s+([a-zA-Z_$][\w$]*)\s*(?::\s*[^=]+)?=\s*(?:async\s+)?(?:<[^>]*>\s*)?\(/gm;
|
|
177
|
+
|
|
178
|
+
function extractManifestFunctions(text: string, relPath: string, surface: LiveSurface): void {
|
|
179
|
+
for (const match of text.matchAll(MANIFEST_FUNCTION_RE)) {
|
|
180
|
+
surface.functions.set(match[1], relPath);
|
|
181
|
+
}
|
|
182
|
+
for (const match of text.matchAll(MANIFEST_CONST_FN_RE)) {
|
|
183
|
+
surface.functions.set(match[1], relPath);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function buildLiveSurfacePolicy(ctx: EmitterContext, surface: LiveSurface): LiveSurfacePolicy {
|
|
188
|
+
const managedPaths = managedPathsFor(ctx, surface);
|
|
189
|
+
const hasExistingSdk = managedPaths.size > 0;
|
|
190
|
+
const adoptedServiceDirs = nodeOptions(ctx).adoptMissingServices
|
|
191
|
+
? computeAdoptedServiceDirs(ctx, surface)
|
|
192
|
+
: new Set<string>();
|
|
193
|
+
const ownedServiceDirs = computeOwnedServiceDirs(ctx);
|
|
194
|
+
for (const dir of ownedServiceDirs) {
|
|
195
|
+
for (const relPath of surface.files) {
|
|
196
|
+
if (topLevelDir(relPath) === dir) managedPaths.add(relPath);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
managedPaths,
|
|
202
|
+
hasExistingSdk,
|
|
203
|
+
adoptedServiceDirs,
|
|
204
|
+
ownedServiceDirs,
|
|
205
|
+
oagenOwnedDirs: topLevelDirs(surface.autogenFiles),
|
|
206
|
+
regenerateOwnedTests: nodeOptions(ctx).regenerateOwnedTests === true,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function computeOwnedServiceDirs(ctx: EmitterContext): Set<string> {
|
|
211
|
+
const dirs = new Set<string>();
|
|
212
|
+
if ((nodeOptions(ctx).ownedServices ?? []).length === 0) return dirs;
|
|
213
|
+
|
|
214
|
+
const mountGroups = groupByMount(ctx);
|
|
215
|
+
const services =
|
|
216
|
+
mountGroups.size > 0
|
|
217
|
+
? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
|
|
218
|
+
: ctx.spec.services;
|
|
219
|
+
const { resolveDir } = createServiceDirResolver(ctx.spec.models, ctx.spec.services, ctx);
|
|
220
|
+
|
|
221
|
+
for (const service of services) {
|
|
222
|
+
const resourceName = resolveResourceClassName(service, ctx);
|
|
223
|
+
if (!isNodeOwnedService(ctx, service.name, resourceName)) continue;
|
|
224
|
+
dirs.add(resolveResourceDir(service, ctx));
|
|
225
|
+
dirs.add(resolveDir(service.name));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return dirs;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function computeAdoptedServiceDirs(ctx: EmitterContext, surface: LiveSurface): Set<string> {
|
|
232
|
+
const dirs = new Set<string>();
|
|
233
|
+
const mountGroups = groupByMount(ctx);
|
|
234
|
+
const services =
|
|
235
|
+
mountGroups.size > 0
|
|
236
|
+
? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
|
|
237
|
+
: ctx.spec.services;
|
|
238
|
+
const { resolveDir } = createServiceDirResolver(ctx.spec.models, ctx.spec.services, ctx);
|
|
239
|
+
|
|
240
|
+
for (const service of services) {
|
|
241
|
+
if (service.operations.length === 0) continue;
|
|
242
|
+
|
|
243
|
+
const resourceName = resolveResourceClassName(service, ctx);
|
|
244
|
+
if (surface.classes.has(resourceName) || ctx.apiSurface?.classes?.[resourceName]) continue;
|
|
245
|
+
|
|
246
|
+
const resourceDir = resolveResourceDir(service, ctx);
|
|
247
|
+
const resourcePath = `src/${resourceDir}/${fileName(resourceName)}.ts`;
|
|
248
|
+
if (surface.protectedFiles.has(resourcePath)) continue;
|
|
249
|
+
|
|
250
|
+
dirs.add(resourceDir);
|
|
251
|
+
dirs.add(resolveDir(service.name));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return dirs;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function computeAdoptedModelNames(ctx: EmitterContext, surface: LiveSurface): Set<string> {
|
|
258
|
+
if (!nodeOptions(ctx).adoptMissingServices) return new Set();
|
|
259
|
+
|
|
260
|
+
const adoptedServiceDirs = computeAdoptedServiceDirs(ctx, surface);
|
|
261
|
+
if (adoptedServiceDirs.size === 0) return new Set();
|
|
262
|
+
|
|
263
|
+
const modelToService = assignModelsToServices(ctx.spec.models, ctx.spec.services, ctx.modelHints);
|
|
264
|
+
const { resolveDir } = createServiceDirResolver(ctx.spec.models, ctx.spec.services, ctx);
|
|
265
|
+
const names = new Set<string>();
|
|
266
|
+
for (const model of ctx.spec.models) {
|
|
267
|
+
const dirName = resolveDir(modelToService.get(model.name));
|
|
268
|
+
if (adoptedServiceDirs.has(dirName)) names.add(model.name);
|
|
269
|
+
}
|
|
270
|
+
return names;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function topLevelDirs(paths: Set<string>): Set<string> {
|
|
274
|
+
const dirs = new Set<string>();
|
|
275
|
+
for (const relPath of paths) {
|
|
276
|
+
const dir = topLevelDir(relPath);
|
|
277
|
+
if (dir) dirs.add(dir);
|
|
278
|
+
}
|
|
279
|
+
return dirs;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function topLevelDir(relPath: string): string | undefined {
|
|
283
|
+
return relPath.match(/^src\/([^/]+)\//)?.[1];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function canCreateNewPath(relPath: string, policy: LiveSurfacePolicy): boolean {
|
|
287
|
+
const dir = topLevelDir(relPath);
|
|
288
|
+
if (!dir) return false;
|
|
289
|
+
return policy.adoptedServiceDirs.has(dir) || policy.ownedServiceDirs.has(dir) || policy.oagenOwnedDirs.has(dir);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function isOwnedPath(relPath: string, policy: LiveSurfacePolicy): boolean {
|
|
293
|
+
const dir = topLevelDir(relPath);
|
|
294
|
+
return dir !== undefined && policy.ownedServiceDirs.has(dir);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function applyLiveSurface(files: GeneratedFile[], ctx: EmitterContext, surface: LiveSurface): GeneratedFile[] {
|
|
298
|
+
const out: GeneratedFile[] = [];
|
|
299
|
+
const policy = buildLiveSurfacePolicy(ctx, surface);
|
|
25
300
|
for (const f of files) {
|
|
301
|
+
const ownedPath = isOwnedPath(f.path, policy);
|
|
302
|
+
if (f.integrateTarget === false) continue;
|
|
303
|
+
if (surface.protectedFiles.has(f.path)) continue;
|
|
304
|
+
if (policy.hasExistingSdk && !policy.managedPaths.has(f.path) && !canCreateNewPath(f.path, policy)) continue;
|
|
305
|
+
|
|
306
|
+
// Hand-written files (on disk, no `auto-generated by oagen` header) →
|
|
307
|
+
// drop. The engine would otherwise prepend the header on
|
|
308
|
+
// `skipIfExists: true` files (writeFiles:~4360), and the merger may
|
|
309
|
+
// try to splice generated symbols into hand-written ones.
|
|
310
|
+
if (surface.files.has(f.path) && !surface.autogenFiles.has(f.path) && !ownedPath) continue;
|
|
311
|
+
|
|
312
|
+
// Test specs and fixtures: author-owned after first emission. Once a
|
|
313
|
+
// test file or fixture exists on disk, the user owns it — the emitter
|
|
314
|
+
// re-emitting against new IR would either replace assertions/data the
|
|
315
|
+
// user has hand-tuned, or leave it pointing at sibling files that no
|
|
316
|
+
// longer have the same shape. Treat existing test/fixture files as
|
|
317
|
+
// frozen even when they carry the auto-gen header.
|
|
318
|
+
//
|
|
319
|
+
// Adopted-service directories are treated like owned dirs for this
|
|
320
|
+
// purpose: adoption means oagen created the directory from scratch, so
|
|
321
|
+
// by construction there is no hand-written content to preserve and
|
|
322
|
+
// emitting tests/fixtures is safe. Rule (a) still drops files that
|
|
323
|
+
// somehow exist hand-written.
|
|
324
|
+
if (isUserOwnedAfterFirstEmit(f.path)) {
|
|
325
|
+
const dir = topLevelDir(f.path);
|
|
326
|
+
const isAdoptedDir = dir !== undefined && policy.adoptedServiceDirs.has(dir);
|
|
327
|
+
const isManagedDir = ownedPath || isAdoptedDir;
|
|
328
|
+
if (surface.files.has(f.path) && !surface.autogenFiles.has(f.path)) continue;
|
|
329
|
+
if (!isManagedDir && !surface.autogenFiles.has(f.path)) continue;
|
|
330
|
+
if (isManagedDir && !policy.regenerateOwnedTests) continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Previously auto-generated files → fully overwrite so spec changes
|
|
334
|
+
// (e.g. parameter renames like `admin_emails` → `it_contact_emails`)
|
|
335
|
+
// propagate. The engine's default AST merger is additive and would
|
|
336
|
+
// leave the old name on existing methods. Files marked
|
|
337
|
+
// `@oagen-ignore-start`/`@oagen-ignore-end` regions inside the file
|
|
338
|
+
// are still preserved by `overwriteWithPreservedRegions` in the
|
|
339
|
+
// engine.
|
|
340
|
+
if (surface.autogenFiles.has(f.path) || ownedPath) {
|
|
341
|
+
f.overwriteExisting = true;
|
|
342
|
+
f.skipIfExists = false;
|
|
343
|
+
}
|
|
344
|
+
|
|
26
345
|
if (f.content && !f.content.endsWith('\n')) {
|
|
27
346
|
f.content += '\n';
|
|
28
347
|
}
|
|
348
|
+
out.push(f);
|
|
29
349
|
}
|
|
30
|
-
return
|
|
350
|
+
return out;
|
|
31
351
|
}
|
|
32
352
|
|
|
33
353
|
export const nodeEmitter: Emitter = {
|
|
34
354
|
language: 'node',
|
|
35
355
|
|
|
36
356
|
generateModels(models: Model[], ctx: EmitterContext): GeneratedFile[] {
|
|
37
|
-
|
|
357
|
+
const nodeCtx = withNodeOperationOverrides(ctx);
|
|
358
|
+
const surface = getSurface(nodeCtx);
|
|
359
|
+
return applyLiveSurface(generateModelsAndSerializers(models, nodeCtx), nodeCtx, surface);
|
|
38
360
|
},
|
|
39
361
|
|
|
40
362
|
generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
41
|
-
|
|
363
|
+
const nodeCtx = withNodeOperationOverrides(ctx);
|
|
364
|
+
const surface = getSurface(nodeCtx);
|
|
365
|
+
return applyLiveSurface(generateEnumFiles(enums, nodeCtx), nodeCtx, surface);
|
|
42
366
|
},
|
|
43
367
|
|
|
44
368
|
generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[] {
|
|
45
|
-
|
|
369
|
+
const nodeCtx = withNodeOperationOverrides(ctx);
|
|
370
|
+
const surface = getSurface(nodeCtx);
|
|
371
|
+
return applyLiveSurface(generateResources(services, nodeCtx), nodeCtx, surface);
|
|
46
372
|
},
|
|
47
373
|
|
|
48
374
|
generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
49
|
-
|
|
375
|
+
const nodeCtx = withNodeOperationOverrides(ctx);
|
|
376
|
+
const surface = getSurface(nodeCtx);
|
|
377
|
+
return applyLiveSurface(generateClient(spec, nodeCtx), nodeCtx, surface);
|
|
50
378
|
},
|
|
51
379
|
|
|
52
|
-
|
|
53
|
-
|
|
380
|
+
// workos-node ships its own exception hierarchy under src/common/exceptions/.
|
|
381
|
+
// Re-emitting them would either skip (if files exist) or overwrite hand-edits.
|
|
382
|
+
generateErrors(_ctx: EmitterContext): GeneratedFile[] {
|
|
383
|
+
return [];
|
|
54
384
|
},
|
|
55
385
|
|
|
56
386
|
generateTypeSignatures(_spec: ApiSpec, _ctx: EmitterContext): GeneratedFile[] {
|
|
57
|
-
// TypeScript uses inline types — no separate type signature files needed
|
|
58
387
|
return [];
|
|
59
388
|
},
|
|
60
389
|
|
|
390
|
+
// Test specs and fixtures are hand-maintained except for explicitly-owned
|
|
391
|
+
// service directories.
|
|
61
392
|
generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
62
|
-
|
|
393
|
+
const nodeCtx = withNodeOperationOverrides(ctx);
|
|
394
|
+
if (!nodeOptions(nodeCtx).regenerateOwnedTests) return [];
|
|
395
|
+
const surface = getSurface(nodeCtx);
|
|
396
|
+
return applyLiveSurface(generateTestFiles(spec, nodeCtx), nodeCtx, surface);
|
|
63
397
|
},
|
|
64
398
|
|
|
65
|
-
|
|
66
|
-
|
|
399
|
+
// No operations map needed — the manifest belongs to the staging+target flow,
|
|
400
|
+
// which the literal `--output <SDK>` command does not use.
|
|
401
|
+
buildOperationsMap(): Record<string, never> {
|
|
402
|
+
return {};
|
|
67
403
|
},
|
|
68
404
|
|
|
69
405
|
fileHeader(): string {
|
|
@@ -79,8 +415,6 @@ export const nodeEmitter: Emitter = {
|
|
|
79
415
|
fs.existsSync(path.join(targetDir, '.eslintrc.js'));
|
|
80
416
|
|
|
81
417
|
if (hasPrettier && hasEslint) {
|
|
82
|
-
// Chain ESLint autofix (e.g. unused-import removal) then prettier.
|
|
83
|
-
// ESLint errors are suppressed so formatting still runs on lint failure.
|
|
84
418
|
return {
|
|
85
419
|
cmd: 'bash',
|
|
86
420
|
args: [
|