@workos/oagen-emitters 0.2.1 → 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 (103) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/.release-please-manifest.json +1 -1
  3. package/CHANGELOG.md +8 -0
  4. package/README.md +129 -0
  5. package/dist/index.d.mts +10 -1
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +11893 -3226
  8. package/dist/index.mjs.map +1 -1
  9. package/docs/sdk-architecture/go.md +338 -0
  10. package/docs/sdk-architecture/php.md +315 -0
  11. package/docs/sdk-architecture/python.md +511 -0
  12. package/oagen.config.ts +298 -2
  13. package/package.json +9 -5
  14. package/scripts/generate-php.js +13 -0
  15. package/scripts/git-push-with-published-oagen.sh +21 -0
  16. package/smoke/sdk-go.ts +116 -42
  17. package/smoke/sdk-php.ts +28 -26
  18. package/smoke/sdk-python.ts +5 -2
  19. package/src/go/client.ts +141 -0
  20. package/src/go/enums.ts +196 -0
  21. package/src/go/fixtures.ts +212 -0
  22. package/src/go/index.ts +81 -0
  23. package/src/go/manifest.ts +36 -0
  24. package/src/go/models.ts +254 -0
  25. package/src/go/naming.ts +191 -0
  26. package/src/go/resources.ts +827 -0
  27. package/src/go/tests.ts +751 -0
  28. package/src/go/type-map.ts +82 -0
  29. package/src/go/wrappers.ts +261 -0
  30. package/src/index.ts +3 -0
  31. package/src/node/client.ts +78 -115
  32. package/src/node/enums.ts +9 -0
  33. package/src/node/errors.ts +37 -232
  34. package/src/node/field-plan.ts +726 -0
  35. package/src/node/fixtures.ts +9 -1
  36. package/src/node/index.ts +2 -9
  37. package/src/node/models.ts +178 -21
  38. package/src/node/naming.ts +49 -111
  39. package/src/node/resources.ts +374 -364
  40. package/src/node/sdk-errors.ts +41 -0
  41. package/src/node/tests.ts +32 -12
  42. package/src/node/type-map.ts +4 -2
  43. package/src/node/utils.ts +13 -71
  44. package/src/node/wrappers.ts +151 -0
  45. package/src/php/client.ts +171 -0
  46. package/src/php/enums.ts +67 -0
  47. package/src/php/errors.ts +9 -0
  48. package/src/php/fixtures.ts +181 -0
  49. package/src/php/index.ts +96 -0
  50. package/src/php/manifest.ts +36 -0
  51. package/src/php/models.ts +310 -0
  52. package/src/php/naming.ts +298 -0
  53. package/src/php/resources.ts +561 -0
  54. package/src/php/tests.ts +533 -0
  55. package/src/php/type-map.ts +90 -0
  56. package/src/php/utils.ts +18 -0
  57. package/src/php/wrappers.ts +151 -0
  58. package/src/python/client.ts +337 -0
  59. package/src/python/enums.ts +313 -0
  60. package/src/python/fixtures.ts +196 -0
  61. package/src/python/index.ts +95 -0
  62. package/src/python/manifest.ts +38 -0
  63. package/src/python/models.ts +688 -0
  64. package/src/python/naming.ts +209 -0
  65. package/src/python/resources.ts +1322 -0
  66. package/src/python/tests.ts +1335 -0
  67. package/src/python/type-map.ts +93 -0
  68. package/src/python/wrappers.ts +191 -0
  69. package/src/shared/model-utils.ts +255 -0
  70. package/src/shared/naming-utils.ts +107 -0
  71. package/src/shared/non-spec-services.ts +54 -0
  72. package/src/shared/resolved-ops.ts +109 -0
  73. package/src/shared/wrapper-utils.ts +59 -0
  74. package/test/go/client.test.ts +92 -0
  75. package/test/go/enums.test.ts +132 -0
  76. package/test/go/errors.test.ts +9 -0
  77. package/test/go/models.test.ts +265 -0
  78. package/test/go/resources.test.ts +408 -0
  79. package/test/go/tests.test.ts +143 -0
  80. package/test/node/client.test.ts +18 -12
  81. package/test/node/enums.test.ts +2 -0
  82. package/test/node/errors.test.ts +2 -41
  83. package/test/node/models.test.ts +2 -0
  84. package/test/node/naming.test.ts +23 -0
  85. package/test/node/resources.test.ts +99 -69
  86. package/test/node/serializers.test.ts +3 -1
  87. package/test/node/type-map.test.ts +11 -0
  88. package/test/php/client.test.ts +94 -0
  89. package/test/php/enums.test.ts +173 -0
  90. package/test/php/errors.test.ts +9 -0
  91. package/test/php/models.test.ts +497 -0
  92. package/test/php/resources.test.ts +644 -0
  93. package/test/php/tests.test.ts +118 -0
  94. package/test/python/client.test.ts +200 -0
  95. package/test/python/enums.test.ts +228 -0
  96. package/test/python/errors.test.ts +16 -0
  97. package/test/python/manifest.test.ts +74 -0
  98. package/test/python/models.test.ts +716 -0
  99. package/test/python/resources.test.ts +617 -0
  100. package/test/python/tests.test.ts +202 -0
  101. package/src/node/common.ts +0 -273
  102. package/src/node/config.ts +0 -71
  103. package/src/node/serializers.ts +0 -746
@@ -0,0 +1,82 @@
1
+ import type { TypeRef, PrimitiveType, UnionType } from '@workos/oagen';
2
+ import { mapTypeRef as irMapTypeRef } from '@workos/oagen';
3
+ import { className } from './naming.js';
4
+
5
+ /**
6
+ * Map an IR TypeRef to a Go type string.
7
+ */
8
+ export function mapTypeRef(ref: TypeRef, asPointer = false): string {
9
+ const base = irMapTypeRef<string>(ref, {
10
+ primitive: mapPrimitive,
11
+ array: (_ref, items) => `[]${items}`,
12
+ model: (r) => `*${className(r.name)}`,
13
+ enum: (r) => className(r.name),
14
+ union: (_r, variants) => joinUnionVariants(_r, variants),
15
+ nullable: (_ref, inner) => {
16
+ // If inner is already a pointer type (model), don't double-pointer
17
+ if (inner.startsWith('*')) return inner;
18
+ return `*${inner}`;
19
+ },
20
+ literal: (r) => {
21
+ if (r.value === null) return 'interface{}';
22
+ if (typeof r.value === 'string') return 'string';
23
+ if (typeof r.value === 'number') return Number.isInteger(r.value) ? 'int' : 'float64';
24
+ if (typeof r.value === 'boolean') return 'bool';
25
+ return 'interface{}';
26
+ },
27
+ map: (_ref, value) => `map[string]${value}`,
28
+ });
29
+ if (asPointer && !base.startsWith('*') && !base.startsWith('[]') && !base.startsWith('map[')) {
30
+ return `*${base}`;
31
+ }
32
+ return base;
33
+ }
34
+
35
+ /**
36
+ * Map an IR TypeRef to a Go type string without pointer wrapping for models.
37
+ * Used for response type references where we don't want a double pointer.
38
+ */
39
+ export function mapTypeRefValue(ref: TypeRef): string {
40
+ return irMapTypeRef<string>(ref, {
41
+ primitive: mapPrimitive,
42
+ array: (_ref, items) => `[]${items}`,
43
+ model: (r) => className(r.name),
44
+ enum: (r) => className(r.name),
45
+ union: (_r, variants) => joinUnionVariants(_r, variants),
46
+ nullable: (_ref, inner) => `*${inner}`,
47
+ literal: (r) => {
48
+ if (r.value === null) return 'interface{}';
49
+ if (typeof r.value === 'string') return 'string';
50
+ if (typeof r.value === 'number') return Number.isInteger(r.value) ? 'int' : 'float64';
51
+ if (typeof r.value === 'boolean') return 'bool';
52
+ return 'interface{}';
53
+ },
54
+ map: (_ref, value) => `map[string]${value}`,
55
+ });
56
+ }
57
+
58
+ function mapPrimitive(ref: PrimitiveType): string {
59
+ if (ref.format === 'binary') return '[]byte';
60
+ switch (ref.type) {
61
+ case 'string':
62
+ return 'string';
63
+ case 'integer':
64
+ return 'int';
65
+ case 'number':
66
+ return 'float64';
67
+ case 'boolean':
68
+ return 'bool';
69
+ case 'unknown':
70
+ return 'interface{}';
71
+ }
72
+ }
73
+
74
+ function joinUnionVariants(_ref: UnionType, variants: string[]): string {
75
+ if (_ref.compositionKind === 'allOf') {
76
+ return variants[0] ?? 'interface{}';
77
+ }
78
+ const unique = [...new Set(variants)];
79
+ if (unique.length === 1) return unique[0];
80
+ // Go doesn't have union types; use interface{}
81
+ return 'interface{}';
82
+ }
@@ -0,0 +1,261 @@
1
+ import type { EmitterContext, ResolvedOperation, ResolvedWrapper } from '@workos/oagen';
2
+ import {
3
+ className as goClassName,
4
+ fieldName as goFieldName,
5
+ methodName as goMethodName,
6
+ unexportedName,
7
+ } from './naming.js';
8
+ import { sortPathParamsByTemplateOrder } from './resources.js';
9
+ import { resolveWrapperParams, formatWrapperDescription, type ResolvedWrapperParam } from '../shared/wrapper-utils.js';
10
+ import { lowerFirstForDoc, fieldDocComment } from '../shared/naming-utils.js';
11
+
12
+ /**
13
+ * Generate Go wrapper method lines for union split operations.
14
+ *
15
+ * Each wrapper is a typed convenience method that:
16
+ * - Accepts only the exposed params (not the full union body)
17
+ * - Injects constant defaults (e.g., grant_type)
18
+ * - Reads inferred fields from client config (e.g., client_id)
19
+ * - Delegates to the HTTP client with the constructed body
20
+ */
21
+ export function generateWrapperMethods(
22
+ serviceType: string,
23
+ resolvedOp: ResolvedOperation,
24
+ ctx: EmitterContext,
25
+ ): string[] {
26
+ if (!resolvedOp.wrappers || resolvedOp.wrappers.length === 0) return [];
27
+
28
+ const lines: string[] = [];
29
+
30
+ for (const wrapper of resolvedOp.wrappers) {
31
+ const wrapperParams = resolveWrapperParams(wrapper, ctx);
32
+ lines.push('');
33
+ emitWrapperParamsStruct(lines, wrapper, wrapperParams);
34
+ lines.push('');
35
+ emitWrapperMethod(lines, serviceType, resolvedOp, wrapper, wrapperParams);
36
+ }
37
+
38
+ return lines;
39
+ }
40
+
41
+ function emitWrapperParamsStruct(
42
+ lines: string[],
43
+ wrapper: ResolvedWrapper,
44
+ wrapperParams: ResolvedWrapperParam[],
45
+ ): void {
46
+ const structName = `${goMethodName(wrapper.name)}Params`;
47
+
48
+ lines.push(`// ${structName} contains the parameters for ${goMethodName(wrapper.name)}.`);
49
+ lines.push(`type ${structName} struct {`);
50
+
51
+ for (const { paramName, field, isOptional } of wrapperParams) {
52
+ const goField = goFieldName(paramName);
53
+ const goType = field ? resolveSimpleGoType(field.type) : 'string';
54
+
55
+ if (field?.description) {
56
+ const fdLines = field.description.split('\n').filter((l: string) => l.trim());
57
+ lines.push(`\t// ${fieldDocComment(goField, fdLines[0])}`);
58
+ for (let i = 1; i < fdLines.length; i++) {
59
+ lines.push(`\t// ${fdLines[i].trim()}`);
60
+ }
61
+ }
62
+ if (isOptional) {
63
+ const optType = goType.startsWith('*') || goType.startsWith('[]') ? goType : `*${goType}`;
64
+ lines.push(`\t${goField} ${optType} \`json:"${paramName},omitempty"\``);
65
+ } else {
66
+ lines.push(`\t${goField} ${goType} \`json:"${paramName}"\``);
67
+ }
68
+ }
69
+
70
+ lines.push('}');
71
+ }
72
+
73
+ function emitWrapperMethod(
74
+ lines: string[],
75
+ serviceType: string,
76
+ resolvedOp: ResolvedOperation,
77
+ wrapper: ResolvedWrapper,
78
+ wrapperParams: ResolvedWrapperParam[],
79
+ ): void {
80
+ const op = resolvedOp.operation;
81
+ const method = goMethodName(wrapper.name);
82
+ const paramsStruct = `${method}Params`;
83
+
84
+ // Return type
85
+ const responseType = wrapper.responseModelName ? goClassName(wrapper.responseModelName) : null;
86
+
87
+ // GoDoc
88
+ lines.push(`// ${method} ${formatWrapperDescription(wrapper.name)}.`);
89
+
90
+ // Signature
91
+ const sigParams: string[] = ['ctx context.Context'];
92
+
93
+ // Path params as positional args (sorted by template order)
94
+ for (const p of sortPathParamsByTemplateOrder(op)) {
95
+ sigParams.push(`${lowerFirstSafe(goFieldName(p.name))} ${resolveSimpleGoType(p.type)}`);
96
+ }
97
+
98
+ sigParams.push(`params *${paramsStruct}`);
99
+ sigParams.push('opts ...RequestOption');
100
+
101
+ if (responseType) {
102
+ lines.push(`func (s *${serviceType}) ${method}(${sigParams.join(', ')}) (*${responseType}, error) {`);
103
+ } else {
104
+ lines.push(`func (s *${serviceType}) ${method}(${sigParams.join(', ')}) error {`);
105
+ }
106
+
107
+ // Build body map with defaults + exposed params
108
+ lines.push('\tbody := map[string]interface{}{');
109
+
110
+ // Constant defaults (e.g., grant_type)
111
+ for (const [key, value] of Object.entries(wrapper.defaults)) {
112
+ lines.push(`\t\t"${key}": ${goLiteral(value)},`);
113
+ }
114
+
115
+ // Required exposed params
116
+ for (const { paramName, isOptional } of wrapperParams) {
117
+ if (isOptional) continue;
118
+ const goField = goFieldName(paramName);
119
+ lines.push(`\t\t"${paramName}": params.${goField},`);
120
+ }
121
+
122
+ lines.push('\t}');
123
+
124
+ // Inferred fields from client config
125
+ for (const field of wrapper.inferFromClient) {
126
+ const expr = clientFieldExpression(field);
127
+ lines.push(`\tif ${expr} != "" {`);
128
+ lines.push(`\t\tbody["${field}"] = ${expr}`);
129
+ lines.push('\t}');
130
+ }
131
+
132
+ // Optional exposed params
133
+ for (const { paramName, isOptional } of wrapperParams) {
134
+ if (!isOptional) continue;
135
+ const goField = goFieldName(paramName);
136
+ lines.push(`\tif params.${goField} != nil {`);
137
+ lines.push(`\t\tbody["${paramName}"] = *params.${goField}`);
138
+ lines.push('\t}');
139
+ }
140
+
141
+ // Build path expression
142
+ let pathExpr: string;
143
+ if (op.pathParams.length > 0) {
144
+ let fmtStr = op.path;
145
+ const fmtArgs: string[] = [];
146
+ for (const p of sortPathParamsByTemplateOrder(op)) {
147
+ fmtStr = fmtStr.replace(`{${p.name}}`, '%s');
148
+ fmtArgs.push(lowerFirstSafe(goFieldName(p.name)));
149
+ }
150
+ pathExpr = `fmt.Sprintf("${fmtStr}", ${fmtArgs.join(', ')})`;
151
+ } else {
152
+ pathExpr = `"${op.path}"`;
153
+ }
154
+
155
+ // Make the request
156
+ if (responseType) {
157
+ lines.push(`\tvar result ${responseType}`);
158
+ lines.push(
159
+ `\t_, err := s.client.request(ctx, "${op.httpMethod.toUpperCase()}", ${pathExpr}, nil, body, &result, opts)`,
160
+ );
161
+ lines.push('\tif err != nil {');
162
+ lines.push('\t\treturn nil, err');
163
+ lines.push('\t}');
164
+ lines.push('\treturn &result, nil');
165
+ } else {
166
+ lines.push(
167
+ `\t_, err := s.client.request(ctx, "${op.httpMethod.toUpperCase()}", ${pathExpr}, nil, body, nil, opts)`,
168
+ );
169
+ lines.push('\treturn err');
170
+ }
171
+
172
+ lines.push('}');
173
+ }
174
+
175
+ /** Convert a value to a Go literal. */
176
+ function goLiteral(value: string | number | boolean): string {
177
+ if (typeof value === 'string') return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
178
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
179
+ return String(value);
180
+ }
181
+
182
+ /** Get the Go expression for reading a client config field. */
183
+ function clientFieldExpression(field: string): string {
184
+ switch (field) {
185
+ case 'client_id':
186
+ return 's.client.clientID';
187
+ case 'client_secret':
188
+ return 's.client.apiKey';
189
+ default:
190
+ return `s.client.${lowerFirstSafe(goFieldName(field))}`;
191
+ }
192
+ }
193
+
194
+ /** Resolve a TypeRef to a simple Go type string. */
195
+ function resolveSimpleGoType(ref: any): string {
196
+ if (ref.kind === 'primitive') {
197
+ switch (ref.type) {
198
+ case 'string':
199
+ return 'string';
200
+ case 'integer':
201
+ return 'int';
202
+ case 'number':
203
+ return 'float64';
204
+ case 'boolean':
205
+ return 'bool';
206
+ default:
207
+ return 'interface{}';
208
+ }
209
+ }
210
+ if (ref.kind === 'nullable') return `*${resolveSimpleGoType(ref.inner)}`;
211
+ if (ref.kind === 'array') return `[]${resolveSimpleGoType(ref.items)}`;
212
+ if (ref.kind === 'model') return `*${goClassName(ref.name)}`;
213
+ if (ref.kind === 'enum') return goClassName(ref.name);
214
+ if (ref.kind === 'union') {
215
+ // For oneOf with a single non-null variant, use that variant's type
216
+ const nonNull = ref.variants.filter((v: any) => v.kind !== 'literal' || v.value !== null);
217
+ if (nonNull.length === 1) return resolveSimpleGoType(nonNull[0]);
218
+ return 'interface{}';
219
+ }
220
+ return 'interface{}';
221
+ }
222
+
223
+ /** Go reserved words set. */
224
+ const GO_RESERVED = new Set([
225
+ 'break',
226
+ 'case',
227
+ 'chan',
228
+ 'const',
229
+ 'continue',
230
+ 'default',
231
+ 'defer',
232
+ 'else',
233
+ 'fallthrough',
234
+ 'for',
235
+ 'func',
236
+ 'go',
237
+ 'goto',
238
+ 'if',
239
+ 'import',
240
+ 'interface',
241
+ 'map',
242
+ 'package',
243
+ 'range',
244
+ 'return',
245
+ 'select',
246
+ 'struct',
247
+ 'switch',
248
+ 'type',
249
+ 'var',
250
+ ]);
251
+
252
+ function lowerFirstSafe(s: string): string {
253
+ if (!s) return s;
254
+ const result = unexportedName(s);
255
+ if (GO_RESERVED.has(result)) return `${result}Param`;
256
+ return result;
257
+ }
258
+
259
+ function _lowerFirstField(s: string): string {
260
+ return lowerFirstForDoc(s);
261
+ }
package/src/index.ts CHANGED
@@ -1 +1,4 @@
1
1
  export { nodeEmitter } from './node/index.js';
2
+ export { pythonEmitter } from './python/index.js';
3
+ export { phpEmitter } from './php/index.js';
4
+ export { goEmitter } from './go/index.js';
@@ -1,4 +1,5 @@
1
1
  import type { ApiSpec, AuthScheme, EmitterContext, GeneratedFile, Service } from '@workos/oagen';
2
+ import { collectReferencedNames } from '@workos/oagen';
2
3
  import { fileName, resolveServiceDir, servicePropertyName, resolveInterfaceName, wireInterfaceName } from './naming.js';
3
4
  import {
4
5
  docComment,
@@ -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
  }
@@ -128,47 +127,36 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
128
127
  // exports a name (e.g., AuditLogSchema from create-audit-log-schema-options),
129
128
  // the generated model with the same name must be skipped to prevent the
130
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
+ };
131
144
  if (ctx.apiSurface?.interfaces) {
132
145
  for (const [name, iface] of Object.entries(ctx.apiSurface.interfaces)) {
133
146
  const sourceFile = (iface as any).sourceFile as string | undefined;
134
- if (!sourceFile) continue;
135
- // Match paths like "src/audit-logs/interfaces/foo.interface.ts" to directory "audit-logs"
136
- const match = sourceFile.match(/^src\/([^/]+)\/interfaces\//);
137
- if (match) {
138
- const dirName = match[1];
139
- if (!dirSymbols.has(dirName)) {
140
- dirSymbols.set(dirName, new Set());
141
- }
142
- dirSymbols.get(dirName)!.add(name);
143
- }
147
+ if (sourceFile) seedFromBaseline(sourceFile, name);
144
148
  }
145
149
  }
146
150
  if (ctx.apiSurface?.enums) {
147
151
  for (const [name, enumDef] of Object.entries(ctx.apiSurface.enums)) {
148
152
  const sourceFile = (enumDef as any).sourceFile as string | undefined;
149
- if (!sourceFile) continue;
150
- const match = sourceFile.match(/^src\/([^/]+)\/interfaces\//);
151
- if (match) {
152
- const dirName = match[1];
153
- if (!dirSymbols.has(dirName)) {
154
- dirSymbols.set(dirName, new Set());
155
- }
156
- dirSymbols.get(dirName)!.add(name);
157
- }
153
+ if (sourceFile) seedFromBaseline(sourceFile, name);
158
154
  }
159
155
  }
160
156
  if (ctx.apiSurface?.typeAliases) {
161
157
  for (const [name, alias] of Object.entries(ctx.apiSurface.typeAliases)) {
162
158
  const sourceFile = (alias as any).sourceFile as string | undefined;
163
- if (!sourceFile) continue;
164
- const match = sourceFile.match(/^src\/([^/]+)\/interfaces\//);
165
- if (match) {
166
- const dirName = match[1];
167
- if (!dirSymbols.has(dirName)) {
168
- dirSymbols.set(dirName, new Set());
169
- }
170
- dirSymbols.get(dirName)!.add(name);
171
- }
159
+ if (sourceFile) seedFromBaseline(sourceFile, name);
172
160
  }
173
161
  }
174
162
 
@@ -187,8 +175,12 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
187
175
  // Models -> service directories
188
176
  // Skip list wrapper and list metadata models — they use shared List<T>/ListMetadata
189
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);
190
181
  for (const model of spec.models) {
191
182
  if (isListMetadataModel(model) || isListWrapperModel(model)) continue;
183
+ if (!barrelReachable.models.has(model.name)) continue;
192
184
  const service = modelToService.get(model.name);
193
185
  const dirName = resolveDir(service);
194
186
  if (!dirExports.has(dirName)) {
@@ -240,14 +232,57 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
240
232
  }
241
233
 
242
234
  for (const [dirName, exports] of dirExports) {
243
- // Deduplicate (an enum and model could theoretically share a file name)
244
- 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];
245
260
  uniqueExports.sort();
246
- files.push({
247
- path: `src/${dirName}/interfaces/index.ts`,
248
- content: uniqueExports.join('\n'),
249
- skipIfExists: true,
250
- });
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
+ }
251
286
  }
252
287
 
253
288
  return files;
@@ -457,7 +492,11 @@ function generateBarrel(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
457
492
  }
458
493
 
459
494
  // Unassigned models (common) — use barrel if any exist
460
- 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));
461
500
  const commonEnums = spec.enums.filter((e) => {
462
501
  const enumService = findEnumService(e.name, spec.services);
463
502
  return !enumService;
@@ -527,23 +566,6 @@ function generateBarrel(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
527
566
  };
528
567
  }
529
568
 
530
- /**
531
- * Generate a worker-compatible barrel file that re-exports everything from
532
- * the main barrel. This keeps type exports in sync automatically.
533
- */
534
- function generateWorkerBarrel(_spec: ApiSpec, _ctx: EmitterContext): GeneratedFile {
535
- const lines: string[] = [];
536
-
537
- // Re-export everything from the main index — keeps type exports in sync
538
- lines.push("export * from './index';");
539
-
540
- return {
541
- path: 'src/index.worker.ts',
542
- content: lines.join('\n'),
543
- skipIfExists: true,
544
- };
545
- }
546
-
547
569
  function findEnumService(enumName: string, services: Service[]): string | undefined {
548
570
  for (const service of services) {
549
571
  for (const op of service.operations) {
@@ -610,62 +632,3 @@ function serverConstName(description: string): string {
610
632
  .toUpperCase()
611
633
  );
612
634
  }
613
-
614
- function generatePackageJson(ctx: EmitterContext): GeneratedFile {
615
- const pkg = {
616
- name: `@${ctx.namespace}/sdk`,
617
- version: '0.0.0',
618
- type: 'module',
619
- main: 'src/index.ts',
620
- types: 'src/index.ts',
621
- exports: {
622
- '.': './src/index.ts',
623
- },
624
- scripts: {
625
- test: 'jest',
626
- build: 'tsc',
627
- },
628
- devDependencies: {
629
- typescript: '^5.0.0',
630
- jest: '^29.0.0',
631
- 'jest-fetch-mock': '^3.0.0',
632
- '@types/jest': '^29.0.0',
633
- 'ts-jest': '^29.0.0',
634
- },
635
- };
636
-
637
- return {
638
- path: 'package.json',
639
- content: JSON.stringify(pkg, null, 2),
640
- skipIfExists: true,
641
- integrateTarget: false,
642
- };
643
- }
644
-
645
- function generateTsConfig(): GeneratedFile {
646
- const config = {
647
- compilerOptions: {
648
- target: 'ES2020',
649
- module: 'CommonJS',
650
- lib: ['ES2020'],
651
- declaration: true,
652
- strict: true,
653
- exactOptionalPropertyTypes: true,
654
- esModuleInterop: true,
655
- skipLibCheck: true,
656
- forceConsistentCasingInFileNames: true,
657
- resolveJsonModule: true,
658
- outDir: './lib',
659
- rootDir: './src',
660
- },
661
- include: ['src/**/*'],
662
- exclude: ['node_modules', 'lib', '**/*.spec.ts'],
663
- };
664
-
665
- return {
666
- path: 'tsconfig.json',
667
- content: JSON.stringify(config, null, 2),
668
- skipIfExists: true,
669
- integrateTarget: false,
670
- };
671
- }
package/src/node/enums.ts CHANGED
@@ -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.