@workos/oagen 0.0.1 → 0.1.1
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-2JXFTDTO.js} +13 -92
- package/dist/cli/index.js +185 -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 +2 -2
- package/skills/check-emitter-parity/SKILL.md +232 -0
package/dist/cli/index.js
CHANGED
|
@@ -284,17 +284,26 @@ function buildInlineModel(name, schema) {
|
|
|
284
284
|
|
|
285
285
|
// src/parser/schemas.ts
|
|
286
286
|
function extractSchemas(schemas) {
|
|
287
|
-
const models = [];
|
|
288
287
|
const enums = [];
|
|
289
|
-
if (!schemas) return { models, enums };
|
|
288
|
+
if (!schemas) return { models: [], enums };
|
|
289
|
+
const modelsByCleanName = /* @__PURE__ */ new Map();
|
|
290
290
|
for (const [name, schema] of Object.entries(schemas)) {
|
|
291
291
|
const pascalName = cleanSchemaName(toPascalCase(name));
|
|
292
292
|
if (schema.enum) {
|
|
293
293
|
enums.push(extractEnum(pascalName, schema));
|
|
294
294
|
} else {
|
|
295
|
-
|
|
295
|
+
const model = extractModel(pascalName, schema, schemas);
|
|
296
|
+
const existing = modelsByCleanName.get(pascalName);
|
|
297
|
+
if (existing) {
|
|
298
|
+
if (model.fields.length > existing.fields.length) {
|
|
299
|
+
modelsByCleanName.set(pascalName, model);
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
modelsByCleanName.set(pascalName, model);
|
|
303
|
+
}
|
|
296
304
|
}
|
|
297
305
|
}
|
|
306
|
+
const models = [...modelsByCleanName.values()];
|
|
298
307
|
const modelNames = new Set(models.map((m) => m.name));
|
|
299
308
|
const inlineQueue = [...models];
|
|
300
309
|
while (inlineQueue.length > 0) {
|
|
@@ -385,9 +394,9 @@ function extractNestedSchema(name, schema) {
|
|
|
385
394
|
if (variant.properties && (variant.type === "object" || !variant.type)) {
|
|
386
395
|
const requiredSet = new Set(variant.required ?? []);
|
|
387
396
|
const fields = [];
|
|
388
|
-
for (const [fn,
|
|
389
|
-
if (!
|
|
390
|
-
fields.push(buildFieldFromSchema(fn,
|
|
397
|
+
for (const [fn, fs2] of Object.entries(variant.properties)) {
|
|
398
|
+
if (!fs2) continue;
|
|
399
|
+
fields.push(buildFieldFromSchema(fn, fs2, name, requiredSet));
|
|
391
400
|
}
|
|
392
401
|
const variantName = models.length === 0 ? name : `${name}${models.length + 1}`;
|
|
393
402
|
models.push({ name: variantName, description: variant.description, fields });
|
|
@@ -398,18 +407,18 @@ function extractNestedSchema(name, schema) {
|
|
|
398
407
|
if (schema.properties && (schema.type === "object" || !schema.type)) {
|
|
399
408
|
const requiredSet = new Set(schema.required ?? []);
|
|
400
409
|
const fields = [];
|
|
401
|
-
for (const [fn,
|
|
402
|
-
if (!
|
|
403
|
-
fields.push(buildFieldFromSchema(fn,
|
|
410
|
+
for (const [fn, fs2] of Object.entries(schema.properties)) {
|
|
411
|
+
if (!fs2) continue;
|
|
412
|
+
fields.push(buildFieldFromSchema(fn, fs2, name, requiredSet));
|
|
404
413
|
}
|
|
405
414
|
return [{ name, description: schema.description, fields }];
|
|
406
415
|
}
|
|
407
416
|
if (schema.type === "array" && schema.items?.properties) {
|
|
408
417
|
const requiredSet = new Set(schema.items.required ?? []);
|
|
409
418
|
const fields = [];
|
|
410
|
-
for (const [fn,
|
|
411
|
-
if (!
|
|
412
|
-
fields.push(buildFieldFromSchema(fn,
|
|
419
|
+
for (const [fn, fs2] of Object.entries(schema.items.properties)) {
|
|
420
|
+
if (!fs2) continue;
|
|
421
|
+
fields.push(buildFieldFromSchema(fn, fs2, name, requiredSet));
|
|
413
422
|
}
|
|
414
423
|
return [{ name, description: schema.items.description, fields }];
|
|
415
424
|
}
|
|
@@ -724,6 +733,18 @@ function classifyAndExtractResponse(schema, contextName) {
|
|
|
724
733
|
}
|
|
725
734
|
return extractDirectResource(schema, contextName);
|
|
726
735
|
}
|
|
736
|
+
var KNOWN_DATA_PATHS = /* @__PURE__ */ new Set(["data", "items", "results", "records", "entries", "values", "nodes", "edges"]);
|
|
737
|
+
var PAGINATION_METADATA_PATTERNS = [
|
|
738
|
+
"metadata",
|
|
739
|
+
"pagination",
|
|
740
|
+
"cursor",
|
|
741
|
+
"has_more",
|
|
742
|
+
"page_info",
|
|
743
|
+
"total",
|
|
744
|
+
"next_page",
|
|
745
|
+
"previous_page",
|
|
746
|
+
"offset"
|
|
747
|
+
];
|
|
727
748
|
function detectListEnvelope(schema) {
|
|
728
749
|
const propSources = [];
|
|
729
750
|
if (schema.allOf) {
|
|
@@ -740,17 +761,26 @@ function detectListEnvelope(schema) {
|
|
|
740
761
|
Object.assign(mergedProps, source);
|
|
741
762
|
}
|
|
742
763
|
const arrayProps = [];
|
|
743
|
-
|
|
764
|
+
const nonArrayKeys = [];
|
|
744
765
|
for (const [key, propSchema] of Object.entries(mergedProps)) {
|
|
745
766
|
if (!propSchema) continue;
|
|
746
767
|
if (propSchema.type === "array") {
|
|
747
768
|
arrayProps.push(key);
|
|
748
769
|
} else {
|
|
749
|
-
|
|
770
|
+
nonArrayKeys.push(key);
|
|
750
771
|
}
|
|
751
772
|
}
|
|
752
|
-
if (arrayProps.length === 1 &&
|
|
753
|
-
|
|
773
|
+
if (arrayProps.length === 1 && nonArrayKeys.length >= 1) {
|
|
774
|
+
const arrayPropName = arrayProps[0];
|
|
775
|
+
if (KNOWN_DATA_PATHS.has(arrayPropName)) {
|
|
776
|
+
return { isEnvelope: true, dataPath: arrayPropName };
|
|
777
|
+
}
|
|
778
|
+
const hasPaginationCompanion = nonArrayKeys.some(
|
|
779
|
+
(key) => PAGINATION_METADATA_PATTERNS.some((pattern) => key.toLowerCase().includes(pattern))
|
|
780
|
+
);
|
|
781
|
+
if (hasPaginationCompanion) {
|
|
782
|
+
return { isEnvelope: true, dataPath: arrayPropName };
|
|
783
|
+
}
|
|
754
784
|
}
|
|
755
785
|
return { isEnvelope: false, dataPath: null };
|
|
756
786
|
}
|
|
@@ -1015,15 +1045,15 @@ function extractOperations(paths, operationIdTransform) {
|
|
|
1015
1045
|
if (!paths) return { services: [], inlineModels: [] };
|
|
1016
1046
|
const serviceMap = /* @__PURE__ */ new Map();
|
|
1017
1047
|
const inlineModels = [];
|
|
1018
|
-
for (const [
|
|
1048
|
+
for (const [path6, pathItem] of Object.entries(paths)) {
|
|
1019
1049
|
const pathLevelParams = pathItem.parameters ?? [];
|
|
1020
1050
|
for (const method of HTTP_METHODS) {
|
|
1021
1051
|
const op = pathItem[method];
|
|
1022
1052
|
if (!op) continue;
|
|
1023
|
-
const serviceName = inferServiceName(
|
|
1053
|
+
const serviceName = inferServiceName(path6, op.tags?.[0]);
|
|
1024
1054
|
const { operation, inlineModels: opModels } = buildOperation(
|
|
1025
1055
|
method,
|
|
1026
|
-
|
|
1056
|
+
path6,
|
|
1027
1057
|
op,
|
|
1028
1058
|
pathLevelParams,
|
|
1029
1059
|
operationIdTransform
|
|
@@ -1043,28 +1073,28 @@ function extractOperations(paths, operationIdTransform) {
|
|
|
1043
1073
|
}));
|
|
1044
1074
|
return { services, inlineModels };
|
|
1045
1075
|
}
|
|
1046
|
-
function inferServiceName(
|
|
1076
|
+
function inferServiceName(path6, tag) {
|
|
1047
1077
|
if (tag) {
|
|
1048
1078
|
return toPascalCase(tag);
|
|
1049
1079
|
}
|
|
1050
|
-
const segments =
|
|
1080
|
+
const segments = path6.split("/").filter(Boolean);
|
|
1051
1081
|
const first = segments[0] ?? "Default";
|
|
1052
1082
|
if (first.startsWith("{")) return "Default";
|
|
1053
1083
|
return toPascalCase(first);
|
|
1054
1084
|
}
|
|
1055
|
-
function inferOperationName(method,
|
|
1085
|
+
function inferOperationName(method, path6, operationId, operationIdTransform) {
|
|
1056
1086
|
if (operationId) {
|
|
1057
1087
|
if (operationIdTransform) {
|
|
1058
1088
|
return operationIdTransform(operationId);
|
|
1059
1089
|
}
|
|
1060
1090
|
return toCamelCase(operationId);
|
|
1061
1091
|
}
|
|
1062
|
-
const verb = inferVerb(method,
|
|
1063
|
-
const resource = inferResourceFromPath(
|
|
1092
|
+
const verb = inferVerb(method, path6);
|
|
1093
|
+
const resource = inferResourceFromPath(path6);
|
|
1064
1094
|
return resource ? toCamelCase(`${verb}_${resource}`) : verb;
|
|
1065
1095
|
}
|
|
1066
|
-
function inferVerb(method,
|
|
1067
|
-
const segments =
|
|
1096
|
+
function inferVerb(method, path6) {
|
|
1097
|
+
const segments = path6.split("/").filter(Boolean);
|
|
1068
1098
|
const hasTrailingParam = segments.length > 0 && segments[segments.length - 1].startsWith("{");
|
|
1069
1099
|
switch (method) {
|
|
1070
1100
|
case "get":
|
|
@@ -1086,8 +1116,8 @@ function inferVerb(method, path8) {
|
|
|
1086
1116
|
return method;
|
|
1087
1117
|
}
|
|
1088
1118
|
}
|
|
1089
|
-
function inferResourceFromPath(
|
|
1090
|
-
const segments =
|
|
1119
|
+
function inferResourceFromPath(path6) {
|
|
1120
|
+
const segments = path6.split("/").filter(Boolean);
|
|
1091
1121
|
if (segments.length <= 1) return null;
|
|
1092
1122
|
let resourceSegment = null;
|
|
1093
1123
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
@@ -1135,15 +1165,15 @@ function disambiguateOperationNames(operations) {
|
|
|
1135
1165
|
}
|
|
1136
1166
|
}
|
|
1137
1167
|
}
|
|
1138
|
-
function inferDeeperContext(
|
|
1139
|
-
const segments =
|
|
1168
|
+
function inferDeeperContext(path6) {
|
|
1169
|
+
const segments = path6.split("/").filter(Boolean);
|
|
1140
1170
|
const nonParams = segments.filter((s) => !s.startsWith("{"));
|
|
1141
1171
|
if (nonParams.length >= 3) {
|
|
1142
1172
|
return toPascalCase(nonParams.slice(1, -1).join("_"));
|
|
1143
1173
|
}
|
|
1144
1174
|
return null;
|
|
1145
1175
|
}
|
|
1146
|
-
function buildOperation(method,
|
|
1176
|
+
function buildOperation(method, path6, op, pathLevelParams, operationIdTransform) {
|
|
1147
1177
|
const allParams = [...pathLevelParams, ...op.parameters ?? []];
|
|
1148
1178
|
const hasIdempotencyHeader = allParams.some((p) => p.in === "header" && p.name.toLowerCase() === "idempotency-key");
|
|
1149
1179
|
const pathParams = extractParams(allParams, "path");
|
|
@@ -1160,7 +1190,7 @@ function buildOperation(method, path8, op, pathLevelParams, operationIdTransform
|
|
|
1160
1190
|
isPaginated,
|
|
1161
1191
|
dataPath: responseDataPath,
|
|
1162
1192
|
itemType: responseItemType
|
|
1163
|
-
} = extractResponses(op.responses, op,
|
|
1193
|
+
} = extractResponses(op.responses, op, path6, method);
|
|
1164
1194
|
inlineModels.push(...reqBodyModels);
|
|
1165
1195
|
const paginationFromParams = detectPagination(response, queryParams, responseDataPath);
|
|
1166
1196
|
let pagination;
|
|
@@ -1178,10 +1208,10 @@ function buildOperation(method, path8, op, pathLevelParams, operationIdTransform
|
|
|
1178
1208
|
}
|
|
1179
1209
|
return {
|
|
1180
1210
|
operation: {
|
|
1181
|
-
name: inferOperationName(method,
|
|
1211
|
+
name: inferOperationName(method, path6, op.operationId, operationIdTransform),
|
|
1182
1212
|
description: buildDescription(op.summary, op.description),
|
|
1183
1213
|
httpMethod: method,
|
|
1184
|
-
path:
|
|
1214
|
+
path: path6,
|
|
1185
1215
|
pathParams,
|
|
1186
1216
|
queryParams,
|
|
1187
1217
|
headerParams,
|
|
@@ -1312,15 +1342,15 @@ function deriveOneOfVariantName(variant, baseName, existingModels) {
|
|
|
1312
1342
|
}
|
|
1313
1343
|
return name;
|
|
1314
1344
|
}
|
|
1315
|
-
function deriveResponseName(op,
|
|
1345
|
+
function deriveResponseName(op, path6, method) {
|
|
1316
1346
|
if (op?.operationId) {
|
|
1317
1347
|
return cleanSchemaName(toPascalCase(op.operationId) + "Response");
|
|
1318
1348
|
}
|
|
1319
|
-
const segments =
|
|
1349
|
+
const segments = path6.split("/").filter(Boolean);
|
|
1320
1350
|
const resource = segments[0] ?? "Unknown";
|
|
1321
1351
|
return toPascalCase(resource) + toPascalCase(method) + "Response";
|
|
1322
1352
|
}
|
|
1323
|
-
function extractResponses(responses, op,
|
|
1353
|
+
function extractResponses(responses, op, path6, method) {
|
|
1324
1354
|
const errors = [];
|
|
1325
1355
|
let response = { kind: "primitive", type: "string" };
|
|
1326
1356
|
let inlineModels = [];
|
|
@@ -1335,7 +1365,7 @@ function extractResponses(responses, op, path8, method) {
|
|
|
1335
1365
|
let extractedType = { kind: "primitive", type: "unknown" };
|
|
1336
1366
|
const jsonContent = resp.content?.["application/json"];
|
|
1337
1367
|
if (jsonContent?.schema) {
|
|
1338
|
-
const contextName = deriveResponseName(op,
|
|
1368
|
+
const contextName = deriveResponseName(op, path6 ?? "/", method ?? "get");
|
|
1339
1369
|
const result = classifyAndExtractResponse(jsonContent.schema, contextName);
|
|
1340
1370
|
extractedType = result.response;
|
|
1341
1371
|
inlineModels = result.inlineModels;
|
|
@@ -1354,7 +1384,7 @@ function extractResponses(responses, op, path8, method) {
|
|
|
1354
1384
|
if (firstContent?.schema) {
|
|
1355
1385
|
extractedType = schemaToTypeRef(
|
|
1356
1386
|
firstContent.schema,
|
|
1357
|
-
deriveResponseName(op,
|
|
1387
|
+
deriveResponseName(op, path6 ?? "/", method ?? "get")
|
|
1358
1388
|
);
|
|
1359
1389
|
}
|
|
1360
1390
|
}
|
|
@@ -1387,20 +1417,24 @@ function keepLargestByName(models) {
|
|
|
1387
1417
|
}
|
|
1388
1418
|
return [...byName.values()];
|
|
1389
1419
|
}
|
|
1390
|
-
function
|
|
1420
|
+
function rewriteModelRef(ref, oldName, newName) {
|
|
1421
|
+
walkTypeRef(ref, {
|
|
1422
|
+
model: (r) => {
|
|
1423
|
+
if (r.name === oldName) r.name = newName;
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
function rewriteModelRefs(models, services, oldName, newName) {
|
|
1428
|
+
for (const model of models) {
|
|
1429
|
+
for (const field of model.fields) {
|
|
1430
|
+
rewriteModelRef(field.type, oldName, newName);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1391
1433
|
for (const service of services) {
|
|
1392
1434
|
for (const op of service.operations) {
|
|
1393
|
-
|
|
1394
|
-
model: (r) => {
|
|
1395
|
-
if (r.name === oldName) r.name = newName;
|
|
1396
|
-
}
|
|
1397
|
-
});
|
|
1435
|
+
rewriteModelRef(op.response, oldName, newName);
|
|
1398
1436
|
if (op.requestBody) {
|
|
1399
|
-
|
|
1400
|
-
model: (r) => {
|
|
1401
|
-
if (r.name === oldName) r.name = newName;
|
|
1402
|
-
}
|
|
1403
|
-
});
|
|
1437
|
+
rewriteModelRef(op.requestBody, oldName, newName);
|
|
1404
1438
|
}
|
|
1405
1439
|
}
|
|
1406
1440
|
}
|
|
@@ -1449,9 +1483,30 @@ function mergeFieldInlineModels(existingModels, fieldInlineModels) {
|
|
|
1449
1483
|
});
|
|
1450
1484
|
return [...existingModels, ...keepLargestByName(deduplicatedFieldModels)];
|
|
1451
1485
|
}
|
|
1486
|
+
function hasModelConstDiscriminant(model) {
|
|
1487
|
+
return model.fields.some(
|
|
1488
|
+
(f) => (f.name === "object" || f.name === "type") && f.type.kind === "literal" && typeof f.type.value === "string"
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
function isModelReferencedByOthers(name, models, excludeNames) {
|
|
1492
|
+
for (const model of models) {
|
|
1493
|
+
if (excludeNames.has(model.name)) continue;
|
|
1494
|
+
for (const field of model.fields) {
|
|
1495
|
+
let found = false;
|
|
1496
|
+
walkTypeRef(field.type, {
|
|
1497
|
+
model: (r) => {
|
|
1498
|
+
if (r.name === name) found = true;
|
|
1499
|
+
}
|
|
1500
|
+
});
|
|
1501
|
+
if (found) return true;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return false;
|
|
1505
|
+
}
|
|
1452
1506
|
function collapseJsonSuffixModels(models, services) {
|
|
1453
1507
|
const normalizedModels = [...models];
|
|
1454
1508
|
const byName = new Map(normalizedModels.map((m) => [m.name, m]));
|
|
1509
|
+
const mergeCandidates = [];
|
|
1455
1510
|
for (const model of normalizedModels) {
|
|
1456
1511
|
if (!model.name.endsWith("Json")) continue;
|
|
1457
1512
|
const baseName = model.name.slice(0, -4);
|
|
@@ -1459,13 +1514,20 @@ function collapseJsonSuffixModels(models, services) {
|
|
|
1459
1514
|
if (!baseModel || model.fields.length <= baseModel.fields.length) continue;
|
|
1460
1515
|
const isSuperset = baseModel.fields.every((f) => model.fields.some((mf) => mf.name === f.name));
|
|
1461
1516
|
if (!isSuperset) continue;
|
|
1462
|
-
baseModel
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1517
|
+
if (hasModelConstDiscriminant(model) && hasModelConstDiscriminant(baseModel)) continue;
|
|
1518
|
+
if (isModelReferencedByOthers(model.name, normalizedModels, /* @__PURE__ */ new Set([model.name, baseName]))) continue;
|
|
1519
|
+
mergeCandidates.push({ jsonModel: model, baseModel });
|
|
1520
|
+
}
|
|
1521
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
1522
|
+
for (const { jsonModel, baseModel } of mergeCandidates) {
|
|
1523
|
+
baseModel.fields = jsonModel.fields;
|
|
1524
|
+
toRemove.add(jsonModel.name);
|
|
1525
|
+
rewriteModelRefs(normalizedModels, services, jsonModel.name, baseModel.name);
|
|
1526
|
+
console.warn(
|
|
1527
|
+
`[oagen] Merged "${jsonModel.name}" into "${baseModel.name}" (${jsonModel.fields.length} fields, superset)`
|
|
1528
|
+
);
|
|
1467
1529
|
}
|
|
1468
|
-
return normalizedModels;
|
|
1530
|
+
return normalizedModels.filter((m) => !toRemove.has(m.name));
|
|
1469
1531
|
}
|
|
1470
1532
|
|
|
1471
1533
|
// src/parser/collect-inline-enums.ts
|
|
@@ -1527,11 +1589,11 @@ async function parseSpec(specPath, options) {
|
|
|
1527
1589
|
options?.operationIdTransform
|
|
1528
1590
|
);
|
|
1529
1591
|
const responseNormalizedModels = mergeInlineResponseModels(models, inlineModels);
|
|
1530
|
-
const jsonCollapsedModels = collapseJsonSuffixModels(responseNormalizedModels, services);
|
|
1531
1592
|
const fieldInlineModels = extractInlineModelsFromSchemas(
|
|
1532
1593
|
spec.components?.schemas
|
|
1533
1594
|
);
|
|
1534
|
-
const
|
|
1595
|
+
const fieldMergedModels = mergeFieldInlineModels(responseNormalizedModels, fieldInlineModels);
|
|
1596
|
+
const finalModels = collapseJsonSuffixModels(fieldMergedModels, services);
|
|
1535
1597
|
collectInlineEnumsFromModels(finalModels, enums);
|
|
1536
1598
|
const auth = extractAuthSchemes(spec.components?.securitySchemes);
|
|
1537
1599
|
const serverEntries = (spec.servers ?? []).map((s) => ({ url: s.url ?? "", description: s.description })).filter((s) => s.url);
|
|
@@ -1617,11 +1679,11 @@ function declarationKey(node) {
|
|
|
1617
1679
|
function importEntries(node, source) {
|
|
1618
1680
|
const entries = [];
|
|
1619
1681
|
const collect = (spec) => {
|
|
1620
|
-
const
|
|
1682
|
+
const path6 = spec.childForFieldName("path")?.text ?? spec.text;
|
|
1621
1683
|
const name = spec.childForFieldName("name")?.text ?? "";
|
|
1622
1684
|
const text = source.slice(spec.startIndex, spec.endIndex);
|
|
1623
1685
|
entries.push({
|
|
1624
|
-
key: `${name}:${
|
|
1686
|
+
key: `${name}:${path6.replace(/^"|"$/g, "")}`,
|
|
1625
1687
|
text
|
|
1626
1688
|
});
|
|
1627
1689
|
};
|
|
@@ -2427,75 +2489,11 @@ async function extractStatements(source, language) {
|
|
|
2427
2489
|
const tree = safeParse(parser, source);
|
|
2428
2490
|
return adapter.parseStatements(tree, source);
|
|
2429
2491
|
}
|
|
2430
|
-
async function mergeIntoExisting(existingContent, generatedContent, language, header
|
|
2492
|
+
async function mergeIntoExisting(existingContent, generatedContent, language, header) {
|
|
2431
2493
|
const adapter = getMergeAdapter(language);
|
|
2432
2494
|
if (!adapter) {
|
|
2433
2495
|
throw new Error(`No merge adapter configured for language "${language}"`);
|
|
2434
2496
|
}
|
|
2435
|
-
if (options?.docstringOnly) {
|
|
2436
|
-
let result2 = existingContent;
|
|
2437
|
-
let docstringUpdates2 = 0;
|
|
2438
|
-
const parser = await getParser(language);
|
|
2439
|
-
const resultTree = safeParse(parser, result2);
|
|
2440
|
-
const generatedTree = safeParse(parser, generatedContent);
|
|
2441
|
-
const resultDocs = adapter.extractDocstrings(resultTree, result2);
|
|
2442
|
-
const generatedDocs = adapter.extractDocstrings(generatedTree, generatedContent);
|
|
2443
|
-
const ignoredRegions = findIgnoredRegions(result2);
|
|
2444
|
-
const ignoredSymbols = buildIgnoredSymbolNames(resultDocs, ignoredRegions);
|
|
2445
|
-
const headerLine2 = header.trim();
|
|
2446
|
-
const edits = [];
|
|
2447
|
-
for (const [symbolName, genInfo] of generatedDocs) {
|
|
2448
|
-
const existInfo = resultDocs.get(symbolName);
|
|
2449
|
-
if (!existInfo) continue;
|
|
2450
|
-
if (ignoredSymbols.has(symbolName)) continue;
|
|
2451
|
-
const genDoc = genInfo.docstring && genInfo.docstring.text.trim() !== headerLine2 ? genInfo.docstring : null;
|
|
2452
|
-
if (genDoc) {
|
|
2453
|
-
if (existInfo.docstring) {
|
|
2454
|
-
const isPreserved = existInfo.docstring.text.includes("@oagen-ignore");
|
|
2455
|
-
if (!isPreserved && existInfo.docstring.text !== genDoc.text) {
|
|
2456
|
-
edits.push({
|
|
2457
|
-
start: existInfo.docstring.startIndex,
|
|
2458
|
-
end: existInfo.docstring.endIndex,
|
|
2459
|
-
newText: genDoc.text
|
|
2460
|
-
});
|
|
2461
|
-
docstringUpdates2++;
|
|
2462
|
-
}
|
|
2463
|
-
} else {
|
|
2464
|
-
const lineStart = existInfo.declStartIndex - existInfo.declColumn;
|
|
2465
|
-
const indent = " ".repeat(existInfo.declColumn);
|
|
2466
|
-
edits.push({ start: lineStart, end: lineStart, newText: indent + genDoc.text + "\n" });
|
|
2467
|
-
docstringUpdates2++;
|
|
2468
|
-
}
|
|
2469
|
-
}
|
|
2470
|
-
for (const [memberName, genMember] of genInfo.members) {
|
|
2471
|
-
const existMember = existInfo.members.get(memberName);
|
|
2472
|
-
if (!existMember || !genMember.docstring) continue;
|
|
2473
|
-
if (existMember.docstring) {
|
|
2474
|
-
const isPreserved = existMember.docstring.text.includes("@oagen-ignore");
|
|
2475
|
-
if (!isPreserved && existMember.docstring.text !== genMember.docstring.text) {
|
|
2476
|
-
edits.push({
|
|
2477
|
-
start: existMember.docstring.startIndex,
|
|
2478
|
-
end: existMember.docstring.endIndex,
|
|
2479
|
-
newText: genMember.docstring.text
|
|
2480
|
-
});
|
|
2481
|
-
docstringUpdates2++;
|
|
2482
|
-
}
|
|
2483
|
-
} else {
|
|
2484
|
-
const lineStart = existMember.declStartIndex - existMember.declColumn;
|
|
2485
|
-
const indent = " ".repeat(existMember.declColumn);
|
|
2486
|
-
edits.push({ start: lineStart, end: lineStart, newText: indent + genMember.docstring.text + "\n" });
|
|
2487
|
-
docstringUpdates2++;
|
|
2488
|
-
}
|
|
2489
|
-
}
|
|
2490
|
-
}
|
|
2491
|
-
if (edits.length > 0) {
|
|
2492
|
-
edits.sort((a, b) => b.start - a.start);
|
|
2493
|
-
for (const edit of edits) {
|
|
2494
|
-
result2 = result2.slice(0, edit.start) + edit.newText + result2.slice(edit.end);
|
|
2495
|
-
}
|
|
2496
|
-
}
|
|
2497
|
-
return { content: result2, added: 0, preserved: 0, changed: docstringUpdates2 > 0 };
|
|
2498
|
-
}
|
|
2499
2497
|
const existingStatements = await extractStatements(existingContent, language);
|
|
2500
2498
|
const generatedStatements = await extractStatements(generatedContent, language);
|
|
2501
2499
|
const existingKeys = /* @__PURE__ */ new Set();
|
|
@@ -2544,6 +2542,14 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
2544
2542
|
preserved++;
|
|
2545
2543
|
continue;
|
|
2546
2544
|
}
|
|
2545
|
+
const braceMatch = imp.text.match(/\{([^}]+)\}/);
|
|
2546
|
+
if (braceMatch) {
|
|
2547
|
+
const names = braceMatch[1].split(",").map((n) => n.replace(/\btype\b/, "").trim()).filter(Boolean);
|
|
2548
|
+
if (names.length > 0 && names.every((n) => existingImportedNames.has(n))) {
|
|
2549
|
+
preserved++;
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2547
2553
|
newImports.push(imp);
|
|
2548
2554
|
}
|
|
2549
2555
|
for (const stmt of generatedStatements.statements) {
|
|
@@ -2768,22 +2774,6 @@ async function writeFiles(files, outputDir, options) {
|
|
|
2768
2774
|
}
|
|
2769
2775
|
if (language && hasGrammar(language)) {
|
|
2770
2776
|
try {
|
|
2771
|
-
if (file.mergeMode === "docstring-only") {
|
|
2772
|
-
const mergeResult2 = await mergeIntoExisting(existingContent, file.content, language, header, {
|
|
2773
|
-
docstringOnly: true
|
|
2774
|
-
});
|
|
2775
|
-
let finalContent2 = mergeResult2.content;
|
|
2776
|
-
if (header && !finalContent2.startsWith(header)) {
|
|
2777
|
-
finalContent2 = header + "\n\n" + finalContent2;
|
|
2778
|
-
}
|
|
2779
|
-
if (finalContent2 === existingContent) {
|
|
2780
|
-
result.identical.push(file.path);
|
|
2781
|
-
} else {
|
|
2782
|
-
await fs.writeFile(fullPath, finalContent2, "utf-8");
|
|
2783
|
-
result.merged.push(file.path);
|
|
2784
|
-
}
|
|
2785
|
-
continue;
|
|
2786
|
-
}
|
|
2787
2777
|
const mergeResult = await mergeIntoExisting(existingContent, file.content, language, header);
|
|
2788
2778
|
let finalContent = mergeResult.content;
|
|
2789
2779
|
if (header && !finalContent.startsWith(header)) {
|
|
@@ -2849,24 +2839,20 @@ function generateFiles(spec, emitter, options) {
|
|
|
2849
2839
|
}
|
|
2850
2840
|
|
|
2851
2841
|
// src/engine/integrate.ts
|
|
2852
|
-
|
|
2853
|
-
import * as path2 from "path";
|
|
2854
|
-
function mapFilesForTargetIntegration(files, language, targetDir) {
|
|
2842
|
+
function mapFilesForTargetIntegration(files, language) {
|
|
2855
2843
|
const langPrefix = `${language}/`;
|
|
2856
2844
|
return files.filter((f) => f.integrateTarget !== false).map((f) => {
|
|
2857
2845
|
const stripped = f.path.startsWith(langPrefix) ? f.path.replace(langPrefix, "") : f.path;
|
|
2858
|
-
const existsInTarget = targetDir ? fs2.existsSync(path2.join(targetDir, stripped)) : false;
|
|
2859
2846
|
return {
|
|
2860
2847
|
...f,
|
|
2861
2848
|
skipIfExists: false,
|
|
2862
2849
|
// Always merge in target — never hard-skip
|
|
2863
|
-
mergeMode: existsInTarget ? "docstring-only" : "full",
|
|
2864
2850
|
path: stripped
|
|
2865
2851
|
};
|
|
2866
2852
|
});
|
|
2867
2853
|
}
|
|
2868
2854
|
async function integrateGeneratedFiles(opts) {
|
|
2869
|
-
return writeFiles(mapFilesForTargetIntegration(opts.files, opts.language
|
|
2855
|
+
return writeFiles(mapFilesForTargetIntegration(opts.files, opts.language), opts.targetDir, {
|
|
2870
2856
|
language: opts.language,
|
|
2871
2857
|
header: opts.header
|
|
2872
2858
|
});
|
|
@@ -2933,8 +2919,8 @@ function getEmitter(language) {
|
|
|
2933
2919
|
}
|
|
2934
2920
|
|
|
2935
2921
|
// src/cli/overlay-loader.ts
|
|
2936
|
-
import { readFileSync, existsSync
|
|
2937
|
-
import * as
|
|
2922
|
+
import { readFileSync, existsSync } from "fs";
|
|
2923
|
+
import * as path2 from "path";
|
|
2938
2924
|
|
|
2939
2925
|
// src/compat/language-hints.ts
|
|
2940
2926
|
var UNTYPED_MAP_PATTERNS = /* @__PURE__ */ new Set([
|
|
@@ -3157,8 +3143,8 @@ function buildOverlayLookup(surface, manifest, spec, hints, options) {
|
|
|
3157
3143
|
for (const name of Object.keys(surface.typeAliases)) {
|
|
3158
3144
|
lookup.typeAliasByName.set(name, name);
|
|
3159
3145
|
}
|
|
3160
|
-
for (const [
|
|
3161
|
-
lookup.requiredExports.set(
|
|
3146
|
+
for (const [path6, symbols] of Object.entries(surface.exports)) {
|
|
3147
|
+
lookup.requiredExports.set(path6, new Set(symbols));
|
|
3162
3148
|
}
|
|
3163
3149
|
for (const record of [surface.classes, surface.interfaces, surface.typeAliases, surface.enums]) {
|
|
3164
3150
|
for (const [name, item] of Object.entries(record)) {
|
|
@@ -3325,11 +3311,11 @@ function patchOverlay(overlay, violations, baseline) {
|
|
|
3325
3311
|
if (v.category === "export-structure") {
|
|
3326
3312
|
const match = v.symbolPath.match(/^exports\[(.+?)\]\.(.+)$/);
|
|
3327
3313
|
if (match) {
|
|
3328
|
-
const [,
|
|
3329
|
-
if (!patched.requiredExports.has(
|
|
3330
|
-
patched.requiredExports.set(
|
|
3314
|
+
const [, path6, symbol] = match;
|
|
3315
|
+
if (!patched.requiredExports.has(path6)) {
|
|
3316
|
+
patched.requiredExports.set(path6, /* @__PURE__ */ new Set());
|
|
3331
3317
|
}
|
|
3332
|
-
patched.requiredExports.get(
|
|
3318
|
+
patched.requiredExports.get(path6).add(symbol);
|
|
3333
3319
|
}
|
|
3334
3320
|
}
|
|
3335
3321
|
}
|
|
@@ -3374,8 +3360,8 @@ function loadOverlayContext(opts) {
|
|
|
3374
3360
|
);
|
|
3375
3361
|
}
|
|
3376
3362
|
let manifest;
|
|
3377
|
-
const resolvedManifestPath = opts.manifestPath ??
|
|
3378
|
-
if (opts.manifestPath ||
|
|
3363
|
+
const resolvedManifestPath = opts.manifestPath ?? path2.join(opts.outputDir, "smoke-manifest.json");
|
|
3364
|
+
if (opts.manifestPath || existsSync(resolvedManifestPath)) {
|
|
3379
3365
|
try {
|
|
3380
3366
|
const parsed = JSON.parse(readFileSync(resolvedManifestPath, "utf-8"));
|
|
3381
3367
|
if (Array.isArray(parsed)) {
|
|
@@ -3862,288 +3848,21 @@ function summarize(changes) {
|
|
|
3862
3848
|
return { added, removed, modified, breaking, additive };
|
|
3863
3849
|
}
|
|
3864
3850
|
|
|
3865
|
-
// src/differ/file-map.ts
|
|
3866
|
-
function mapChangesToFiles(changes, emitter, ctx) {
|
|
3867
|
-
const regenerate = /* @__PURE__ */ new Set();
|
|
3868
|
-
const toDelete = /* @__PURE__ */ new Set();
|
|
3869
|
-
const modelFiles = /* @__PURE__ */ new Map();
|
|
3870
|
-
const enumFiles = /* @__PURE__ */ new Map();
|
|
3871
|
-
const serviceFiles = /* @__PURE__ */ new Map();
|
|
3872
|
-
for (const model of ctx.spec.models) {
|
|
3873
|
-
const files = emitter.generateModels([model], ctx);
|
|
3874
|
-
const sigs = emitter.generateTypeSignatures?.({ ...ctx.spec, models: [model], enums: [], services: [] }, ctx) ?? [];
|
|
3875
|
-
modelFiles.set(
|
|
3876
|
-
model.name,
|
|
3877
|
-
[...files, ...sigs].map((f) => f.path)
|
|
3878
|
-
);
|
|
3879
|
-
}
|
|
3880
|
-
for (const enumDef of ctx.spec.enums) {
|
|
3881
|
-
const files = emitter.generateEnums([enumDef], ctx);
|
|
3882
|
-
enumFiles.set(
|
|
3883
|
-
enumDef.name,
|
|
3884
|
-
files.map((f) => f.path)
|
|
3885
|
-
);
|
|
3886
|
-
}
|
|
3887
|
-
for (const service of ctx.spec.services) {
|
|
3888
|
-
const files = [
|
|
3889
|
-
...emitter.generateResources([service], ctx),
|
|
3890
|
-
...emitter.generateTests({ ...ctx.spec, services: [service], models: [], enums: [] }, ctx)
|
|
3891
|
-
];
|
|
3892
|
-
const sigs = emitter.generateTypeSignatures?.({ ...ctx.spec, services: [service], models: [], enums: [] }, ctx) ?? [];
|
|
3893
|
-
serviceFiles.set(
|
|
3894
|
-
service.name,
|
|
3895
|
-
[...files, ...sigs].map((f) => f.path)
|
|
3896
|
-
);
|
|
3897
|
-
}
|
|
3898
|
-
const modelToServices = buildReferenceGraph(ctx.spec);
|
|
3899
|
-
for (const change of changes) {
|
|
3900
|
-
switch (change.kind) {
|
|
3901
|
-
case "model-added":
|
|
3902
|
-
case "model-modified": {
|
|
3903
|
-
const files = modelFiles.get(change.name) ?? [];
|
|
3904
|
-
files.forEach((f) => regenerate.add(f));
|
|
3905
|
-
const refs = modelToServices.get(change.name) ?? [];
|
|
3906
|
-
for (const svc of refs) {
|
|
3907
|
-
const svcFiles = serviceFiles.get(svc) ?? [];
|
|
3908
|
-
svcFiles.forEach((f) => regenerate.add(f));
|
|
3909
|
-
}
|
|
3910
|
-
break;
|
|
3911
|
-
}
|
|
3912
|
-
case "model-removed": {
|
|
3913
|
-
const files = modelFiles.get(change.name) ?? [];
|
|
3914
|
-
files.forEach((f) => toDelete.add(f));
|
|
3915
|
-
const refs = modelToServices.get(change.name) ?? [];
|
|
3916
|
-
for (const svc of refs) {
|
|
3917
|
-
const svcFiles = serviceFiles.get(svc) ?? [];
|
|
3918
|
-
svcFiles.forEach((f) => regenerate.add(f));
|
|
3919
|
-
}
|
|
3920
|
-
break;
|
|
3921
|
-
}
|
|
3922
|
-
case "enum-added":
|
|
3923
|
-
case "enum-modified": {
|
|
3924
|
-
const files = enumFiles.get(change.name) ?? [];
|
|
3925
|
-
files.forEach((f) => regenerate.add(f));
|
|
3926
|
-
const refs = modelToServices.get(change.name) ?? [];
|
|
3927
|
-
for (const svc of refs) {
|
|
3928
|
-
const svcFiles = serviceFiles.get(svc) ?? [];
|
|
3929
|
-
svcFiles.forEach((f) => regenerate.add(f));
|
|
3930
|
-
}
|
|
3931
|
-
break;
|
|
3932
|
-
}
|
|
3933
|
-
case "enum-removed": {
|
|
3934
|
-
const files = enumFiles.get(change.name) ?? [];
|
|
3935
|
-
files.forEach((f) => toDelete.add(f));
|
|
3936
|
-
const refs = modelToServices.get(change.name) ?? [];
|
|
3937
|
-
for (const svc of refs) {
|
|
3938
|
-
const svcFiles = serviceFiles.get(svc) ?? [];
|
|
3939
|
-
svcFiles.forEach((f) => regenerate.add(f));
|
|
3940
|
-
}
|
|
3941
|
-
break;
|
|
3942
|
-
}
|
|
3943
|
-
case "service-added": {
|
|
3944
|
-
const files = serviceFiles.get(change.name) ?? [];
|
|
3945
|
-
files.forEach((f) => regenerate.add(f));
|
|
3946
|
-
break;
|
|
3947
|
-
}
|
|
3948
|
-
case "service-removed": {
|
|
3949
|
-
const files = serviceFiles.get(change.name) ?? [];
|
|
3950
|
-
files.forEach((f) => toDelete.add(f));
|
|
3951
|
-
break;
|
|
3952
|
-
}
|
|
3953
|
-
case "operation-added":
|
|
3954
|
-
case "operation-removed":
|
|
3955
|
-
case "operation-modified": {
|
|
3956
|
-
const files = serviceFiles.get(change.serviceName) ?? [];
|
|
3957
|
-
files.forEach((f) => regenerate.add(f));
|
|
3958
|
-
break;
|
|
3959
|
-
}
|
|
3960
|
-
}
|
|
3961
|
-
}
|
|
3962
|
-
for (const f of toDelete) {
|
|
3963
|
-
regenerate.delete(f);
|
|
3964
|
-
}
|
|
3965
|
-
return {
|
|
3966
|
-
regenerate: [...regenerate].sort(),
|
|
3967
|
-
delete: [...toDelete].sort()
|
|
3968
|
-
};
|
|
3969
|
-
}
|
|
3970
|
-
function buildReferenceGraph(spec) {
|
|
3971
|
-
const refs = /* @__PURE__ */ new Map();
|
|
3972
|
-
function trackRef(typeRef, serviceName) {
|
|
3973
|
-
const addName = (name) => {
|
|
3974
|
-
if (!refs.has(name)) refs.set(name, /* @__PURE__ */ new Set());
|
|
3975
|
-
refs.get(name).add(serviceName);
|
|
3976
|
-
};
|
|
3977
|
-
walkTypeRef(typeRef, {
|
|
3978
|
-
model: (r) => addName(r.name),
|
|
3979
|
-
enum: (r) => addName(r.name)
|
|
3980
|
-
});
|
|
3981
|
-
}
|
|
3982
|
-
for (const service of spec.services) {
|
|
3983
|
-
for (const op of service.operations) {
|
|
3984
|
-
for (const p of [...op.pathParams, ...op.queryParams, ...op.headerParams]) {
|
|
3985
|
-
trackRef(p.type, service.name);
|
|
3986
|
-
}
|
|
3987
|
-
if (op.requestBody) trackRef(op.requestBody, service.name);
|
|
3988
|
-
trackRef(op.response, service.name);
|
|
3989
|
-
}
|
|
3990
|
-
}
|
|
3991
|
-
const modelDeps = /* @__PURE__ */ new Map();
|
|
3992
|
-
function collectDeps(typeRef, deps) {
|
|
3993
|
-
walkTypeRef(typeRef, {
|
|
3994
|
-
model: (r) => deps.add(r.name),
|
|
3995
|
-
enum: (r) => deps.add(r.name)
|
|
3996
|
-
});
|
|
3997
|
-
}
|
|
3998
|
-
for (const model of spec.models) {
|
|
3999
|
-
const deps = /* @__PURE__ */ new Set();
|
|
4000
|
-
for (const field of model.fields) {
|
|
4001
|
-
collectDeps(field.type, deps);
|
|
4002
|
-
}
|
|
4003
|
-
deps.delete(model.name);
|
|
4004
|
-
if (deps.size > 0) {
|
|
4005
|
-
modelDeps.set(model.name, deps);
|
|
4006
|
-
}
|
|
4007
|
-
}
|
|
4008
|
-
let changed = true;
|
|
4009
|
-
while (changed) {
|
|
4010
|
-
changed = false;
|
|
4011
|
-
for (const [modelName, services] of refs) {
|
|
4012
|
-
const deps = modelDeps.get(modelName);
|
|
4013
|
-
if (!deps) continue;
|
|
4014
|
-
for (const dep of deps) {
|
|
4015
|
-
if (!refs.has(dep)) refs.set(dep, /* @__PURE__ */ new Set());
|
|
4016
|
-
const depServices = refs.get(dep);
|
|
4017
|
-
for (const svc of services) {
|
|
4018
|
-
if (!depServices.has(svc)) {
|
|
4019
|
-
depServices.add(svc);
|
|
4020
|
-
changed = true;
|
|
4021
|
-
}
|
|
4022
|
-
}
|
|
4023
|
-
}
|
|
4024
|
-
}
|
|
4025
|
-
}
|
|
4026
|
-
const result = /* @__PURE__ */ new Map();
|
|
4027
|
-
for (const [name, svcSet] of refs) {
|
|
4028
|
-
result.set(name, [...svcSet]);
|
|
4029
|
-
}
|
|
4030
|
-
return result;
|
|
4031
|
-
}
|
|
4032
|
-
|
|
4033
|
-
// src/engine/incremental.ts
|
|
4034
|
-
import * as fs3 from "fs/promises";
|
|
4035
|
-
import * as path4 from "path";
|
|
4036
|
-
async function generateIncremental(oldSpec, newSpec, emitter, options) {
|
|
4037
|
-
const diff = diffSpecs(oldSpec, newSpec);
|
|
4038
|
-
if (diff.changes.length === 0) {
|
|
4039
|
-
return { generated: [], deleted: [], diff };
|
|
4040
|
-
}
|
|
4041
|
-
const ctx = buildEmitterContext(newSpec, options);
|
|
4042
|
-
const affected = mapChangesToFiles(diff.changes, emitter, ctx);
|
|
4043
|
-
const allFiles = generateAllFiles(newSpec, emitter, ctx);
|
|
4044
|
-
const header = emitter.fileHeader();
|
|
4045
|
-
const affectedSet = new Set(affected.regenerate);
|
|
4046
|
-
const generated = applyFileHeaders(
|
|
4047
|
-
allFiles.filter((f) => affectedSet.has(f.path)),
|
|
4048
|
-
header
|
|
4049
|
-
);
|
|
4050
|
-
if (!options.dryRun) {
|
|
4051
|
-
const writeResult = await writeFiles(generated, options.outputDir, {
|
|
4052
|
-
language: emitter.language,
|
|
4053
|
-
header
|
|
4054
|
-
});
|
|
4055
|
-
if (writeResult.ignored.length > 0) {
|
|
4056
|
-
console.log(`Ignored ${writeResult.ignored.length} files (@oagen-ignore-file)`);
|
|
4057
|
-
}
|
|
4058
|
-
if (options.target) {
|
|
4059
|
-
const targetResult = await integrateGeneratedFiles({
|
|
4060
|
-
files: generated,
|
|
4061
|
-
language: emitter.language,
|
|
4062
|
-
targetDir: options.target,
|
|
4063
|
-
header
|
|
4064
|
-
});
|
|
4065
|
-
if (targetResult.written.length > 0) {
|
|
4066
|
-
console.log(`Target: created ${targetResult.written.length} new files`);
|
|
4067
|
-
}
|
|
4068
|
-
if (targetResult.merged.length > 0) {
|
|
4069
|
-
console.log(`Target: merged into ${targetResult.merged.length} existing files (additive only)`);
|
|
4070
|
-
}
|
|
4071
|
-
if (targetResult.skipped.length > 0) {
|
|
4072
|
-
console.log(`Target: skipped ${targetResult.skipped.length} files (excluded or no grammar)`);
|
|
4073
|
-
}
|
|
4074
|
-
if (targetResult.ignored.length > 0) {
|
|
4075
|
-
console.log(`Target: ignored ${targetResult.ignored.length} files (@oagen-ignore-file)`);
|
|
4076
|
-
}
|
|
4077
|
-
}
|
|
4078
|
-
if (options.force) {
|
|
4079
|
-
for (const filePath of affected.delete) {
|
|
4080
|
-
const fullPath = path4.join(options.outputDir, filePath);
|
|
4081
|
-
await fs3.rm(fullPath, { force: true });
|
|
4082
|
-
}
|
|
4083
|
-
}
|
|
4084
|
-
}
|
|
4085
|
-
return {
|
|
4086
|
-
generated,
|
|
4087
|
-
deleted: options.force ? affected.delete : [],
|
|
4088
|
-
diff
|
|
4089
|
-
};
|
|
4090
|
-
}
|
|
4091
|
-
|
|
4092
3851
|
// src/cli/diff.ts
|
|
4093
3852
|
async function diffCommand(opts) {
|
|
4094
3853
|
const parseOptions = { operationIdTransform: opts.operationIdTransform };
|
|
4095
|
-
let [oldSpec, newSpec] = await Promise.all([
|
|
4096
|
-
parseSpec(opts.old, parseOptions),
|
|
4097
|
-
parseSpec(opts.new, parseOptions)
|
|
4098
|
-
]);
|
|
3854
|
+
let [oldSpec, newSpec] = await Promise.all([parseSpec(opts.old, parseOptions), parseSpec(opts.new, parseOptions)]);
|
|
4099
3855
|
if (opts.docUrl) {
|
|
4100
3856
|
oldSpec = expandDocUrls(oldSpec, opts.docUrl);
|
|
4101
3857
|
newSpec = expandDocUrls(newSpec, opts.docUrl);
|
|
4102
3858
|
}
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
);
|
|
4111
|
-
}
|
|
4112
|
-
if (!opts.lang || !opts.output) {
|
|
4113
|
-
throw new CommandError("--lang and --output are required for incremental generation", "", 1);
|
|
4114
|
-
}
|
|
4115
|
-
let apiSurface;
|
|
4116
|
-
let overlayLookup;
|
|
4117
|
-
if (opts.apiSurface) {
|
|
4118
|
-
const ctx = loadOverlayContext({
|
|
4119
|
-
apiSurfacePath: opts.apiSurface,
|
|
4120
|
-
manifestPath: opts.manifest,
|
|
4121
|
-
outputDir: opts.output,
|
|
4122
|
-
lang: opts.lang
|
|
4123
|
-
});
|
|
4124
|
-
apiSurface = ctx.apiSurface;
|
|
4125
|
-
overlayLookup = ctx.overlayLookup;
|
|
4126
|
-
}
|
|
4127
|
-
const emitter = getEmitter(opts.lang);
|
|
4128
|
-
const result = await generateIncremental(oldSpec, newSpec, emitter, {
|
|
4129
|
-
namespace: newSpec.name,
|
|
4130
|
-
outputDir: opts.output,
|
|
4131
|
-
force: opts.force,
|
|
4132
|
-
target: opts.target,
|
|
4133
|
-
apiSurface,
|
|
4134
|
-
overlayLookup
|
|
4135
|
-
});
|
|
4136
|
-
if (result.diff.changes.length === 0) {
|
|
4137
|
-
console.log("No changes detected");
|
|
4138
|
-
} else {
|
|
4139
|
-
console.log(`Regenerated ${result.generated.length} files`);
|
|
4140
|
-
if (result.deleted.length > 0) {
|
|
4141
|
-
console.log(`Deleted ${result.deleted.length} files`);
|
|
4142
|
-
}
|
|
4143
|
-
if (!opts.force && result.diff.changes.some((c) => c.kind.endsWith("-removed"))) {
|
|
4144
|
-
console.log("Use --force to delete files for removed schemas");
|
|
4145
|
-
}
|
|
4146
|
-
}
|
|
3859
|
+
const diff = diffSpecs(oldSpec, newSpec);
|
|
3860
|
+
console.log(JSON.stringify(diff, null, 2));
|
|
3861
|
+
throw new CommandError(
|
|
3862
|
+
"",
|
|
3863
|
+
"",
|
|
3864
|
+
diff.summary.breaking > 0 ? 2 : diff.summary.modified > 0 || diff.summary.removed > 0 ? 1 : diff.summary.added > 0 ? 1 : 0
|
|
3865
|
+
);
|
|
4147
3866
|
}
|
|
4148
3867
|
|
|
4149
3868
|
// src/cli/extract.ts
|
|
@@ -4158,7 +3877,7 @@ async function extractCommand(opts) {
|
|
|
4158
3877
|
}
|
|
4159
3878
|
|
|
4160
3879
|
// src/cli/verify.ts
|
|
4161
|
-
import { existsSync as
|
|
3880
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
4162
3881
|
|
|
4163
3882
|
// src/compat/spec-filter.ts
|
|
4164
3883
|
function specDerivedNames(spec, hints) {
|
|
@@ -4257,9 +3976,9 @@ function filterSurface(surface, allowedNames, opts) {
|
|
|
4257
3976
|
let filteredClasses = filterRecord(surface.classes, allowedNames);
|
|
4258
3977
|
if (methodPaths) {
|
|
4259
3978
|
const serviceClassNames = /* @__PURE__ */ new Set();
|
|
4260
|
-
for (const
|
|
4261
|
-
const dot =
|
|
4262
|
-
if (dot !== -1) serviceClassNames.add(
|
|
3979
|
+
for (const path6 of methodPaths) {
|
|
3980
|
+
const dot = path6.indexOf(".");
|
|
3981
|
+
if (dot !== -1) serviceClassNames.add(path6.slice(0, dot));
|
|
4263
3982
|
}
|
|
4264
3983
|
const newClasses = {};
|
|
4265
3984
|
for (const [name, cls] of Object.entries(filteredClasses)) {
|
|
@@ -4686,17 +4405,17 @@ function diffSurfaces(baseline, candidate, hints) {
|
|
|
4686
4405
|
additions.push({ symbolPath: name, symbolType: "enum" });
|
|
4687
4406
|
}
|
|
4688
4407
|
}
|
|
4689
|
-
for (const [
|
|
4690
|
-
const candExports = candidate.exports[
|
|
4408
|
+
for (const [path6, baseExports] of Object.entries(baseline.exports)) {
|
|
4409
|
+
const candExports = candidate.exports[path6];
|
|
4691
4410
|
if (!candExports) {
|
|
4692
4411
|
for (const exp of baseExports) {
|
|
4693
4412
|
violations.push({
|
|
4694
4413
|
category: "export-structure",
|
|
4695
4414
|
severity: "warning",
|
|
4696
|
-
symbolPath: `exports[${
|
|
4415
|
+
symbolPath: `exports[${path6}].${exp}`,
|
|
4697
4416
|
baseline: exp,
|
|
4698
4417
|
candidate: "(missing)",
|
|
4699
|
-
message: `Export "${exp}" from "${
|
|
4418
|
+
message: `Export "${exp}" from "${path6}" not found in generated output`
|
|
4700
4419
|
});
|
|
4701
4420
|
}
|
|
4702
4421
|
continue;
|
|
@@ -4707,10 +4426,10 @@ function diffSurfaces(baseline, candidate, hints) {
|
|
|
4707
4426
|
violations.push({
|
|
4708
4427
|
category: "export-structure",
|
|
4709
4428
|
severity: "warning",
|
|
4710
|
-
symbolPath: `exports[${
|
|
4429
|
+
symbolPath: `exports[${path6}].${exp}`,
|
|
4711
4430
|
baseline: exp,
|
|
4712
4431
|
candidate: "(missing)",
|
|
4713
|
-
message: `Export "${exp}" from "${
|
|
4432
|
+
message: `Export "${exp}" from "${path6}" not found in generated output`
|
|
4714
4433
|
});
|
|
4715
4434
|
}
|
|
4716
4435
|
}
|
|
@@ -4764,8 +4483,8 @@ async function runCompatCheck(baseline, outputDir, lang, spec) {
|
|
|
4764
4483
|
}
|
|
4765
4484
|
|
|
4766
4485
|
// src/verify/run-overlay-retry-loop.ts
|
|
4767
|
-
import { existsSync as
|
|
4768
|
-
import * as
|
|
4486
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, unlinkSync, statSync, openSync, readSync, closeSync } from "fs";
|
|
4487
|
+
import * as path3 from "path";
|
|
4769
4488
|
var PATCHABLE_CATEGORIES = /* @__PURE__ */ new Set(["public-api", "export-structure"]);
|
|
4770
4489
|
var GENERATED_MARKERS = ["Code generated by oagen", "auto-generated by oagen", "This file is auto-generated"];
|
|
4771
4490
|
function cleanGeneratedFiles(dir) {
|
|
@@ -4776,7 +4495,7 @@ function cleanGeneratedFiles(dir) {
|
|
|
4776
4495
|
return;
|
|
4777
4496
|
}
|
|
4778
4497
|
for (const entry of entries) {
|
|
4779
|
-
const full =
|
|
4498
|
+
const full = path3.join(dir, entry);
|
|
4780
4499
|
let stat;
|
|
4781
4500
|
try {
|
|
4782
4501
|
stat = statSync(full);
|
|
@@ -4805,8 +4524,8 @@ function cleanGeneratedFiles(dir) {
|
|
|
4805
4524
|
}
|
|
4806
4525
|
}
|
|
4807
4526
|
function loadManifestFromDir(outputDir) {
|
|
4808
|
-
const manifestPath =
|
|
4809
|
-
if (!
|
|
4527
|
+
const manifestPath = path3.join(outputDir, "smoke-manifest.json");
|
|
4528
|
+
if (!existsSync2(manifestPath)) return void 0;
|
|
4810
4529
|
try {
|
|
4811
4530
|
const parsed = JSON.parse(readFileSync2(manifestPath, "utf-8"));
|
|
4812
4531
|
if (Array.isArray(parsed)) return parsed;
|
|
@@ -4994,17 +4713,17 @@ function runStalenessCheck(baseline, oldSpec, newSpec, lang) {
|
|
|
4994
4713
|
|
|
4995
4714
|
// src/verify/run-smoke-check.ts
|
|
4996
4715
|
import { execFileSync } from "child_process";
|
|
4997
|
-
import { existsSync as
|
|
4716
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
4998
4717
|
import { fileURLToPath } from "url";
|
|
4999
|
-
import * as
|
|
4718
|
+
import * as path4 from "path";
|
|
5000
4719
|
var __filename = fileURLToPath(import.meta.url);
|
|
5001
|
-
var __dirname =
|
|
4720
|
+
var __dirname = path4.dirname(__filename);
|
|
5002
4721
|
function resolveScript(scriptRelPath) {
|
|
5003
|
-
const distScript =
|
|
5004
|
-
if (
|
|
4722
|
+
const distScript = path4.resolve(__dirname, "..", "..", "dist", "scripts", scriptRelPath.replace(/\.ts$/, ".js"));
|
|
4723
|
+
if (existsSync3(distScript)) {
|
|
5005
4724
|
return { bin: "node", script: distScript };
|
|
5006
4725
|
}
|
|
5007
|
-
const srcScript =
|
|
4726
|
+
const srcScript = path4.resolve(__dirname, "..", "..", "scripts", scriptRelPath);
|
|
5008
4727
|
return { bin: "npx", script: srcScript };
|
|
5009
4728
|
}
|
|
5010
4729
|
function runPackagedScript(scriptRelPath, args) {
|
|
@@ -5016,7 +4735,7 @@ function runSmokeCheck(opts) {
|
|
|
5016
4735
|
const { spec, lang, output, rawResults, smokeConfig, smokeRunner } = opts;
|
|
5017
4736
|
let baselinePath = rawResults ?? "smoke-results-raw.json";
|
|
5018
4737
|
let generatedBaseline = false;
|
|
5019
|
-
if (!rawResults && !
|
|
4738
|
+
if (!rawResults && !existsSync3("smoke-results-raw.json")) {
|
|
5020
4739
|
if (!spec) {
|
|
5021
4740
|
throw new CommandError(
|
|
5022
4741
|
"error: --spec <path> or OPENAPI_SPEC_PATH env var is required when no raw baseline exists",
|
|
@@ -5045,10 +4764,10 @@ function runSmokeCheck(opts) {
|
|
|
5045
4764
|
}
|
|
5046
4765
|
return { passed: true, baselinePath, generatedBaseline };
|
|
5047
4766
|
} catch {
|
|
5048
|
-
if (
|
|
4767
|
+
if (existsSync3("smoke-compile-errors.json")) {
|
|
5049
4768
|
return { passed: false, compileErrors: true, baselinePath, generatedBaseline };
|
|
5050
4769
|
}
|
|
5051
|
-
const findingsCount =
|
|
4770
|
+
const findingsCount = existsSync3("smoke-diff-findings.json") ? JSON.parse(readFileSync3("smoke-diff-findings.json", "utf-8")).length : void 0;
|
|
5052
4771
|
return { passed: false, findingsCount, baselinePath, generatedBaseline };
|
|
5053
4772
|
}
|
|
5054
4773
|
}
|
|
@@ -5206,7 +4925,7 @@ ${separator}`);
|
|
|
5206
4925
|
}
|
|
5207
4926
|
stepNum++;
|
|
5208
4927
|
}
|
|
5209
|
-
const needsBaselineStep = !rawResults && !
|
|
4928
|
+
const needsBaselineStep = !rawResults && !existsSync4("smoke-results-raw.json");
|
|
5210
4929
|
if (needsBaselineStep) {
|
|
5211
4930
|
console.log(`
|
|
5212
4931
|
${separator}`);
|
|
@@ -5259,7 +4978,7 @@ Remediation guide (by finding type):
|
|
|
5259
4978
|
|
|
5260
4979
|
// src/cli/init.ts
|
|
5261
4980
|
import { resolve as resolve3, relative } from "path";
|
|
5262
|
-
import { existsSync as
|
|
4981
|
+
import { existsSync as existsSync5, mkdirSync, writeFileSync as writeFileSync3, readFileSync as readFileSync5 } from "fs";
|
|
5263
4982
|
|
|
5264
4983
|
// src/cli/templates/init.ts
|
|
5265
4984
|
function oagenScripts(lang) {
|
|
@@ -5425,7 +5144,7 @@ export const ${varName}: Emitter = {
|
|
|
5425
5144
|
async function initCommand(opts) {
|
|
5426
5145
|
const projectDir = resolve3(opts.project ?? ".");
|
|
5427
5146
|
const lang = opts.lang;
|
|
5428
|
-
if (
|
|
5147
|
+
if (existsSync5(resolve3(projectDir, "oagen.config.ts"))) {
|
|
5429
5148
|
throw new Error("Project already initialized \u2014 oagen.config.ts exists");
|
|
5430
5149
|
}
|
|
5431
5150
|
const dirs = [`src/${lang}`, "test", "smoke", "docs/sdk-architecture"];
|
|
@@ -5442,7 +5161,7 @@ async function initCommand(opts) {
|
|
|
5442
5161
|
];
|
|
5443
5162
|
const packageJsonPath = resolve3(projectDir, "package.json");
|
|
5444
5163
|
let packageJsonContent;
|
|
5445
|
-
if (
|
|
5164
|
+
if (existsSync5(packageJsonPath)) {
|
|
5446
5165
|
const existingContent = readFileSync5(packageJsonPath, "utf-8");
|
|
5447
5166
|
const existingPkg = JSON.parse(existingContent);
|
|
5448
5167
|
const newPkg = JSON.parse(packageJson(lang));
|
|
@@ -5467,7 +5186,7 @@ async function initCommand(opts) {
|
|
|
5467
5186
|
}
|
|
5468
5187
|
const gitignorePath = resolve3(projectDir, ".gitignore");
|
|
5469
5188
|
let gitignoreContent;
|
|
5470
|
-
if (
|
|
5189
|
+
if (existsSync5(gitignorePath)) {
|
|
5471
5190
|
const existingContent = readFileSync5(gitignorePath, "utf-8");
|
|
5472
5191
|
const newContent = gitignore();
|
|
5473
5192
|
const existingLines = new Set(
|
|
@@ -5505,14 +5224,14 @@ async function initCommand(opts) {
|
|
|
5505
5224
|
}
|
|
5506
5225
|
|
|
5507
5226
|
// src/cli/config-loader.ts
|
|
5508
|
-
import * as
|
|
5509
|
-
import { existsSync as
|
|
5227
|
+
import * as path5 from "path";
|
|
5228
|
+
import { existsSync as existsSync6 } from "fs";
|
|
5510
5229
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
5511
5230
|
var CONFIG_NAMES = ["oagen.config.ts", "oagen.config.js", "oagen.config.mjs"];
|
|
5512
5231
|
async function loadConfig(cwd = process.cwd()) {
|
|
5513
5232
|
for (const name of CONFIG_NAMES) {
|
|
5514
|
-
const configPath =
|
|
5515
|
-
if (!
|
|
5233
|
+
const configPath = path5.resolve(cwd, name);
|
|
5234
|
+
if (!existsSync6(configPath)) continue;
|
|
5516
5235
|
try {
|
|
5517
5236
|
const mod = await import(pathToFileURL2(configPath).href);
|
|
5518
5237
|
const config = mod.default ?? mod;
|
|
@@ -5578,7 +5297,7 @@ program.command("generate").description("Run a registered emitter against an Ope
|
|
|
5578
5297
|
handleError
|
|
5579
5298
|
);
|
|
5580
5299
|
});
|
|
5581
|
-
program.command("diff").description("
|
|
5300
|
+
program.command("diff").description("Compare two OpenAPI specs and output a diff report").requiredOption("--old <path>", "Path to old spec").requiredOption("--new <path>", "Path to new spec").action((opts) => {
|
|
5582
5301
|
diffCommand({ ...opts, operationIdTransform: configOperationIdTransform, docUrl: configDocUrl }).catch(handleError);
|
|
5583
5302
|
});
|
|
5584
5303
|
program.command("extract").description("Extract public API surface from a live SDK").requiredOption("--sdk-path <path>", "Path to the live SDK").requiredOption("--lang <language>", "Target language").option("--output <path>", "Output file path").action((opts) => {
|