@workos/oagen-emitters 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.
Files changed (110) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/.oxfmtrc.json +8 -1
  3. package/.release-please-manifest.json +1 -1
  4. package/CHANGELOG.md +15 -0
  5. package/README.md +129 -0
  6. package/dist/index.d.mts +10 -1
  7. package/dist/index.d.mts.map +1 -1
  8. package/dist/index.mjs +11943 -2728
  9. package/dist/index.mjs.map +1 -1
  10. package/docs/sdk-architecture/go.md +338 -0
  11. package/docs/sdk-architecture/php.md +315 -0
  12. package/docs/sdk-architecture/python.md +511 -0
  13. package/oagen.config.ts +298 -2
  14. package/package.json +9 -5
  15. package/scripts/generate-php.js +13 -0
  16. package/scripts/git-push-with-published-oagen.sh +21 -0
  17. package/smoke/sdk-dotnet.ts +17 -3
  18. package/smoke/sdk-elixir.ts +17 -3
  19. package/smoke/sdk-go.ts +137 -46
  20. package/smoke/sdk-kotlin.ts +23 -4
  21. package/smoke/sdk-node.ts +15 -3
  22. package/smoke/sdk-php.ts +28 -26
  23. package/smoke/sdk-python.ts +5 -2
  24. package/smoke/sdk-ruby.ts +17 -3
  25. package/smoke/sdk-rust.ts +16 -3
  26. package/src/go/client.ts +141 -0
  27. package/src/go/enums.ts +196 -0
  28. package/src/go/fixtures.ts +212 -0
  29. package/src/go/index.ts +81 -0
  30. package/src/go/manifest.ts +36 -0
  31. package/src/go/models.ts +254 -0
  32. package/src/go/naming.ts +191 -0
  33. package/src/go/resources.ts +827 -0
  34. package/src/go/tests.ts +751 -0
  35. package/src/go/type-map.ts +82 -0
  36. package/src/go/wrappers.ts +261 -0
  37. package/src/index.ts +3 -0
  38. package/src/node/client.ts +167 -122
  39. package/src/node/enums.ts +13 -4
  40. package/src/node/errors.ts +42 -233
  41. package/src/node/field-plan.ts +726 -0
  42. package/src/node/fixtures.ts +15 -5
  43. package/src/node/index.ts +65 -16
  44. package/src/node/models.ts +264 -96
  45. package/src/node/naming.ts +52 -25
  46. package/src/node/resources.ts +621 -172
  47. package/src/node/sdk-errors.ts +41 -0
  48. package/src/node/tests.ts +71 -27
  49. package/src/node/type-map.ts +4 -2
  50. package/src/node/utils.ts +56 -64
  51. package/src/node/wrappers.ts +151 -0
  52. package/src/php/client.ts +171 -0
  53. package/src/php/enums.ts +67 -0
  54. package/src/php/errors.ts +9 -0
  55. package/src/php/fixtures.ts +181 -0
  56. package/src/php/index.ts +96 -0
  57. package/src/php/manifest.ts +36 -0
  58. package/src/php/models.ts +310 -0
  59. package/src/php/naming.ts +298 -0
  60. package/src/php/resources.ts +561 -0
  61. package/src/php/tests.ts +533 -0
  62. package/src/php/type-map.ts +90 -0
  63. package/src/php/utils.ts +18 -0
  64. package/src/php/wrappers.ts +151 -0
  65. package/src/python/client.ts +337 -0
  66. package/src/python/enums.ts +313 -0
  67. package/src/python/fixtures.ts +196 -0
  68. package/src/python/index.ts +95 -0
  69. package/src/python/manifest.ts +38 -0
  70. package/src/python/models.ts +688 -0
  71. package/src/python/naming.ts +209 -0
  72. package/src/python/resources.ts +1322 -0
  73. package/src/python/tests.ts +1335 -0
  74. package/src/python/type-map.ts +93 -0
  75. package/src/python/wrappers.ts +191 -0
  76. package/src/shared/model-utils.ts +255 -0
  77. package/src/shared/naming-utils.ts +107 -0
  78. package/src/shared/non-spec-services.ts +54 -0
  79. package/src/shared/resolved-ops.ts +109 -0
  80. package/src/shared/wrapper-utils.ts +59 -0
  81. package/test/go/client.test.ts +92 -0
  82. package/test/go/enums.test.ts +132 -0
  83. package/test/go/errors.test.ts +9 -0
  84. package/test/go/models.test.ts +265 -0
  85. package/test/go/resources.test.ts +408 -0
  86. package/test/go/tests.test.ts +143 -0
  87. package/test/node/client.test.ts +199 -94
  88. package/test/node/enums.test.ts +75 -3
  89. package/test/node/errors.test.ts +2 -41
  90. package/test/node/models.test.ts +109 -20
  91. package/test/node/naming.test.ts +37 -4
  92. package/test/node/resources.test.ts +662 -30
  93. package/test/node/serializers.test.ts +36 -7
  94. package/test/node/type-map.test.ts +11 -0
  95. package/test/php/client.test.ts +94 -0
  96. package/test/php/enums.test.ts +173 -0
  97. package/test/php/errors.test.ts +9 -0
  98. package/test/php/models.test.ts +497 -0
  99. package/test/php/resources.test.ts +644 -0
  100. package/test/php/tests.test.ts +118 -0
  101. package/test/python/client.test.ts +200 -0
  102. package/test/python/enums.test.ts +228 -0
  103. package/test/python/errors.test.ts +16 -0
  104. package/test/python/manifest.test.ts +74 -0
  105. package/test/python/models.test.ts +716 -0
  106. package/test/python/resources.test.ts +617 -0
  107. package/test/python/tests.test.ts +202 -0
  108. package/src/node/common.ts +0 -273
  109. package/src/node/config.ts +0 -71
  110. package/src/node/serializers.ts +0 -744
@@ -1,5 +1,6 @@
1
1
  import type { ApiSpec, AuthScheme, EmitterContext, GeneratedFile, Service } from '@workos/oagen';
2
- import { fileName, serviceDirName, servicePropertyName, resolveInterfaceName, wireInterfaceName } from './naming.js';
2
+ import { collectReferencedNames } from '@workos/oagen';
3
+ import { fileName, resolveServiceDir, servicePropertyName, resolveInterfaceName, wireInterfaceName } from './naming.js';
3
4
  import {
4
5
  docComment,
5
6
  createServiceDirResolver,
@@ -15,9 +16,7 @@ export function generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFil
15
16
  files.push(generateWorkOSClient(spec, ctx));
16
17
  files.push(...generateServiceBarrels(spec, ctx));
17
18
  files.push(generateBarrel(spec, ctx));
18
- files.push(generateWorkerBarrel(spec, ctx));
19
- files.push(generatePackageJson(ctx));
20
- files.push(generateTsConfig());
19
+ // worker barrel, package.json, tsconfig.json are now hand-maintained in the target SDK
21
20
 
22
21
  return files;
23
22
  }
@@ -46,7 +45,7 @@ function generateWorkOSClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
46
45
  for (const service of spec.services) {
47
46
  if (coveredServices.has(service.name)) continue;
48
47
  const resolvedName = resolveResourceClassName(service, ctx);
49
- const serviceDir = serviceDirName(resolvedName);
48
+ const serviceDir = resolveServiceDir(resolvedName);
50
49
  lines.push(`import { ${resolvedName} } from './${serviceDir}/${fileName(resolvedName)}';`);
51
50
  }
52
51
 
@@ -99,7 +98,11 @@ function generateWorkOSClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
99
98
 
100
99
  lines.push('}');
101
100
 
102
- return { path: 'src/workos.ts', content: lines.join('\n'), skipIfExists: true };
101
+ return {
102
+ path: 'src/workos.ts',
103
+ content: lines.join('\n'),
104
+ skipIfExists: true,
105
+ };
103
106
  }
104
107
 
105
108
  /**
@@ -124,47 +127,36 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
124
127
  // exports a name (e.g., AuditLogSchema from create-audit-log-schema-options),
125
128
  // the generated model with the same name must be skipped to prevent the
126
129
  // merger from adding a duplicate `export *` that causes TS2308.
130
+ //
131
+ // Also track baseline file stems per directory so we can detect when the
132
+ // barrel needs updating with new export lines (see hasNewExports below).
133
+ const dirSymbolsFromBaseline = new Map<string, Set<string>>();
134
+ const seedFromBaseline = (sourceFile: string, name: string) => {
135
+ const match = sourceFile.match(/^src\/([^/]+)\/interfaces\/(.+)\.ts$/);
136
+ if (!match) return;
137
+ const dirName = match[1];
138
+ const fileStem = match[2];
139
+ if (!dirSymbols.has(dirName)) dirSymbols.set(dirName, new Set());
140
+ dirSymbols.get(dirName)!.add(name);
141
+ if (!dirSymbolsFromBaseline.has(dirName)) dirSymbolsFromBaseline.set(dirName, new Set());
142
+ dirSymbolsFromBaseline.get(dirName)!.add(fileStem);
143
+ };
127
144
  if (ctx.apiSurface?.interfaces) {
128
145
  for (const [name, iface] of Object.entries(ctx.apiSurface.interfaces)) {
129
146
  const sourceFile = (iface as any).sourceFile as string | undefined;
130
- if (!sourceFile) continue;
131
- // Match paths like "src/audit-logs/interfaces/foo.interface.ts" to directory "audit-logs"
132
- const match = sourceFile.match(/^src\/([^/]+)\/interfaces\//);
133
- if (match) {
134
- const dirName = match[1];
135
- if (!dirSymbols.has(dirName)) {
136
- dirSymbols.set(dirName, new Set());
137
- }
138
- dirSymbols.get(dirName)!.add(name);
139
- }
147
+ if (sourceFile) seedFromBaseline(sourceFile, name);
140
148
  }
141
149
  }
142
150
  if (ctx.apiSurface?.enums) {
143
151
  for (const [name, enumDef] of Object.entries(ctx.apiSurface.enums)) {
144
152
  const sourceFile = (enumDef as any).sourceFile as string | undefined;
145
- if (!sourceFile) continue;
146
- const match = sourceFile.match(/^src\/([^/]+)\/interfaces\//);
147
- if (match) {
148
- const dirName = match[1];
149
- if (!dirSymbols.has(dirName)) {
150
- dirSymbols.set(dirName, new Set());
151
- }
152
- dirSymbols.get(dirName)!.add(name);
153
- }
153
+ if (sourceFile) seedFromBaseline(sourceFile, name);
154
154
  }
155
155
  }
156
156
  if (ctx.apiSurface?.typeAliases) {
157
157
  for (const [name, alias] of Object.entries(ctx.apiSurface.typeAliases)) {
158
158
  const sourceFile = (alias as any).sourceFile as string | undefined;
159
- if (!sourceFile) continue;
160
- const match = sourceFile.match(/^src\/([^/]+)\/interfaces\//);
161
- if (match) {
162
- const dirName = match[1];
163
- if (!dirSymbols.has(dirName)) {
164
- dirSymbols.set(dirName, new Set());
165
- }
166
- dirSymbols.get(dirName)!.add(name);
167
- }
159
+ if (sourceFile) seedFromBaseline(sourceFile, name);
168
160
  }
169
161
  }
170
162
 
@@ -183,8 +175,12 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
183
175
  // Models -> service directories
184
176
  // Skip list wrapper and list metadata models — they use shared List<T>/ListMetadata
185
177
  // from common utils, so no per-resource interface file is generated.
178
+ // Also skip unreachable models — oagen only passes service-referenced models
179
+ // to generateModels, so unreachable models have no interface file to export.
180
+ const barrelReachable = collectReferencedNames(spec.services, spec.models);
186
181
  for (const model of spec.models) {
187
182
  if (isListMetadataModel(model) || isListWrapperModel(model)) continue;
183
+ if (!barrelReachable.models.has(model.name)) continue;
188
184
  const service = modelToService.get(model.name);
189
185
  const dirName = resolveDir(service);
190
186
  if (!dirExports.has(dirName)) {
@@ -236,14 +232,57 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
236
232
  }
237
233
 
238
234
  for (const [dirName, exports] of dirExports) {
239
- // Deduplicate (an enum and model could theoretically share a file name)
240
- const uniqueExports = [...new Set(exports)];
235
+ const exportSet = new Set(exports);
236
+
237
+ // When integrating into an existing SDK, include baseline exports from
238
+ // the api-surface so the barrel is comprehensive. This ensures stale
239
+ // entries (e.g., renamed files from previous generations) are removed
240
+ // when overwriteExisting replaces the barrel.
241
+ if (ctx.apiSurface) {
242
+ const addBaselineExports = (items: Record<string, any> | undefined) => {
243
+ if (!items) return;
244
+ for (const item of Object.values(items)) {
245
+ const sourceFile = (item as any).sourceFile as string | undefined;
246
+ if (!sourceFile) continue;
247
+ const match = sourceFile.match(/^src\/([^/]+)\/interfaces\/(.+)\.ts$/);
248
+ if (match && match[1] === dirName) {
249
+ exportSet.add(`export * from './${match[2].replace(/\.ts$/, '')}';`);
250
+ }
251
+ }
252
+ };
253
+ addBaselineExports(ctx.apiSurface.interfaces);
254
+ addBaselineExports(ctx.apiSurface.typeAliases);
255
+ addBaselineExports(ctx.apiSurface.enums);
256
+ }
257
+
258
+ // Deduplicate and sort
259
+ const uniqueExports = [...exportSet];
241
260
  uniqueExports.sort();
242
- files.push({
243
- path: `src/${dirName}/interfaces/index.ts`,
244
- content: uniqueExports.join('\n'),
245
- skipIfExists: true,
246
- });
261
+
262
+ if (ctx.apiSurface) {
263
+ // Integration mode: overwrite the barrel so stale entries are removed.
264
+ files.push({
265
+ path: `src/${dirName}/interfaces/index.ts`,
266
+ content: uniqueExports.join('\n'),
267
+ overwriteExisting: true,
268
+ });
269
+ } else {
270
+ // Standalone generation: only update if there are new exports.
271
+ const baselineSymbols = dirSymbolsFromBaseline.get(dirName);
272
+ const hasNewExports = baselineSymbols
273
+ ? uniqueExports.some((exp) => {
274
+ const match = exp.match(/from '\.\/(.*?)'/);
275
+ if (!match) return false;
276
+ return !baselineSymbols.has(match[1]);
277
+ })
278
+ : false;
279
+
280
+ files.push({
281
+ path: `src/${dirName}/interfaces/index.ts`,
282
+ content: uniqueExports.join('\n'),
283
+ skipIfExists: !hasNewExports,
284
+ });
285
+ }
247
286
  }
248
287
 
249
288
  return files;
@@ -315,10 +354,68 @@ function generateBarrel(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
315
354
  // Track directories that have already been wildcard-exported
316
355
  const exportedDirs = new Set<string>();
317
356
 
357
+ // Pre-compute all names per interfaces directory (generated + baseline) to detect
358
+ // cross-directory conflicts before emitting any star exports. A star export is
359
+ // unsafe when two different directories export the same name (e.g., Factor in
360
+ // both mfa/interfaces and user-management/interfaces).
361
+ const dirAllNames = new Map<string, Set<string>>();
362
+ for (const service of spec.services) {
363
+ const iDir = resolveDir(service.name);
364
+ if (!dirAllNames.has(iDir)) dirAllNames.set(iDir, new Set());
365
+ const names = dirAllNames.get(iDir)!;
366
+ for (const model of spec.models) {
367
+ if (modelToService.get(model.name) !== service.name) continue;
368
+ if (isListMetadataModel(model) || isListWrapperModel(model)) continue;
369
+ names.add(resolveInterfaceName(model.name, ctx));
370
+ names.add(wireInterfaceName(resolveInterfaceName(model.name, ctx)));
371
+ }
372
+ }
373
+ // Add baseline names per directory
374
+ if (ctx.apiSurface?.interfaces) {
375
+ for (const [name, iface] of Object.entries(ctx.apiSurface.interfaces)) {
376
+ const sourceFile = (iface as any).sourceFile as string | undefined;
377
+ if (!sourceFile) continue;
378
+ const match = sourceFile.match(/^src\/([^/]+)\/interfaces\//);
379
+ if (match) {
380
+ const dirName = match[1];
381
+ if (!dirAllNames.has(dirName)) dirAllNames.set(dirName, new Set());
382
+ dirAllNames.get(dirName)!.add(name);
383
+ }
384
+ }
385
+ }
386
+ if (ctx.apiSurface?.typeAliases) {
387
+ for (const [name, alias] of Object.entries(ctx.apiSurface.typeAliases)) {
388
+ const sourceFile = (alias as any).sourceFile as string | undefined;
389
+ if (!sourceFile) continue;
390
+ const match = sourceFile.match(/^src\/([^/]+)\/interfaces\//);
391
+ if (match) {
392
+ const dirName = match[1];
393
+ if (!dirAllNames.has(dirName)) dirAllNames.set(dirName, new Set());
394
+ dirAllNames.get(dirName)!.add(name);
395
+ }
396
+ }
397
+ }
398
+ // Detect directories with cross-directory name conflicts
399
+ const unsafeStarDirs = new Set<string>();
400
+ const allDirEntries = [...dirAllNames.entries()];
401
+ for (let i = 0; i < allDirEntries.length; i++) {
402
+ for (let j = i + 1; j < allDirEntries.length; j++) {
403
+ const [dirA, namesA] = allDirEntries[i];
404
+ const [dirB, namesB] = allDirEntries[j];
405
+ for (const name of namesA) {
406
+ if (namesB.has(name)) {
407
+ unsafeStarDirs.add(dirA);
408
+ unsafeStarDirs.add(dirB);
409
+ break;
410
+ }
411
+ }
412
+ }
413
+ }
414
+
318
415
  // Per-service exports: service barrel + resource class
319
416
  for (const service of spec.services) {
320
417
  const resolvedName = resolveResourceClassName(service, ctx);
321
- const serviceDir = serviceDirName(resolvedName);
418
+ const serviceDir = resolveServiceDir(resolvedName);
322
419
  // The interfaces directory may differ from the resource class directory when
323
420
  // a service's class name is remapped (e.g., WebhooksEndpoints class lives in
324
421
  // webhooks-endpoints/ but its model interfaces live in webhooks/).
@@ -338,13 +435,25 @@ function generateBarrel(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
338
435
  return enumService === service.name;
339
436
  });
340
437
 
341
- // Check whether any model or enum in this service conflicts with existingSdkExports.
342
- // If so, fall back to individual exports to avoid shadowing hand-written types.
438
+ // Check whether any model or enum in this service conflicts with names already
439
+ // exported (from earlier star exports or the existing SDK baseline). If so, fall
440
+ // back to individual named exports to avoid duplicate-export TS2308 errors.
343
441
  const hasConflict =
344
- serviceModels.some((m) => existingSdkExports.has(resolveInterfaceName(m.name, ctx))) ||
345
- serviceEnums.some((e) => existingSdkExports.has(e.name));
346
-
347
- if ((serviceModels.length > 0 || serviceEnums.length > 0) && !exportedDirs.has(interfacesDir) && !hasConflict) {
442
+ serviceModels.some((m) => {
443
+ const name = resolveInterfaceName(m.name, ctx);
444
+ return existingSdkExports.has(name) || exportedNames.has(name) || exportedNames.has(wireInterfaceName(name));
445
+ }) || serviceEnums.some((e) => existingSdkExports.has(e.name) || exportedNames.has(e.name));
446
+
447
+ // Skip star export for covered services — their directory may have hand-written types
448
+ // (e.g., Factor in mfa/) that conflict with types in the covering service's directory.
449
+ const isCovered = coveredServicesBarrel.has(service.name);
450
+ if (
451
+ (serviceModels.length > 0 || serviceEnums.length > 0) &&
452
+ !exportedDirs.has(interfacesDir) &&
453
+ !hasConflict &&
454
+ !unsafeStarDirs.has(interfacesDir) &&
455
+ !isCovered
456
+ ) {
348
457
  exportedDirs.add(interfacesDir);
349
458
  lines.push(`export * from './${interfacesDir}/interfaces';`);
350
459
  // Track the individual names so they don't get re-exported below
@@ -383,7 +492,11 @@ function generateBarrel(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
383
492
  }
384
493
 
385
494
  // Unassigned models (common) — use barrel if any exist
386
- const unassignedModels = spec.models.filter((m) => !modelToService.has(m.name));
495
+ // Filter to reachable models only: oagen's generateAllFiles passes only
496
+ // service-referenced models to generateModels, so unreachable models
497
+ // never get interface files. Exporting them here would create broken imports.
498
+ const reachable = collectReferencedNames(spec.services, spec.models);
499
+ const unassignedModels = spec.models.filter((m) => !modelToService.has(m.name) && reachable.models.has(m.name));
387
500
  const commonEnums = spec.enums.filter((e) => {
388
501
  const enumService = findEnumService(e.name, spec.services);
389
502
  return !enumService;
@@ -446,20 +559,11 @@ function generateBarrel(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
446
559
  lines.push("export { WorkOS } from './workos';");
447
560
  }
448
561
 
449
- return { path: 'src/index.ts', content: lines.join('\n'), skipIfExists: true };
450
- }
451
-
452
- /**
453
- * Generate a worker-compatible barrel file that re-exports everything from
454
- * the main barrel. This keeps type exports in sync automatically.
455
- */
456
- function generateWorkerBarrel(_spec: ApiSpec, _ctx: EmitterContext): GeneratedFile {
457
- const lines: string[] = [];
458
-
459
- // Re-export everything from the main index — keeps type exports in sync
460
- lines.push("export * from './index';");
461
-
462
- return { path: 'src/index.worker.ts', content: lines.join('\n'), skipIfExists: true };
562
+ return {
563
+ path: 'src/index.ts',
564
+ content: lines.join('\n'),
565
+ skipIfExists: true,
566
+ };
463
567
  }
464
568
 
465
569
  function findEnumService(enumName: string, services: Service[]): string | undefined {
@@ -528,62 +632,3 @@ function serverConstName(description: string): string {
528
632
  .toUpperCase()
529
633
  );
530
634
  }
531
-
532
- function generatePackageJson(ctx: EmitterContext): GeneratedFile {
533
- const pkg = {
534
- name: `@${ctx.namespace}/sdk`,
535
- version: '0.0.0',
536
- type: 'module',
537
- main: 'src/index.ts',
538
- types: 'src/index.ts',
539
- exports: {
540
- '.': './src/index.ts',
541
- },
542
- scripts: {
543
- test: 'jest',
544
- build: 'tsc',
545
- },
546
- devDependencies: {
547
- typescript: '^5.0.0',
548
- jest: '^29.0.0',
549
- 'jest-fetch-mock': '^3.0.0',
550
- '@types/jest': '^29.0.0',
551
- 'ts-jest': '^29.0.0',
552
- },
553
- };
554
-
555
- return {
556
- path: 'package.json',
557
- content: JSON.stringify(pkg, null, 2),
558
- skipIfExists: true,
559
- integrateTarget: false,
560
- };
561
- }
562
-
563
- function generateTsConfig(): GeneratedFile {
564
- const config = {
565
- compilerOptions: {
566
- target: 'ES2020',
567
- module: 'CommonJS',
568
- lib: ['ES2020'],
569
- declaration: true,
570
- strict: true,
571
- exactOptionalPropertyTypes: true,
572
- esModuleInterop: true,
573
- skipLibCheck: true,
574
- forceConsistentCasingInFileNames: true,
575
- resolveJsonModule: true,
576
- outDir: './lib',
577
- rootDir: './src',
578
- },
579
- include: ['src/**/*'],
580
- exclude: ['node_modules', 'lib', '**/*.spec.ts'],
581
- };
582
-
583
- return {
584
- path: 'tsconfig.json',
585
- content: JSON.stringify(config, null, 2),
586
- skipIfExists: true,
587
- integrateTarget: false,
588
- };
589
- }
package/src/node/enums.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Enum, EmitterContext, GeneratedFile, Service } from '@workos/oagen';
2
- import { walkTypeRef } from '@workos/oagen';
3
- import { fileName, serviceDirName, buildServiceNameMap } from './naming.js';
2
+ import { toPascalCase, walkTypeRef } from '@workos/oagen';
3
+ import { fileName, resolveServiceDir, buildServiceNameMap } from './naming.js';
4
4
  import { docComment } from './utils.js';
5
5
 
6
6
  export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
@@ -9,7 +9,7 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
9
9
  const enumToService = assignEnumsToServices(enums, ctx.spec.services);
10
10
  const serviceNameMap = buildServiceNameMap(ctx.spec.services, ctx);
11
11
  const resolveDir = (irService: string | undefined) =>
12
- irService ? serviceDirName(serviceNameMap.get(irService) ?? irService) : 'common';
12
+ irService ? resolveServiceDir(serviceNameMap.get(irService) ?? irService) : 'common';
13
13
  const files: GeneratedFile[] = [];
14
14
 
15
15
  for (const enumDef of enums) {
@@ -19,6 +19,15 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
19
19
  // Check baseline surface for representation and values
20
20
  const baselineEnum = ctx.apiSurface?.enums?.[enumDef.name];
21
21
  const baselineAlias = ctx.apiSurface?.typeAliases?.[enumDef.name];
22
+ const generatedPath = `src/${dirName}/interfaces/${fileName(enumDef.name)}.interface.ts`;
23
+
24
+ // If the baseline already provides this enum from a different file (e.g., `.enum.ts`),
25
+ // skip generation to avoid duplicate exports from the same barrel.
26
+ const baselineSourceFile = (baselineEnum as any)?.sourceFile ?? (baselineAlias as any)?.sourceFile;
27
+ if (baselineSourceFile && baselineSourceFile !== generatedPath) {
28
+ continue;
29
+ }
30
+
22
31
  const lines: string[] = [];
23
32
 
24
33
  // Track whether the generated content has new values not in the baseline.
@@ -41,7 +50,7 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
41
50
  // Append new values from the spec that the baseline is missing
42
51
  for (const val of missingValues) {
43
52
  // Derive a PascalCase member name from the value
44
- const memberName = val.replace(/[^a-zA-Z0-9]+/g, '');
53
+ const memberName = toPascalCase(val);
45
54
  lines.push(` ${memberName} = '${val}',`);
46
55
  }
47
56
  lines.push('}');