@workos/oagen 0.0.1 → 0.1.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/README.md +1 -3
- package/dist/{chunk-P5XUDUGY.js → chunk-VUSRNIG2.js} +5 -92
- package/dist/cli/index.js +177 -466
- package/dist/compat/index.js +40 -2
- package/dist/index.d.ts +2 -25
- package/dist/index.js +122 -290
- package/dist/scripts/smoke/baseline.js +88 -26
- package/dist/smoke.js +88 -26
- package/dist/verify/index.js +1 -1
- package/docs/agents/architecture.md +1 -1
- package/docs/agents/emitters.md +2 -2
- package/docs/architecture/dependency-layers.md +1 -5
- package/docs/architecture/emitter-contract.md +13 -10
- package/docs/architecture/extractor-contract.md +30 -22
- package/docs/architecture/ir-types.md +2 -1
- package/docs/architecture/pipeline.md +1 -1
- package/docs/architecture/workflows.md +21 -20
- package/docs/cli.md +10 -14
- package/docs/contributor/public-api.md +2 -2
- package/package.json +1 -1
- package/skills/check-emitter-parity/SKILL.md +232 -0
package/dist/index.js
CHANGED
|
@@ -8,9 +8,8 @@ import {
|
|
|
8
8
|
getEmitter,
|
|
9
9
|
integrateGeneratedFiles,
|
|
10
10
|
mapFilesForTargetIntegration,
|
|
11
|
-
registerEmitter
|
|
12
|
-
|
|
13
|
-
} from "./chunk-P5XUDUGY.js";
|
|
11
|
+
registerEmitter
|
|
12
|
+
} from "./chunk-VUSRNIG2.js";
|
|
14
13
|
import {
|
|
15
14
|
ACRONYM_SET,
|
|
16
15
|
CommandError,
|
|
@@ -139,17 +138,26 @@ function buildInlineModel(name, schema) {
|
|
|
139
138
|
|
|
140
139
|
// src/parser/schemas.ts
|
|
141
140
|
function extractSchemas(schemas) {
|
|
142
|
-
const models = [];
|
|
143
141
|
const enums = [];
|
|
144
|
-
if (!schemas) return { models, enums };
|
|
142
|
+
if (!schemas) return { models: [], enums };
|
|
143
|
+
const modelsByCleanName = /* @__PURE__ */ new Map();
|
|
145
144
|
for (const [name, schema] of Object.entries(schemas)) {
|
|
146
145
|
const pascalName = cleanSchemaName(toPascalCase(name));
|
|
147
146
|
if (schema.enum) {
|
|
148
147
|
enums.push(extractEnum(pascalName, schema));
|
|
149
148
|
} else {
|
|
150
|
-
|
|
149
|
+
const model = extractModel(pascalName, schema, schemas);
|
|
150
|
+
const existing = modelsByCleanName.get(pascalName);
|
|
151
|
+
if (existing) {
|
|
152
|
+
if (model.fields.length > existing.fields.length) {
|
|
153
|
+
modelsByCleanName.set(pascalName, model);
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
modelsByCleanName.set(pascalName, model);
|
|
157
|
+
}
|
|
151
158
|
}
|
|
152
159
|
}
|
|
160
|
+
const models = [...modelsByCleanName.values()];
|
|
153
161
|
const modelNames = new Set(models.map((m) => m.name));
|
|
154
162
|
const inlineQueue = [...models];
|
|
155
163
|
while (inlineQueue.length > 0) {
|
|
@@ -240,9 +248,9 @@ function extractNestedSchema(name, schema) {
|
|
|
240
248
|
if (variant.properties && (variant.type === "object" || !variant.type)) {
|
|
241
249
|
const requiredSet = new Set(variant.required ?? []);
|
|
242
250
|
const fields = [];
|
|
243
|
-
for (const [fn,
|
|
244
|
-
if (!
|
|
245
|
-
fields.push(buildFieldFromSchema(fn,
|
|
251
|
+
for (const [fn, fs] of Object.entries(variant.properties)) {
|
|
252
|
+
if (!fs) continue;
|
|
253
|
+
fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
|
|
246
254
|
}
|
|
247
255
|
const variantName = models.length === 0 ? name : `${name}${models.length + 1}`;
|
|
248
256
|
models.push({ name: variantName, description: variant.description, fields });
|
|
@@ -253,18 +261,18 @@ function extractNestedSchema(name, schema) {
|
|
|
253
261
|
if (schema.properties && (schema.type === "object" || !schema.type)) {
|
|
254
262
|
const requiredSet = new Set(schema.required ?? []);
|
|
255
263
|
const fields = [];
|
|
256
|
-
for (const [fn,
|
|
257
|
-
if (!
|
|
258
|
-
fields.push(buildFieldFromSchema(fn,
|
|
264
|
+
for (const [fn, fs] of Object.entries(schema.properties)) {
|
|
265
|
+
if (!fs) continue;
|
|
266
|
+
fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
|
|
259
267
|
}
|
|
260
268
|
return [{ name, description: schema.description, fields }];
|
|
261
269
|
}
|
|
262
270
|
if (schema.type === "array" && schema.items?.properties) {
|
|
263
271
|
const requiredSet = new Set(schema.items.required ?? []);
|
|
264
272
|
const fields = [];
|
|
265
|
-
for (const [fn,
|
|
266
|
-
if (!
|
|
267
|
-
fields.push(buildFieldFromSchema(fn,
|
|
273
|
+
for (const [fn, fs] of Object.entries(schema.items.properties)) {
|
|
274
|
+
if (!fs) continue;
|
|
275
|
+
fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
|
|
268
276
|
}
|
|
269
277
|
return [{ name, description: schema.items.description, fields }];
|
|
270
278
|
}
|
|
@@ -579,6 +587,18 @@ function classifyAndExtractResponse(schema, contextName) {
|
|
|
579
587
|
}
|
|
580
588
|
return extractDirectResource(schema, contextName);
|
|
581
589
|
}
|
|
590
|
+
var KNOWN_DATA_PATHS = /* @__PURE__ */ new Set(["data", "items", "results", "records", "entries", "values", "nodes", "edges"]);
|
|
591
|
+
var PAGINATION_METADATA_PATTERNS = [
|
|
592
|
+
"metadata",
|
|
593
|
+
"pagination",
|
|
594
|
+
"cursor",
|
|
595
|
+
"has_more",
|
|
596
|
+
"page_info",
|
|
597
|
+
"total",
|
|
598
|
+
"next_page",
|
|
599
|
+
"previous_page",
|
|
600
|
+
"offset"
|
|
601
|
+
];
|
|
582
602
|
function detectListEnvelope(schema) {
|
|
583
603
|
const propSources = [];
|
|
584
604
|
if (schema.allOf) {
|
|
@@ -595,17 +615,26 @@ function detectListEnvelope(schema) {
|
|
|
595
615
|
Object.assign(mergedProps, source);
|
|
596
616
|
}
|
|
597
617
|
const arrayProps = [];
|
|
598
|
-
|
|
618
|
+
const nonArrayKeys = [];
|
|
599
619
|
for (const [key, propSchema] of Object.entries(mergedProps)) {
|
|
600
620
|
if (!propSchema) continue;
|
|
601
621
|
if (propSchema.type === "array") {
|
|
602
622
|
arrayProps.push(key);
|
|
603
623
|
} else {
|
|
604
|
-
|
|
624
|
+
nonArrayKeys.push(key);
|
|
605
625
|
}
|
|
606
626
|
}
|
|
607
|
-
if (arrayProps.length === 1 &&
|
|
608
|
-
|
|
627
|
+
if (arrayProps.length === 1 && nonArrayKeys.length >= 1) {
|
|
628
|
+
const arrayPropName = arrayProps[0];
|
|
629
|
+
if (KNOWN_DATA_PATHS.has(arrayPropName)) {
|
|
630
|
+
return { isEnvelope: true, dataPath: arrayPropName };
|
|
631
|
+
}
|
|
632
|
+
const hasPaginationCompanion = nonArrayKeys.some(
|
|
633
|
+
(key) => PAGINATION_METADATA_PATTERNS.some((pattern) => key.toLowerCase().includes(pattern))
|
|
634
|
+
);
|
|
635
|
+
if (hasPaginationCompanion) {
|
|
636
|
+
return { isEnvelope: true, dataPath: arrayPropName };
|
|
637
|
+
}
|
|
609
638
|
}
|
|
610
639
|
return { isEnvelope: false, dataPath: null };
|
|
611
640
|
}
|
|
@@ -870,15 +899,15 @@ function extractOperations(paths, operationIdTransform) {
|
|
|
870
899
|
if (!paths) return { services: [], inlineModels: [] };
|
|
871
900
|
const serviceMap = /* @__PURE__ */ new Map();
|
|
872
901
|
const inlineModels = [];
|
|
873
|
-
for (const [
|
|
902
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
874
903
|
const pathLevelParams = pathItem.parameters ?? [];
|
|
875
904
|
for (const method of HTTP_METHODS) {
|
|
876
905
|
const op = pathItem[method];
|
|
877
906
|
if (!op) continue;
|
|
878
|
-
const serviceName = inferServiceName(
|
|
907
|
+
const serviceName = inferServiceName(path, op.tags?.[0]);
|
|
879
908
|
const { operation, inlineModels: opModels } = buildOperation(
|
|
880
909
|
method,
|
|
881
|
-
|
|
910
|
+
path,
|
|
882
911
|
op,
|
|
883
912
|
pathLevelParams,
|
|
884
913
|
operationIdTransform
|
|
@@ -898,28 +927,28 @@ function extractOperations(paths, operationIdTransform) {
|
|
|
898
927
|
}));
|
|
899
928
|
return { services, inlineModels };
|
|
900
929
|
}
|
|
901
|
-
function inferServiceName(
|
|
930
|
+
function inferServiceName(path, tag) {
|
|
902
931
|
if (tag) {
|
|
903
932
|
return toPascalCase(tag);
|
|
904
933
|
}
|
|
905
|
-
const segments =
|
|
934
|
+
const segments = path.split("/").filter(Boolean);
|
|
906
935
|
const first = segments[0] ?? "Default";
|
|
907
936
|
if (first.startsWith("{")) return "Default";
|
|
908
937
|
return toPascalCase(first);
|
|
909
938
|
}
|
|
910
|
-
function inferOperationName(method,
|
|
939
|
+
function inferOperationName(method, path, operationId, operationIdTransform) {
|
|
911
940
|
if (operationId) {
|
|
912
941
|
if (operationIdTransform) {
|
|
913
942
|
return operationIdTransform(operationId);
|
|
914
943
|
}
|
|
915
944
|
return toCamelCase(operationId);
|
|
916
945
|
}
|
|
917
|
-
const verb = inferVerb(method,
|
|
918
|
-
const resource = inferResourceFromPath(
|
|
946
|
+
const verb = inferVerb(method, path);
|
|
947
|
+
const resource = inferResourceFromPath(path);
|
|
919
948
|
return resource ? toCamelCase(`${verb}_${resource}`) : verb;
|
|
920
949
|
}
|
|
921
|
-
function inferVerb(method,
|
|
922
|
-
const segments =
|
|
950
|
+
function inferVerb(method, path) {
|
|
951
|
+
const segments = path.split("/").filter(Boolean);
|
|
923
952
|
const hasTrailingParam = segments.length > 0 && segments[segments.length - 1].startsWith("{");
|
|
924
953
|
switch (method) {
|
|
925
954
|
case "get":
|
|
@@ -941,8 +970,8 @@ function inferVerb(method, path2) {
|
|
|
941
970
|
return method;
|
|
942
971
|
}
|
|
943
972
|
}
|
|
944
|
-
function inferResourceFromPath(
|
|
945
|
-
const segments =
|
|
973
|
+
function inferResourceFromPath(path) {
|
|
974
|
+
const segments = path.split("/").filter(Boolean);
|
|
946
975
|
if (segments.length <= 1) return null;
|
|
947
976
|
let resourceSegment = null;
|
|
948
977
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
@@ -990,15 +1019,15 @@ function disambiguateOperationNames(operations) {
|
|
|
990
1019
|
}
|
|
991
1020
|
}
|
|
992
1021
|
}
|
|
993
|
-
function inferDeeperContext(
|
|
994
|
-
const segments =
|
|
1022
|
+
function inferDeeperContext(path) {
|
|
1023
|
+
const segments = path.split("/").filter(Boolean);
|
|
995
1024
|
const nonParams = segments.filter((s) => !s.startsWith("{"));
|
|
996
1025
|
if (nonParams.length >= 3) {
|
|
997
1026
|
return toPascalCase(nonParams.slice(1, -1).join("_"));
|
|
998
1027
|
}
|
|
999
1028
|
return null;
|
|
1000
1029
|
}
|
|
1001
|
-
function buildOperation(method,
|
|
1030
|
+
function buildOperation(method, path, op, pathLevelParams, operationIdTransform) {
|
|
1002
1031
|
const allParams = [...pathLevelParams, ...op.parameters ?? []];
|
|
1003
1032
|
const hasIdempotencyHeader = allParams.some((p) => p.in === "header" && p.name.toLowerCase() === "idempotency-key");
|
|
1004
1033
|
const pathParams = extractParams(allParams, "path");
|
|
@@ -1015,7 +1044,7 @@ function buildOperation(method, path2, op, pathLevelParams, operationIdTransform
|
|
|
1015
1044
|
isPaginated,
|
|
1016
1045
|
dataPath: responseDataPath,
|
|
1017
1046
|
itemType: responseItemType
|
|
1018
|
-
} = extractResponses(op.responses, op,
|
|
1047
|
+
} = extractResponses(op.responses, op, path, method);
|
|
1019
1048
|
inlineModels.push(...reqBodyModels);
|
|
1020
1049
|
const paginationFromParams = detectPagination(response, queryParams, responseDataPath);
|
|
1021
1050
|
let pagination;
|
|
@@ -1033,10 +1062,10 @@ function buildOperation(method, path2, op, pathLevelParams, operationIdTransform
|
|
|
1033
1062
|
}
|
|
1034
1063
|
return {
|
|
1035
1064
|
operation: {
|
|
1036
|
-
name: inferOperationName(method,
|
|
1065
|
+
name: inferOperationName(method, path, op.operationId, operationIdTransform),
|
|
1037
1066
|
description: buildDescription(op.summary, op.description),
|
|
1038
1067
|
httpMethod: method,
|
|
1039
|
-
path
|
|
1068
|
+
path,
|
|
1040
1069
|
pathParams,
|
|
1041
1070
|
queryParams,
|
|
1042
1071
|
headerParams,
|
|
@@ -1167,15 +1196,15 @@ function deriveOneOfVariantName(variant, baseName, existingModels) {
|
|
|
1167
1196
|
}
|
|
1168
1197
|
return name;
|
|
1169
1198
|
}
|
|
1170
|
-
function deriveResponseName(op,
|
|
1199
|
+
function deriveResponseName(op, path, method) {
|
|
1171
1200
|
if (op?.operationId) {
|
|
1172
1201
|
return cleanSchemaName(toPascalCase(op.operationId) + "Response");
|
|
1173
1202
|
}
|
|
1174
|
-
const segments =
|
|
1203
|
+
const segments = path.split("/").filter(Boolean);
|
|
1175
1204
|
const resource = segments[0] ?? "Unknown";
|
|
1176
1205
|
return toPascalCase(resource) + toPascalCase(method) + "Response";
|
|
1177
1206
|
}
|
|
1178
|
-
function extractResponses(responses, op,
|
|
1207
|
+
function extractResponses(responses, op, path, method) {
|
|
1179
1208
|
const errors = [];
|
|
1180
1209
|
let response = { kind: "primitive", type: "string" };
|
|
1181
1210
|
let inlineModels = [];
|
|
@@ -1190,7 +1219,7 @@ function extractResponses(responses, op, path2, method) {
|
|
|
1190
1219
|
let extractedType = { kind: "primitive", type: "unknown" };
|
|
1191
1220
|
const jsonContent = resp.content?.["application/json"];
|
|
1192
1221
|
if (jsonContent?.schema) {
|
|
1193
|
-
const contextName = deriveResponseName(op,
|
|
1222
|
+
const contextName = deriveResponseName(op, path ?? "/", method ?? "get");
|
|
1194
1223
|
const result = classifyAndExtractResponse(jsonContent.schema, contextName);
|
|
1195
1224
|
extractedType = result.response;
|
|
1196
1225
|
inlineModels = result.inlineModels;
|
|
@@ -1209,7 +1238,7 @@ function extractResponses(responses, op, path2, method) {
|
|
|
1209
1238
|
if (firstContent?.schema) {
|
|
1210
1239
|
extractedType = schemaToTypeRef(
|
|
1211
1240
|
firstContent.schema,
|
|
1212
|
-
deriveResponseName(op,
|
|
1241
|
+
deriveResponseName(op, path ?? "/", method ?? "get")
|
|
1213
1242
|
);
|
|
1214
1243
|
}
|
|
1215
1244
|
}
|
|
@@ -1242,20 +1271,24 @@ function keepLargestByName(models) {
|
|
|
1242
1271
|
}
|
|
1243
1272
|
return [...byName.values()];
|
|
1244
1273
|
}
|
|
1245
|
-
function
|
|
1274
|
+
function rewriteModelRef(ref, oldName, newName) {
|
|
1275
|
+
walkTypeRef(ref, {
|
|
1276
|
+
model: (r) => {
|
|
1277
|
+
if (r.name === oldName) r.name = newName;
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
function rewriteModelRefs(models, services, oldName, newName) {
|
|
1282
|
+
for (const model of models) {
|
|
1283
|
+
for (const field of model.fields) {
|
|
1284
|
+
rewriteModelRef(field.type, oldName, newName);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1246
1287
|
for (const service of services) {
|
|
1247
1288
|
for (const op of service.operations) {
|
|
1248
|
-
|
|
1249
|
-
model: (r) => {
|
|
1250
|
-
if (r.name === oldName) r.name = newName;
|
|
1251
|
-
}
|
|
1252
|
-
});
|
|
1289
|
+
rewriteModelRef(op.response, oldName, newName);
|
|
1253
1290
|
if (op.requestBody) {
|
|
1254
|
-
|
|
1255
|
-
model: (r) => {
|
|
1256
|
-
if (r.name === oldName) r.name = newName;
|
|
1257
|
-
}
|
|
1258
|
-
});
|
|
1291
|
+
rewriteModelRef(op.requestBody, oldName, newName);
|
|
1259
1292
|
}
|
|
1260
1293
|
}
|
|
1261
1294
|
}
|
|
@@ -1304,9 +1337,30 @@ function mergeFieldInlineModels(existingModels, fieldInlineModels) {
|
|
|
1304
1337
|
});
|
|
1305
1338
|
return [...existingModels, ...keepLargestByName(deduplicatedFieldModels)];
|
|
1306
1339
|
}
|
|
1340
|
+
function hasModelConstDiscriminant(model) {
|
|
1341
|
+
return model.fields.some(
|
|
1342
|
+
(f) => (f.name === "object" || f.name === "type") && f.type.kind === "literal" && typeof f.type.value === "string"
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
function isModelReferencedByOthers(name, models, excludeNames) {
|
|
1346
|
+
for (const model of models) {
|
|
1347
|
+
if (excludeNames.has(model.name)) continue;
|
|
1348
|
+
for (const field of model.fields) {
|
|
1349
|
+
let found = false;
|
|
1350
|
+
walkTypeRef(field.type, {
|
|
1351
|
+
model: (r) => {
|
|
1352
|
+
if (r.name === name) found = true;
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
if (found) return true;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
return false;
|
|
1359
|
+
}
|
|
1307
1360
|
function collapseJsonSuffixModels(models, services) {
|
|
1308
1361
|
const normalizedModels = [...models];
|
|
1309
1362
|
const byName = new Map(normalizedModels.map((m) => [m.name, m]));
|
|
1363
|
+
const mergeCandidates = [];
|
|
1310
1364
|
for (const model of normalizedModels) {
|
|
1311
1365
|
if (!model.name.endsWith("Json")) continue;
|
|
1312
1366
|
const baseName = model.name.slice(0, -4);
|
|
@@ -1314,13 +1368,20 @@ function collapseJsonSuffixModels(models, services) {
|
|
|
1314
1368
|
if (!baseModel || model.fields.length <= baseModel.fields.length) continue;
|
|
1315
1369
|
const isSuperset = baseModel.fields.every((f) => model.fields.some((mf) => mf.name === f.name));
|
|
1316
1370
|
if (!isSuperset) continue;
|
|
1317
|
-
baseModel
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1371
|
+
if (hasModelConstDiscriminant(model) && hasModelConstDiscriminant(baseModel)) continue;
|
|
1372
|
+
if (isModelReferencedByOthers(model.name, normalizedModels, /* @__PURE__ */ new Set([model.name, baseName]))) continue;
|
|
1373
|
+
mergeCandidates.push({ jsonModel: model, baseModel });
|
|
1374
|
+
}
|
|
1375
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
1376
|
+
for (const { jsonModel, baseModel } of mergeCandidates) {
|
|
1377
|
+
baseModel.fields = jsonModel.fields;
|
|
1378
|
+
toRemove.add(jsonModel.name);
|
|
1379
|
+
rewriteModelRefs(normalizedModels, services, jsonModel.name, baseModel.name);
|
|
1380
|
+
console.warn(
|
|
1381
|
+
`[oagen] Merged "${jsonModel.name}" into "${baseModel.name}" (${jsonModel.fields.length} fields, superset)`
|
|
1382
|
+
);
|
|
1322
1383
|
}
|
|
1323
|
-
return normalizedModels;
|
|
1384
|
+
return normalizedModels.filter((m) => !toRemove.has(m.name));
|
|
1324
1385
|
}
|
|
1325
1386
|
|
|
1326
1387
|
// src/parser/collect-inline-enums.ts
|
|
@@ -1382,11 +1443,11 @@ async function parseSpec(specPath, options) {
|
|
|
1382
1443
|
options?.operationIdTransform
|
|
1383
1444
|
);
|
|
1384
1445
|
const responseNormalizedModels = mergeInlineResponseModels(models, inlineModels);
|
|
1385
|
-
const jsonCollapsedModels = collapseJsonSuffixModels(responseNormalizedModels, services);
|
|
1386
1446
|
const fieldInlineModels = extractInlineModelsFromSchemas(
|
|
1387
1447
|
spec.components?.schemas
|
|
1388
1448
|
);
|
|
1389
|
-
const
|
|
1449
|
+
const fieldMergedModels = mergeFieldInlineModels(responseNormalizedModels, fieldInlineModels);
|
|
1450
|
+
const finalModels = collapseJsonSuffixModels(fieldMergedModels, services);
|
|
1390
1451
|
collectInlineEnumsFromModels(finalModels, enums);
|
|
1391
1452
|
const auth = extractAuthSchemes(spec.components?.securitySchemes);
|
|
1392
1453
|
const serverEntries = (spec.servers ?? []).map((s) => ({ url: s.url ?? "", description: s.description })).filter((s) => s.url);
|
|
@@ -1419,233 +1480,6 @@ function extractAuthSchemes(securitySchemes) {
|
|
|
1419
1480
|
return schemes.length > 0 ? schemes : void 0;
|
|
1420
1481
|
}
|
|
1421
1482
|
|
|
1422
|
-
// src/differ/file-map.ts
|
|
1423
|
-
function mapChangesToFiles(changes, emitter, ctx) {
|
|
1424
|
-
const regenerate = /* @__PURE__ */ new Set();
|
|
1425
|
-
const toDelete = /* @__PURE__ */ new Set();
|
|
1426
|
-
const modelFiles = /* @__PURE__ */ new Map();
|
|
1427
|
-
const enumFiles = /* @__PURE__ */ new Map();
|
|
1428
|
-
const serviceFiles = /* @__PURE__ */ new Map();
|
|
1429
|
-
for (const model of ctx.spec.models) {
|
|
1430
|
-
const files = emitter.generateModels([model], ctx);
|
|
1431
|
-
const sigs = emitter.generateTypeSignatures?.({ ...ctx.spec, models: [model], enums: [], services: [] }, ctx) ?? [];
|
|
1432
|
-
modelFiles.set(
|
|
1433
|
-
model.name,
|
|
1434
|
-
[...files, ...sigs].map((f) => f.path)
|
|
1435
|
-
);
|
|
1436
|
-
}
|
|
1437
|
-
for (const enumDef of ctx.spec.enums) {
|
|
1438
|
-
const files = emitter.generateEnums([enumDef], ctx);
|
|
1439
|
-
enumFiles.set(
|
|
1440
|
-
enumDef.name,
|
|
1441
|
-
files.map((f) => f.path)
|
|
1442
|
-
);
|
|
1443
|
-
}
|
|
1444
|
-
for (const service of ctx.spec.services) {
|
|
1445
|
-
const files = [
|
|
1446
|
-
...emitter.generateResources([service], ctx),
|
|
1447
|
-
...emitter.generateTests({ ...ctx.spec, services: [service], models: [], enums: [] }, ctx)
|
|
1448
|
-
];
|
|
1449
|
-
const sigs = emitter.generateTypeSignatures?.({ ...ctx.spec, services: [service], models: [], enums: [] }, ctx) ?? [];
|
|
1450
|
-
serviceFiles.set(
|
|
1451
|
-
service.name,
|
|
1452
|
-
[...files, ...sigs].map((f) => f.path)
|
|
1453
|
-
);
|
|
1454
|
-
}
|
|
1455
|
-
const modelToServices = buildReferenceGraph(ctx.spec);
|
|
1456
|
-
for (const change of changes) {
|
|
1457
|
-
switch (change.kind) {
|
|
1458
|
-
case "model-added":
|
|
1459
|
-
case "model-modified": {
|
|
1460
|
-
const files = modelFiles.get(change.name) ?? [];
|
|
1461
|
-
files.forEach((f) => regenerate.add(f));
|
|
1462
|
-
const refs = modelToServices.get(change.name) ?? [];
|
|
1463
|
-
for (const svc of refs) {
|
|
1464
|
-
const svcFiles = serviceFiles.get(svc) ?? [];
|
|
1465
|
-
svcFiles.forEach((f) => regenerate.add(f));
|
|
1466
|
-
}
|
|
1467
|
-
break;
|
|
1468
|
-
}
|
|
1469
|
-
case "model-removed": {
|
|
1470
|
-
const files = modelFiles.get(change.name) ?? [];
|
|
1471
|
-
files.forEach((f) => toDelete.add(f));
|
|
1472
|
-
const refs = modelToServices.get(change.name) ?? [];
|
|
1473
|
-
for (const svc of refs) {
|
|
1474
|
-
const svcFiles = serviceFiles.get(svc) ?? [];
|
|
1475
|
-
svcFiles.forEach((f) => regenerate.add(f));
|
|
1476
|
-
}
|
|
1477
|
-
break;
|
|
1478
|
-
}
|
|
1479
|
-
case "enum-added":
|
|
1480
|
-
case "enum-modified": {
|
|
1481
|
-
const files = enumFiles.get(change.name) ?? [];
|
|
1482
|
-
files.forEach((f) => regenerate.add(f));
|
|
1483
|
-
const refs = modelToServices.get(change.name) ?? [];
|
|
1484
|
-
for (const svc of refs) {
|
|
1485
|
-
const svcFiles = serviceFiles.get(svc) ?? [];
|
|
1486
|
-
svcFiles.forEach((f) => regenerate.add(f));
|
|
1487
|
-
}
|
|
1488
|
-
break;
|
|
1489
|
-
}
|
|
1490
|
-
case "enum-removed": {
|
|
1491
|
-
const files = enumFiles.get(change.name) ?? [];
|
|
1492
|
-
files.forEach((f) => toDelete.add(f));
|
|
1493
|
-
const refs = modelToServices.get(change.name) ?? [];
|
|
1494
|
-
for (const svc of refs) {
|
|
1495
|
-
const svcFiles = serviceFiles.get(svc) ?? [];
|
|
1496
|
-
svcFiles.forEach((f) => regenerate.add(f));
|
|
1497
|
-
}
|
|
1498
|
-
break;
|
|
1499
|
-
}
|
|
1500
|
-
case "service-added": {
|
|
1501
|
-
const files = serviceFiles.get(change.name) ?? [];
|
|
1502
|
-
files.forEach((f) => regenerate.add(f));
|
|
1503
|
-
break;
|
|
1504
|
-
}
|
|
1505
|
-
case "service-removed": {
|
|
1506
|
-
const files = serviceFiles.get(change.name) ?? [];
|
|
1507
|
-
files.forEach((f) => toDelete.add(f));
|
|
1508
|
-
break;
|
|
1509
|
-
}
|
|
1510
|
-
case "operation-added":
|
|
1511
|
-
case "operation-removed":
|
|
1512
|
-
case "operation-modified": {
|
|
1513
|
-
const files = serviceFiles.get(change.serviceName) ?? [];
|
|
1514
|
-
files.forEach((f) => regenerate.add(f));
|
|
1515
|
-
break;
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
for (const f of toDelete) {
|
|
1520
|
-
regenerate.delete(f);
|
|
1521
|
-
}
|
|
1522
|
-
return {
|
|
1523
|
-
regenerate: [...regenerate].sort(),
|
|
1524
|
-
delete: [...toDelete].sort()
|
|
1525
|
-
};
|
|
1526
|
-
}
|
|
1527
|
-
function buildReferenceGraph(spec) {
|
|
1528
|
-
const refs = /* @__PURE__ */ new Map();
|
|
1529
|
-
function trackRef(typeRef, serviceName) {
|
|
1530
|
-
const addName = (name) => {
|
|
1531
|
-
if (!refs.has(name)) refs.set(name, /* @__PURE__ */ new Set());
|
|
1532
|
-
refs.get(name).add(serviceName);
|
|
1533
|
-
};
|
|
1534
|
-
walkTypeRef(typeRef, {
|
|
1535
|
-
model: (r) => addName(r.name),
|
|
1536
|
-
enum: (r) => addName(r.name)
|
|
1537
|
-
});
|
|
1538
|
-
}
|
|
1539
|
-
for (const service of spec.services) {
|
|
1540
|
-
for (const op of service.operations) {
|
|
1541
|
-
for (const p of [...op.pathParams, ...op.queryParams, ...op.headerParams]) {
|
|
1542
|
-
trackRef(p.type, service.name);
|
|
1543
|
-
}
|
|
1544
|
-
if (op.requestBody) trackRef(op.requestBody, service.name);
|
|
1545
|
-
trackRef(op.response, service.name);
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
const modelDeps = /* @__PURE__ */ new Map();
|
|
1549
|
-
function collectDeps(typeRef, deps) {
|
|
1550
|
-
walkTypeRef(typeRef, {
|
|
1551
|
-
model: (r) => deps.add(r.name),
|
|
1552
|
-
enum: (r) => deps.add(r.name)
|
|
1553
|
-
});
|
|
1554
|
-
}
|
|
1555
|
-
for (const model of spec.models) {
|
|
1556
|
-
const deps = /* @__PURE__ */ new Set();
|
|
1557
|
-
for (const field of model.fields) {
|
|
1558
|
-
collectDeps(field.type, deps);
|
|
1559
|
-
}
|
|
1560
|
-
deps.delete(model.name);
|
|
1561
|
-
if (deps.size > 0) {
|
|
1562
|
-
modelDeps.set(model.name, deps);
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
let changed = true;
|
|
1566
|
-
while (changed) {
|
|
1567
|
-
changed = false;
|
|
1568
|
-
for (const [modelName, services] of refs) {
|
|
1569
|
-
const deps = modelDeps.get(modelName);
|
|
1570
|
-
if (!deps) continue;
|
|
1571
|
-
for (const dep of deps) {
|
|
1572
|
-
if (!refs.has(dep)) refs.set(dep, /* @__PURE__ */ new Set());
|
|
1573
|
-
const depServices = refs.get(dep);
|
|
1574
|
-
for (const svc of services) {
|
|
1575
|
-
if (!depServices.has(svc)) {
|
|
1576
|
-
depServices.add(svc);
|
|
1577
|
-
changed = true;
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
const result = /* @__PURE__ */ new Map();
|
|
1584
|
-
for (const [name, svcSet] of refs) {
|
|
1585
|
-
result.set(name, [...svcSet]);
|
|
1586
|
-
}
|
|
1587
|
-
return result;
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
// src/engine/incremental.ts
|
|
1591
|
-
import * as fs from "fs/promises";
|
|
1592
|
-
import * as path from "path";
|
|
1593
|
-
async function generateIncremental(oldSpec, newSpec, emitter, options) {
|
|
1594
|
-
const diff = diffSpecs(oldSpec, newSpec);
|
|
1595
|
-
if (diff.changes.length === 0) {
|
|
1596
|
-
return { generated: [], deleted: [], diff };
|
|
1597
|
-
}
|
|
1598
|
-
const ctx = buildEmitterContext(newSpec, options);
|
|
1599
|
-
const affected = mapChangesToFiles(diff.changes, emitter, ctx);
|
|
1600
|
-
const allFiles = generateAllFiles(newSpec, emitter, ctx);
|
|
1601
|
-
const header = emitter.fileHeader();
|
|
1602
|
-
const affectedSet = new Set(affected.regenerate);
|
|
1603
|
-
const generated = applyFileHeaders(
|
|
1604
|
-
allFiles.filter((f) => affectedSet.has(f.path)),
|
|
1605
|
-
header
|
|
1606
|
-
);
|
|
1607
|
-
if (!options.dryRun) {
|
|
1608
|
-
const writeResult = await writeFiles(generated, options.outputDir, {
|
|
1609
|
-
language: emitter.language,
|
|
1610
|
-
header
|
|
1611
|
-
});
|
|
1612
|
-
if (writeResult.ignored.length > 0) {
|
|
1613
|
-
console.log(`Ignored ${writeResult.ignored.length} files (@oagen-ignore-file)`);
|
|
1614
|
-
}
|
|
1615
|
-
if (options.target) {
|
|
1616
|
-
const targetResult = await integrateGeneratedFiles({
|
|
1617
|
-
files: generated,
|
|
1618
|
-
language: emitter.language,
|
|
1619
|
-
targetDir: options.target,
|
|
1620
|
-
header
|
|
1621
|
-
});
|
|
1622
|
-
if (targetResult.written.length > 0) {
|
|
1623
|
-
console.log(`Target: created ${targetResult.written.length} new files`);
|
|
1624
|
-
}
|
|
1625
|
-
if (targetResult.merged.length > 0) {
|
|
1626
|
-
console.log(`Target: merged into ${targetResult.merged.length} existing files (additive only)`);
|
|
1627
|
-
}
|
|
1628
|
-
if (targetResult.skipped.length > 0) {
|
|
1629
|
-
console.log(`Target: skipped ${targetResult.skipped.length} files (excluded or no grammar)`);
|
|
1630
|
-
}
|
|
1631
|
-
if (targetResult.ignored.length > 0) {
|
|
1632
|
-
console.log(`Target: ignored ${targetResult.ignored.length} files (@oagen-ignore-file)`);
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
if (options.force) {
|
|
1636
|
-
for (const filePath of affected.delete) {
|
|
1637
|
-
const fullPath = path.join(options.outputDir, filePath);
|
|
1638
|
-
await fs.rm(fullPath, { force: true });
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
return {
|
|
1643
|
-
generated,
|
|
1644
|
-
deleted: options.force ? affected.delete : [],
|
|
1645
|
-
diff
|
|
1646
|
-
};
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
1483
|
// src/engine/operation-plan.ts
|
|
1650
1484
|
function planOperation(op) {
|
|
1651
1485
|
const isDelete = op.httpMethod === "delete";
|
|
@@ -1714,10 +1548,8 @@ export {
|
|
|
1714
1548
|
generate,
|
|
1715
1549
|
generateAllFiles,
|
|
1716
1550
|
generateFiles,
|
|
1717
|
-
generateIncremental,
|
|
1718
1551
|
getEmitter,
|
|
1719
1552
|
integrateGeneratedFiles,
|
|
1720
|
-
mapChangesToFiles,
|
|
1721
1553
|
mapFilesForTargetIntegration,
|
|
1722
1554
|
mapTypeRef,
|
|
1723
1555
|
parseSpec,
|