@workos/oagen 0.20.0 → 0.21.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/dist/cli/index.mjs +387 -55
- package/dist/cli/index.mjs.map +1 -1
- package/dist/{diff-C8mjApjv.mjs → diff-Dobf9PHC.mjs} +210 -34
- package/dist/diff-Dobf9PHC.mjs.map +1 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +180 -24
- package/dist/index.mjs.map +1 -1
- package/dist/scripts/smoke/baseline.mjs +178 -22
- package/dist/scripts/smoke/baseline.mjs.map +1 -1
- package/dist/smoke.mjs +178 -22
- package/dist/smoke.mjs.map +1 -1
- package/dist/verify/index.mjs +1 -1
- package/package.json +1 -1
- package/skills/generate-smoke-test/SKILL.md +19 -41
- package/skills/generate-smoke-test/references/implementation-patterns.md +87 -0
- package/dist/diff-C8mjApjv.mjs.map +0 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -433,7 +433,7 @@ function qualifyInlineModelName(baseName, parentName) {
|
|
|
433
433
|
function extractInlineModelsFromSchemas(schemas) {
|
|
434
434
|
if (!schemas) return [];
|
|
435
435
|
const inlineModels = [];
|
|
436
|
-
for (const [schemaName, schema] of Object.entries(schemas)) extractInlineModelsFromProperties(schema, inlineModels,
|
|
436
|
+
for (const [schemaName, schema] of Object.entries(schemas)) extractInlineModelsFromProperties(schema, inlineModels, resolveSchemaName(schemaName));
|
|
437
437
|
return inlineModels;
|
|
438
438
|
}
|
|
439
439
|
function extractInlineModelsFromProperties(schema, results, parentName) {
|
|
@@ -837,9 +837,10 @@ function collectLiteralStringConsts(variants) {
|
|
|
837
837
|
function buildSyntheticEnumRef(values, contextName, parentModelName) {
|
|
838
838
|
const baseName = toPascalCase(contextName ?? "UnknownEnum");
|
|
839
839
|
const cleanParent = parentModelName ? stripListItemMarkers(parentModelName) : void 0;
|
|
840
|
+
const qualifiedName = cleanParent && !baseName.startsWith(cleanParent) ? `${cleanParent}${baseName}` : baseName;
|
|
840
841
|
return {
|
|
841
842
|
kind: "enum",
|
|
842
|
-
name:
|
|
843
|
+
name: activeSchemaNameTransform ? activeSchemaNameTransform(qualifiedName) : qualifiedName,
|
|
843
844
|
values
|
|
844
845
|
};
|
|
845
846
|
}
|
|
@@ -1047,6 +1048,36 @@ function extractAllOfModel(name, schema, schemas) {
|
|
|
1047
1048
|
};
|
|
1048
1049
|
}
|
|
1049
1050
|
/**
|
|
1051
|
+
* Walk through `allOf` wrappers to collect the effective top-level properties
|
|
1052
|
+
* of a schema. Stops at `oneOf` / `anyOf` boundaries — those are different
|
|
1053
|
+
* variant axes, not part of the same flat object.
|
|
1054
|
+
*
|
|
1055
|
+
* Used by `detectAllOfVariantDiscriminator` and `deriveDiscriminatedVariantName`
|
|
1056
|
+
* so a oneOf variant of the form `{ allOf: [{ properties: { … } }, { oneOf: […] }] }`
|
|
1057
|
+
* still surfaces its top-level `properties` (in particular its discriminator
|
|
1058
|
+
* `const`) instead of being treated as having none.
|
|
1059
|
+
*/
|
|
1060
|
+
function getEffectiveProperties(schema) {
|
|
1061
|
+
const props = {};
|
|
1062
|
+
const visit = (s) => {
|
|
1063
|
+
if (s.properties) {
|
|
1064
|
+
for (const [k, v] of Object.entries(s.properties)) if (!(k in props) && v) props[k] = v;
|
|
1065
|
+
}
|
|
1066
|
+
if (s.allOf) for (const sub of s.allOf) visit(sub);
|
|
1067
|
+
};
|
|
1068
|
+
visit(schema);
|
|
1069
|
+
return props;
|
|
1070
|
+
}
|
|
1071
|
+
function getEffectiveConstPropertyValue(schema, property) {
|
|
1072
|
+
const direct = getConstPropertyValue(schema, property);
|
|
1073
|
+
if (direct !== null) return direct;
|
|
1074
|
+
const effective = getEffectiveProperties(schema)[property];
|
|
1075
|
+
if (!effective) return null;
|
|
1076
|
+
if (typeof effective.const === "string") return effective.const;
|
|
1077
|
+
if (Array.isArray(effective.enum) && effective.enum.length === 1 && typeof effective.enum[0] === "string") return effective.enum[0];
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1050
1081
|
* Detect an implicit discriminator on an allOf+oneOf where every variant is an
|
|
1051
1082
|
* object that pins the same property to a distinct const value. Unlike
|
|
1052
1083
|
* detectConstPropertyDiscriminator (which requires $ref or title for variant
|
|
@@ -1054,13 +1085,17 @@ function extractAllOfModel(name, schema, schemas) {
|
|
|
1054
1085
|
* using deriveDiscriminatedVariantName — matching how extractDiscriminatedAllOfVariantModels
|
|
1055
1086
|
* names those models.
|
|
1056
1087
|
*
|
|
1088
|
+
* Walks through allOf wrappers on each variant so a oneOf branch like
|
|
1089
|
+
* { allOf: [{ properties: { type: { const: 'oauth' } } }, { oneOf: […] }] }
|
|
1090
|
+
* still surfaces `type` as a candidate discriminator.
|
|
1091
|
+
*
|
|
1057
1092
|
* Returns null when no discriminator can be reliably detected.
|
|
1058
1093
|
*/
|
|
1059
1094
|
function detectAllOfVariantDiscriminator(variants, fallbackName) {
|
|
1060
1095
|
if (variants.length < 2) return null;
|
|
1061
|
-
const
|
|
1096
|
+
const firstProps = getEffectiveProperties(variants[0] ?? {});
|
|
1062
1097
|
let candidateProperty = null;
|
|
1063
|
-
for (const propName of
|
|
1098
|
+
for (const propName of Object.keys(firstProps)) if (variants.every((v) => getEffectiveConstPropertyValue(v, propName) !== null)) {
|
|
1064
1099
|
candidateProperty = propName;
|
|
1065
1100
|
break;
|
|
1066
1101
|
}
|
|
@@ -1068,7 +1103,7 @@ function detectAllOfVariantDiscriminator(variants, fallbackName) {
|
|
|
1068
1103
|
const mapping = {};
|
|
1069
1104
|
const seenModelNames = /* @__PURE__ */ new Set();
|
|
1070
1105
|
for (const v of variants) {
|
|
1071
|
-
const value =
|
|
1106
|
+
const value = getEffectiveConstPropertyValue(v, candidateProperty);
|
|
1072
1107
|
if (value === null) return null;
|
|
1073
1108
|
const modelName = deriveDiscriminatedVariantName(v, fallbackName);
|
|
1074
1109
|
if (seenModelNames.has(modelName)) return null;
|
|
@@ -1083,10 +1118,28 @@ function detectAllOfVariantDiscriminator(variants, fallbackName) {
|
|
|
1083
1118
|
function extractDiscriminatedAllOfVariantModels(schema, fallbackName) {
|
|
1084
1119
|
const models = [];
|
|
1085
1120
|
const seenNames = /* @__PURE__ */ new Set();
|
|
1121
|
+
const baseProperties = {};
|
|
1122
|
+
const baseRequired = /* @__PURE__ */ new Set();
|
|
1123
|
+
for (const subSchema of schema.allOf ?? []) {
|
|
1124
|
+
if (subSchema.oneOf) continue;
|
|
1125
|
+
const flat = subSchema.allOf ? flattenVariantForExtraction(subSchema) : subSchema;
|
|
1126
|
+
if (flat.properties) {
|
|
1127
|
+
for (const [k, v] of Object.entries(flat.properties)) if (!(k in baseProperties) && v) baseProperties[k] = v;
|
|
1128
|
+
}
|
|
1129
|
+
if (flat.required) for (const r of flat.required) baseRequired.add(r);
|
|
1130
|
+
}
|
|
1086
1131
|
for (const subSchema of schema.allOf ?? []) for (const variant of subSchema.oneOf ?? []) {
|
|
1087
|
-
|
|
1132
|
+
const flat = flattenVariantForExtraction(variant);
|
|
1133
|
+
if (!flat.properties || flat.type !== void 0 && flat.type !== "object") continue;
|
|
1134
|
+
const mergedProperties = { ...baseProperties };
|
|
1135
|
+
for (const [k, v] of Object.entries(flat.properties)) if (v) mergedProperties[k] = v;
|
|
1136
|
+
const merged = {
|
|
1137
|
+
type: "object",
|
|
1138
|
+
properties: mergedProperties,
|
|
1139
|
+
required: [...new Set([...baseRequired, ...flat.required ?? []])]
|
|
1140
|
+
};
|
|
1088
1141
|
const variantName = deriveDiscriminatedVariantName(variant, fallbackName);
|
|
1089
|
-
for (const model of extractInlineModelDeep(variantName,
|
|
1142
|
+
for (const model of extractInlineModelDeep(variantName, merged)) {
|
|
1090
1143
|
if (seenNames.has(model.name)) continue;
|
|
1091
1144
|
seenNames.add(model.name);
|
|
1092
1145
|
models.push(model);
|
|
@@ -1094,24 +1147,103 @@ function extractDiscriminatedAllOfVariantModels(schema, fallbackName) {
|
|
|
1094
1147
|
}
|
|
1095
1148
|
return models;
|
|
1096
1149
|
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Flatten a oneOf variant schema into a single object-shaped schema for
|
|
1152
|
+
* variant-model extraction. Recursively merges `allOf` members and folds
|
|
1153
|
+
* any nested `oneOf` into an inline-oneOf field group whose properties are
|
|
1154
|
+
* the union across sub-branches (required only when present in every
|
|
1155
|
+
* sub-branch). Returns the schema unchanged when no flattening is needed.
|
|
1156
|
+
*/
|
|
1157
|
+
function flattenVariantForExtraction(variant) {
|
|
1158
|
+
if (!variant.allOf) return variant;
|
|
1159
|
+
const mergedProperties = {};
|
|
1160
|
+
const mergedRequired = /* @__PURE__ */ new Set();
|
|
1161
|
+
const nestedOneOfs = [];
|
|
1162
|
+
const collect = (s) => {
|
|
1163
|
+
if (s.properties) {
|
|
1164
|
+
for (const [k, v] of Object.entries(s.properties)) if (!(k in mergedProperties) && v) mergedProperties[k] = v;
|
|
1165
|
+
}
|
|
1166
|
+
if (s.required) for (const r of s.required) mergedRequired.add(r);
|
|
1167
|
+
if (s.allOf) for (const sub of s.allOf) collect(sub);
|
|
1168
|
+
if (s.oneOf) nestedOneOfs.push(s.oneOf);
|
|
1169
|
+
};
|
|
1170
|
+
collect(variant);
|
|
1171
|
+
for (const branches of nestedOneOfs) {
|
|
1172
|
+
const flatBranches = branches.map(flattenVariantForExtraction);
|
|
1173
|
+
const branchKeys = /* @__PURE__ */ new Set();
|
|
1174
|
+
for (const b of flatBranches) for (const k of Object.keys(b.properties ?? {})) branchKeys.add(k);
|
|
1175
|
+
for (const key of branchKeys) {
|
|
1176
|
+
const schemas = [];
|
|
1177
|
+
for (const b of flatBranches) {
|
|
1178
|
+
const s = b.properties?.[key];
|
|
1179
|
+
if (s) schemas.push(s);
|
|
1180
|
+
}
|
|
1181
|
+
const merged = mergeBranchPropertySchemas(schemas);
|
|
1182
|
+
if (!(key in mergedProperties)) mergedProperties[key] = merged;
|
|
1183
|
+
if (flatBranches.every((b) => b.properties && key in b.properties) && flatBranches.every((b) => (b.required ?? []).includes(key))) mergedRequired.add(key);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return {
|
|
1187
|
+
type: "object",
|
|
1188
|
+
properties: mergedProperties,
|
|
1189
|
+
required: [...mergedRequired]
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Merge same-named property schemas from across sub-variants. If every
|
|
1194
|
+
* occurrence is a boolean const, widen to plain boolean; if every occurrence
|
|
1195
|
+
* is a distinct string const, widen to plain string. Otherwise return the
|
|
1196
|
+
* first occurrence — emitters that build their own widened types (e.g. the
|
|
1197
|
+
* Node discriminated-models module) re-derive from the raw spec.
|
|
1198
|
+
*/
|
|
1199
|
+
function mergeBranchPropertySchemas(schemas) {
|
|
1200
|
+
if (schemas.length === 0) return {};
|
|
1201
|
+
if (schemas.length === 1) return schemas[0];
|
|
1202
|
+
if (schemas.every((s) => s.type === "boolean" && typeof s.const === "boolean")) return {
|
|
1203
|
+
type: "boolean",
|
|
1204
|
+
description: schemas[0].description
|
|
1205
|
+
};
|
|
1206
|
+
if (schemas.every((s) => s.type === "string" && typeof s.const === "string")) {
|
|
1207
|
+
const values = schemas.map((s) => s.const);
|
|
1208
|
+
if (new Set(values).size === 1) return schemas[0];
|
|
1209
|
+
return {
|
|
1210
|
+
type: "string",
|
|
1211
|
+
description: schemas[0].description
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
return schemas[0];
|
|
1215
|
+
}
|
|
1097
1216
|
function deriveDiscriminatedVariantName(schema, fallbackName) {
|
|
1098
|
-
|
|
1099
|
-
|
|
1217
|
+
const effective = getEffectiveProperties(schema);
|
|
1218
|
+
if (Object.keys(effective).length === 0) return fallbackName;
|
|
1219
|
+
const pickConst = (name) => {
|
|
1220
|
+
const field = effective[name];
|
|
1221
|
+
if (!field) return null;
|
|
1222
|
+
if (typeof field.const === "string") return field.const;
|
|
1223
|
+
if (field.enum && field.enum.length === 1 && typeof field.enum[0] === "string") return field.enum[0];
|
|
1224
|
+
return null;
|
|
1225
|
+
};
|
|
1226
|
+
let constValue = null;
|
|
1227
|
+
for (const preferred of [
|
|
1100
1228
|
"event",
|
|
1101
1229
|
"type",
|
|
1102
1230
|
"object"
|
|
1103
1231
|
]) {
|
|
1104
|
-
|
|
1105
|
-
if (
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
return
|
|
1232
|
+
constValue = pickConst(preferred);
|
|
1233
|
+
if (constValue) break;
|
|
1234
|
+
}
|
|
1235
|
+
if (constValue === null) for (const name of Object.keys(effective)) {
|
|
1236
|
+
constValue = pickConst(name);
|
|
1237
|
+
if (constValue) break;
|
|
1238
|
+
}
|
|
1239
|
+
if (constValue === null) return fallbackName;
|
|
1240
|
+
const pascal = toPascalCase(constValue);
|
|
1241
|
+
if (!pascal) return fallbackName;
|
|
1242
|
+
if (splitWords(constValue).length >= 2) return pascal;
|
|
1243
|
+
if (pascal === fallbackName) return pascal;
|
|
1244
|
+
if (fallbackName.startsWith(pascal)) return fallbackName;
|
|
1245
|
+
if (pascal.endsWith(fallbackName)) return pascal;
|
|
1246
|
+
return `${fallbackName}${pascal}`;
|
|
1115
1247
|
}
|
|
1116
1248
|
function extractInlineModelDeep(name, schema) {
|
|
1117
1249
|
const requiredSet = new Set(schema.required ?? []);
|
|
@@ -1340,9 +1472,10 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
|
|
|
1340
1472
|
if (schema.enum) {
|
|
1341
1473
|
const baseName = toPascalCase(contextName ?? "UnknownEnum");
|
|
1342
1474
|
const cleanParent = parentModelName ? stripListItemMarkers(parentModelName) : void 0;
|
|
1475
|
+
const qualifiedName = cleanParent && !baseName.startsWith(cleanParent) ? `${cleanParent}${baseName}` : baseName;
|
|
1343
1476
|
return {
|
|
1344
1477
|
kind: "enum",
|
|
1345
|
-
name:
|
|
1478
|
+
name: activeSchemaNameTransform ? activeSchemaNameTransform(qualifiedName) : qualifiedName,
|
|
1346
1479
|
values: schema.enum.map((v) => typeof v === "number" ? v : String(v))
|
|
1347
1480
|
};
|
|
1348
1481
|
}
|
|
@@ -2484,6 +2617,27 @@ function collapseJsonSuffixModels(models, services) {
|
|
|
2484
2617
|
rewriteModelRefs(normalizedModels, services, jsonModel.name, baseModel.name);
|
|
2485
2618
|
console.warn(`[oagen] Merged "${jsonModel.name}" into "${baseModel.name}" (${jsonModel.fields.length} fields, superset)`);
|
|
2486
2619
|
}
|
|
2620
|
+
for (const { jsonModel, baseModel } of mergeCandidates) {
|
|
2621
|
+
const jsonPrefix = jsonModel.name;
|
|
2622
|
+
const basePrefix = baseModel.name;
|
|
2623
|
+
for (const model of normalizedModels) {
|
|
2624
|
+
if (toRemove.has(model.name)) continue;
|
|
2625
|
+
if (model.name.length <= jsonPrefix.length) continue;
|
|
2626
|
+
if (!model.name.startsWith(jsonPrefix)) continue;
|
|
2627
|
+
const newName = basePrefix + model.name.slice(jsonPrefix.length);
|
|
2628
|
+
const existing = byName.get(newName);
|
|
2629
|
+
if (existing && !toRemove.has(existing.name)) {
|
|
2630
|
+
toRemove.add(model.name);
|
|
2631
|
+
rewriteModelRefs(normalizedModels, services, model.name, newName);
|
|
2632
|
+
console.warn(`[oagen] Collapsed child "${model.name}" into existing "${newName}"`);
|
|
2633
|
+
} else {
|
|
2634
|
+
rewriteModelRefs(normalizedModels, services, model.name, newName);
|
|
2635
|
+
model.name = newName;
|
|
2636
|
+
byName.set(newName, model);
|
|
2637
|
+
console.warn(`[oagen] Renamed child "${jsonPrefix}…" → "${newName}"`);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2487
2641
|
return normalizedModels.filter((m) => !toRemove.has(m.name));
|
|
2488
2642
|
}
|
|
2489
2643
|
//#endregion
|
|
@@ -2536,8 +2690,10 @@ async function parseSpec(specPath, options) {
|
|
|
2536
2690
|
if (!version.startsWith("3.")) throw new SpecParseError(`Unsupported OpenAPI version: ${version}. oagen requires OpenAPI 3.x`, `Update the spec to OpenAPI 3.0 or 3.1. If you are using Swagger 2.x, convert it first with \`npx swagger2openapi ${specPath}\`.`);
|
|
2537
2691
|
const { models, enums } = extractSchemas(spec.components?.schemas, { schemaNameTransform: options?.schemaNameTransform });
|
|
2538
2692
|
const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform, spec.components?.schemas);
|
|
2693
|
+
const responseNormalizedModels = mergeInlineResponseModels(models, inlineModels);
|
|
2694
|
+
const fieldInlineModels = extractInlineModelsFromSchemas(spec.components?.schemas);
|
|
2539
2695
|
clearSchemaNameTransform();
|
|
2540
|
-
const finalModels = collapseJsonSuffixModels(mergeFieldInlineModels(
|
|
2696
|
+
const finalModels = collapseJsonSuffixModels(mergeFieldInlineModels(responseNormalizedModels, fieldInlineModels), services);
|
|
2541
2697
|
collectInlineEnumsFromModels(finalModels, enums);
|
|
2542
2698
|
collectInlineEnumsFromOperations(services, enums);
|
|
2543
2699
|
const auth = extractAuthSchemes(spec.components?.securitySchemes);
|
|
@@ -4146,10 +4302,10 @@ async function extractClassEndLines(source, language) {
|
|
|
4146
4302
|
const tree = safeParse(await getParser(language), source);
|
|
4147
4303
|
const map = /* @__PURE__ */ new Map();
|
|
4148
4304
|
function processClassNode(node) {
|
|
4149
|
-
const nameNode = node.childForFieldName("name");
|
|
4305
|
+
const nameNode = node.childForFieldName("name") ?? node.children.find((c) => c.type === "type_identifier" || c.type === "identifier");
|
|
4150
4306
|
if (!nameNode) return;
|
|
4151
4307
|
const name = nameNode.text;
|
|
4152
|
-
const body = node.childForFieldName("body");
|
|
4308
|
+
const body = node.childForFieldName("body") ?? node.children.find((c) => c.type === "class_body" || c.type === "body");
|
|
4153
4309
|
const endRow = body ? body.endPosition.row : node.endPosition.row;
|
|
4154
4310
|
if (source[body ? body.endIndex - 1 : node.endIndex - 1] === "}") map.set(name, { bodyEndLine: endRow });
|
|
4155
4311
|
else map.set(name, { bodyEndLine: endRow + 1 });
|
|
@@ -4698,9 +4854,16 @@ async function overwriteWithPreservedRegions(existingContent, generatedContent,
|
|
|
4698
4854
|
if (!existingLines[i].includes("@oagen-ignore-start")) continue;
|
|
4699
4855
|
const startIdx = i;
|
|
4700
4856
|
while (i < existingLines.length && !existingLines[i].includes("@oagen-ignore-end")) i++;
|
|
4857
|
+
let anchor = "";
|
|
4858
|
+
for (let a = i + 1; a < existingLines.length; a++) if (existingLines[a].trim()) {
|
|
4859
|
+
anchor = existingLines[a].trimStart();
|
|
4860
|
+
break;
|
|
4861
|
+
}
|
|
4701
4862
|
blocks.push({
|
|
4702
4863
|
containingClass: findContainingClass(existingLines, startIdx),
|
|
4703
|
-
|
|
4864
|
+
containingScope: findContainingDescribeScope(existingLines, startIdx),
|
|
4865
|
+
lines: existingLines.slice(startIdx, i + 1),
|
|
4866
|
+
anchor
|
|
4704
4867
|
});
|
|
4705
4868
|
}
|
|
4706
4869
|
if (blocks.length === 0) return generatedContent;
|
|
@@ -4710,15 +4873,19 @@ async function overwriteWithPreservedRegions(existingContent, generatedContent,
|
|
|
4710
4873
|
const blocksByClass = /* @__PURE__ */ new Map();
|
|
4711
4874
|
for (const b of blocks) {
|
|
4712
4875
|
if (!blocksByClass.has(b.containingClass)) blocksByClass.set(b.containingClass, []);
|
|
4713
|
-
blocksByClass.get(b.containingClass).push(
|
|
4876
|
+
blocksByClass.get(b.containingClass).push({
|
|
4877
|
+
containingScope: b.containingScope,
|
|
4878
|
+
lines: b.lines,
|
|
4879
|
+
anchor: b.anchor
|
|
4880
|
+
});
|
|
4714
4881
|
}
|
|
4715
4882
|
const replacedClasses = /* @__PURE__ */ new Set();
|
|
4716
4883
|
const handledBlockKeys = /* @__PURE__ */ new Set();
|
|
4717
4884
|
const replacements = [];
|
|
4718
4885
|
for (const [className, classBlocks] of blocksByClass) {
|
|
4719
4886
|
let targetClass = className;
|
|
4720
|
-
if (!targetClass) for (const
|
|
4721
|
-
for (const l of
|
|
4887
|
+
if (!targetClass) for (const entry of classBlocks) {
|
|
4888
|
+
for (const l of entry.lines) {
|
|
4722
4889
|
const m = l.match(/^(?:export\s+)?class\s+(\w+)/);
|
|
4723
4890
|
if (m && classEndLines.has(m[1])) {
|
|
4724
4891
|
targetClass = m[1];
|
|
@@ -4728,7 +4895,7 @@ async function overwriteWithPreservedRegions(existingContent, generatedContent,
|
|
|
4728
4895
|
if (targetClass) break;
|
|
4729
4896
|
}
|
|
4730
4897
|
if (!targetClass) continue;
|
|
4731
|
-
if (!classBlocks.some((
|
|
4898
|
+
if (!classBlocks.some((entry) => entry.lines.some((l) => {
|
|
4732
4899
|
return l.match(/^(?:export\s+)?class\s+(\w+)/)?.[1] === targetClass;
|
|
4733
4900
|
}))) continue;
|
|
4734
4901
|
const info = classEndLines.get(targetClass);
|
|
@@ -4736,9 +4903,9 @@ async function overwriteWithPreservedRegions(existingContent, generatedContent,
|
|
|
4736
4903
|
const startLine = findClassDeclarationStart(result, targetClass, info.bodyEndLine);
|
|
4737
4904
|
if (startLine < 0) continue;
|
|
4738
4905
|
const content = [];
|
|
4739
|
-
for (const
|
|
4906
|
+
for (const entry of classBlocks) {
|
|
4740
4907
|
content.push("");
|
|
4741
|
-
content.push(...
|
|
4908
|
+
content.push(...entry.lines);
|
|
4742
4909
|
}
|
|
4743
4910
|
replacements.push({
|
|
4744
4911
|
startLine,
|
|
@@ -4753,22 +4920,46 @@ async function overwriteWithPreservedRegions(existingContent, generatedContent,
|
|
|
4753
4920
|
let effectiveClassEndLines = classEndLines;
|
|
4754
4921
|
if (replacedClasses.size > 0) effectiveClassEndLines = await extractClassEndLines(result.join("\n"), language);
|
|
4755
4922
|
const insertions = [];
|
|
4923
|
+
let insertionOrder = 0;
|
|
4756
4924
|
for (const [className, classBlocks] of blocksByClass) {
|
|
4757
4925
|
if (handledBlockKeys.has(className) || replacedClasses.has(className)) continue;
|
|
4758
4926
|
const info = className ? effectiveClassEndLines.get(className) : void 0;
|
|
4759
|
-
const
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4927
|
+
for (const entry of classBlocks) {
|
|
4928
|
+
let insertLine;
|
|
4929
|
+
if (entry.anchor && !entry.anchor.includes("@oagen-ignore") && /[a-zA-Z]|^\/\*|^\/\//.test(entry.anchor)) {
|
|
4930
|
+
for (let li = 0; li < result.length; li++) if (result[li].trimStart() === entry.anchor) {
|
|
4931
|
+
insertLine = li;
|
|
4932
|
+
break;
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4935
|
+
if (insertLine === void 0) if (info) insertLine = info.bodyEndLine;
|
|
4936
|
+
else {
|
|
4937
|
+
const scopeEndLine = entry.containingScope ? findDescribeScopeEndLine(result, entry.containingScope) : void 0;
|
|
4938
|
+
const blockHasImportsFallback = entry.lines.some((l) => /^\s*(import|from)\s/.test(l));
|
|
4939
|
+
if (scopeEndLine !== void 0) insertLine = scopeEndLine;
|
|
4940
|
+
else if (blockHasImportsFallback || hasTopLevelDeclaration(entry.lines)) {
|
|
4941
|
+
let lastImportLine = 0;
|
|
4942
|
+
for (let li = 0; li < result.length; li++) {
|
|
4943
|
+
const trimmed = result[li].trimStart();
|
|
4944
|
+
if (/^(import|from)\s/.test(trimmed)) lastImportLine = li + 1;
|
|
4945
|
+
}
|
|
4946
|
+
insertLine = lastImportLine;
|
|
4947
|
+
} else insertLine = result.length;
|
|
4948
|
+
}
|
|
4949
|
+
if (entry.lines.some((l) => /^\s*import\s/.test(l))) while (insertLine >= 2 && result[insertLine - 1]?.trim() === "" && /^\s*(import|using|from)\s/.test(result[insertLine - 2] ?? "")) insertLine--;
|
|
4950
|
+
const prevLine = insertLine > 0 ? result[insertLine - 1]?.trimStart() ?? "" : "";
|
|
4951
|
+
const blockContent = /^import\s/.test(prevLine) || /^using\s/.test(prevLine) || /^from\s/.test(prevLine) || insertLine > 0 && prevLine === "" ? [...entry.lines] : ["", ...entry.lines];
|
|
4952
|
+
if ((result[insertLine]?.trimStart() ?? "").startsWith("///")) blockContent.push("");
|
|
4953
|
+
insertions.push({
|
|
4954
|
+
line: insertLine,
|
|
4955
|
+
order: insertionOrder++,
|
|
4956
|
+
content: blockContent
|
|
4957
|
+
});
|
|
4764
4958
|
}
|
|
4765
|
-
insertions.push({
|
|
4766
|
-
line: insertLine,
|
|
4767
|
-
content
|
|
4768
|
-
});
|
|
4769
4959
|
}
|
|
4770
|
-
insertions.sort((a, b) => b.line - a.line);
|
|
4960
|
+
insertions.sort((a, b) => b.line - a.line || b.order - a.order);
|
|
4771
4961
|
for (const ins of insertions) result.splice(ins.line, 0, ...ins.content);
|
|
4962
|
+
removeDuplicateImportsCoveredByIgnoreBlocks(result);
|
|
4772
4963
|
return result.join("\n");
|
|
4773
4964
|
}
|
|
4774
4965
|
/**
|
|
@@ -4796,12 +4987,47 @@ function stripIgnoreRegions(lines) {
|
|
|
4796
4987
|
}
|
|
4797
4988
|
/** Scan backwards from `lineIdx` to find the nearest class declaration name. */
|
|
4798
4989
|
function findContainingClass(lines, lineIdx) {
|
|
4990
|
+
if (/^\S/.test(lines[lineIdx] ?? "")) return "";
|
|
4799
4991
|
for (let j = lineIdx - 1; j >= 0; j--) {
|
|
4800
|
-
const m = lines[j].match(/^\s*(?:export\s+)
|
|
4992
|
+
const m = lines[j].match(/^\s*(?:(?:export|public|private|protected|internal|abstract|sealed|static|partial)\s+)*class\s+(\w+)/);
|
|
4801
4993
|
if (m) return m[1];
|
|
4802
4994
|
}
|
|
4803
4995
|
return "";
|
|
4804
4996
|
}
|
|
4997
|
+
function findContainingDescribeScope(lines, lineIdx) {
|
|
4998
|
+
const markerLine = lines[lineIdx] ?? "";
|
|
4999
|
+
if (/^\S/.test(markerLine)) return "";
|
|
5000
|
+
const markerIndent = leadingSpaces(markerLine);
|
|
5001
|
+
for (let j = lineIdx - 1; j >= 0; j--) {
|
|
5002
|
+
const line = lines[j];
|
|
5003
|
+
if (leadingSpaces(line) >= markerIndent) continue;
|
|
5004
|
+
const match = line.match(/^\s*describe\((['"`])(.+?)\1\s*,/);
|
|
5005
|
+
if (match) return match[2];
|
|
5006
|
+
if (/^\s*(?:export\s+)?class\s+\w+/.test(line)) return "";
|
|
5007
|
+
}
|
|
5008
|
+
return "";
|
|
5009
|
+
}
|
|
5010
|
+
function findDescribeScopeEndLine(lines, scopeName) {
|
|
5011
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5012
|
+
const match = lines[i].match(/^\s*describe\((['"`])(.+?)\1\s*,/);
|
|
5013
|
+
if (!match || match[2] !== scopeName) continue;
|
|
5014
|
+
let depth = 0;
|
|
5015
|
+
for (let j = i; j < lines.length; j++) {
|
|
5016
|
+
depth += countChar(lines[j], "{");
|
|
5017
|
+
depth -= countChar(lines[j], "}");
|
|
5018
|
+
if (j > i && depth <= 0) return j;
|
|
5019
|
+
}
|
|
5020
|
+
}
|
|
5021
|
+
}
|
|
5022
|
+
function leadingSpaces(line) {
|
|
5023
|
+
return line.match(/^\s*/)?.[0].length ?? 0;
|
|
5024
|
+
}
|
|
5025
|
+
function countChar(line, char) {
|
|
5026
|
+
return [...line].filter((c) => c === char).length;
|
|
5027
|
+
}
|
|
5028
|
+
function hasTopLevelDeclaration(lines) {
|
|
5029
|
+
return lines.some((line) => /^(?:export\s+)?(?:interface|type|class|function|const|let|var|enum)\s+\w+/.test(line.trim()));
|
|
5030
|
+
}
|
|
4805
5031
|
/**
|
|
4806
5032
|
* Find the start line of a class declaration (including decorators above it)
|
|
4807
5033
|
* by scanning backwards from `beforeLine`.
|
|
@@ -4879,9 +5105,13 @@ function collectTsImports(lines) {
|
|
|
4879
5105
|
const mod = m[4];
|
|
4880
5106
|
if (defaultName) out.defaults.set(mod, defaultName);
|
|
4881
5107
|
const names = namesRaw.split(",").map((s) => s.trim().replace(/\s+as\s+\w+$/, "")).filter(Boolean);
|
|
4882
|
-
const
|
|
4883
|
-
|
|
4884
|
-
|
|
5108
|
+
for (const rawName of names) {
|
|
5109
|
+
const isInlineTypeOnly = rawName.startsWith("type ");
|
|
5110
|
+
const name = rawName.replace(/^type\s+/, "");
|
|
5111
|
+
const target = isTypeOnly || isInlineTypeOnly ? out.typeOnly : out.named;
|
|
5112
|
+
if (!target.has(mod)) target.set(mod, /* @__PURE__ */ new Set());
|
|
5113
|
+
target.get(mod).add(name);
|
|
5114
|
+
}
|
|
4885
5115
|
continue;
|
|
4886
5116
|
}
|
|
4887
5117
|
m = normalized.match(/^import\s+(?:type\s+)?(\w+)\s+from\s+['"]([^'"]+)['"]$/);
|
|
@@ -4904,12 +5134,99 @@ function collectTsImports(lines) {
|
|
|
4904
5134
|
*
|
|
4905
5135
|
* Mutates `resultLines` in place.
|
|
4906
5136
|
*/
|
|
5137
|
+
function filterOutIgnoreRegions(lines) {
|
|
5138
|
+
const out = [];
|
|
5139
|
+
let skipping = false;
|
|
5140
|
+
for (const line of lines) {
|
|
5141
|
+
if (!skipping && line.includes("@oagen-ignore-start")) {
|
|
5142
|
+
skipping = true;
|
|
5143
|
+
continue;
|
|
5144
|
+
}
|
|
5145
|
+
if (skipping) {
|
|
5146
|
+
if (line.includes("@oagen-ignore-end")) skipping = false;
|
|
5147
|
+
continue;
|
|
5148
|
+
}
|
|
5149
|
+
out.push(line);
|
|
5150
|
+
}
|
|
5151
|
+
return out;
|
|
5152
|
+
}
|
|
5153
|
+
function collectIgnoreRegionUsageText(lines) {
|
|
5154
|
+
const out = [];
|
|
5155
|
+
let skipping = false;
|
|
5156
|
+
for (const line of lines) {
|
|
5157
|
+
if (!skipping && line.includes("@oagen-ignore-start")) {
|
|
5158
|
+
skipping = true;
|
|
5159
|
+
continue;
|
|
5160
|
+
}
|
|
5161
|
+
if (!skipping) continue;
|
|
5162
|
+
if (/^\s*(import|from)\s/.test(line)) continue;
|
|
5163
|
+
out.push(line);
|
|
5164
|
+
if (line.includes("@oagen-ignore-end")) skipping = false;
|
|
5165
|
+
}
|
|
5166
|
+
return out.join("\n");
|
|
5167
|
+
}
|
|
5168
|
+
function removeDuplicateImportsCoveredByIgnoreBlocks(lines) {
|
|
5169
|
+
const ignoredImports = /* @__PURE__ */ new Set();
|
|
5170
|
+
const importStatements = collectImportStatements(lines);
|
|
5171
|
+
for (const stmt of importStatements) if (stmt.inIgnore) ignoredImports.add(normalizeImportStatement(stmt.lines));
|
|
5172
|
+
if (ignoredImports.size === 0) return;
|
|
5173
|
+
for (const stmt of [...importStatements].reverse()) {
|
|
5174
|
+
if (stmt.inIgnore) continue;
|
|
5175
|
+
if (ignoredImports.has(normalizeImportStatement(stmt.lines))) lines.splice(stmt.start, stmt.end - stmt.start + 1);
|
|
5176
|
+
}
|
|
5177
|
+
}
|
|
5178
|
+
function collectImportStatements(lines) {
|
|
5179
|
+
const statements = [];
|
|
5180
|
+
let inIgnore = false;
|
|
5181
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5182
|
+
const line = lines[i];
|
|
5183
|
+
if (!inIgnore && line.includes("@oagen-ignore-start")) {
|
|
5184
|
+
inIgnore = true;
|
|
5185
|
+
continue;
|
|
5186
|
+
}
|
|
5187
|
+
if (inIgnore && line.includes("@oagen-ignore-end")) {
|
|
5188
|
+
inIgnore = false;
|
|
5189
|
+
continue;
|
|
5190
|
+
}
|
|
5191
|
+
if (!isImportLine(line)) continue;
|
|
5192
|
+
const start = i;
|
|
5193
|
+
let end = i;
|
|
5194
|
+
const statementLines = [line];
|
|
5195
|
+
if ((line.match(/[({]/g) ?? []).length > (line.match(/[)}]/g) ?? []).length) {
|
|
5196
|
+
while (end + 1 < lines.length) {
|
|
5197
|
+
end++;
|
|
5198
|
+
statementLines.push(lines[end]);
|
|
5199
|
+
if ((lines[end].match(/[)}]/g) ?? []).length > 0 && /\bfrom\b/.test(lines[end])) break;
|
|
5200
|
+
}
|
|
5201
|
+
i = end;
|
|
5202
|
+
}
|
|
5203
|
+
statements.push({
|
|
5204
|
+
start,
|
|
5205
|
+
end,
|
|
5206
|
+
lines: statementLines,
|
|
5207
|
+
inIgnore
|
|
5208
|
+
});
|
|
5209
|
+
}
|
|
5210
|
+
return statements;
|
|
5211
|
+
}
|
|
5212
|
+
function normalizeImportStatement(lines) {
|
|
5213
|
+
return lines.join("\n").replace(/,\s*}/g, " }").replace(/\s+/g, " ").trim();
|
|
5214
|
+
}
|
|
5215
|
+
function isImportLine(line) {
|
|
5216
|
+
const trimmed = line.trim();
|
|
5217
|
+
return /^import(\s|$)/.test(trimmed) || /^from\s+/.test(trimmed) || /^use\s+[\w\\]/.test(trimmed) || /^require(_relative)?\s+['"]/.test(trimmed);
|
|
5218
|
+
}
|
|
5219
|
+
function identifierUsed(source, identifier) {
|
|
5220
|
+
return new RegExp(`\\b${identifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(source);
|
|
5221
|
+
}
|
|
4907
5222
|
function spliceExtraImports(existingLines, resultLines) {
|
|
5223
|
+
const existingOutsideIgnore = filterOutIgnoreRegions(existingLines);
|
|
5224
|
+
const ignoreRegionText = collectIgnoreRegionUsageText(existingLines);
|
|
4908
5225
|
const isPythonSimpleImport = (l) => /^import\s+\w/.test(l) && !/\sfrom\s/.test(l) && !/[{}]/.test(l);
|
|
4909
|
-
const existingSimple = new Set(
|
|
5226
|
+
const existingSimple = new Set(existingOutsideIgnore.filter(isPythonSimpleImport).map((l) => l.trim()));
|
|
4910
5227
|
const generatedSimple = new Set(resultLines.filter(isPythonSimpleImport).map((l) => l.trim()));
|
|
4911
5228
|
const extraSimple = [...existingSimple].filter((l) => !generatedSimple.has(l));
|
|
4912
|
-
const existingFrom = collectFromImports(
|
|
5229
|
+
const existingFrom = collectFromImports(existingOutsideIgnore);
|
|
4913
5230
|
const generatedFrom = collectFromImports(resultLines);
|
|
4914
5231
|
const allGeneratedIds = /* @__PURE__ */ new Set();
|
|
4915
5232
|
for (const ids of generatedFrom.values()) for (const id of ids) allGeneratedIds.add(id);
|
|
@@ -4924,29 +5241,33 @@ function spliceExtraImports(existingLines, resultLines) {
|
|
|
4924
5241
|
if (extra.length > 0) extraFromLines.push(`from ${mod} import ${extra.join(", ")}`);
|
|
4925
5242
|
}
|
|
4926
5243
|
}
|
|
4927
|
-
const existingTs = collectTsImports(
|
|
5244
|
+
const existingTs = collectTsImports(existingOutsideIgnore);
|
|
4928
5245
|
const generatedTs = collectTsImports(resultLines);
|
|
4929
5246
|
const extraTsLines = [];
|
|
4930
5247
|
for (const mod of existingTs.sideEffects) if (!generatedTs.sideEffects.has(mod)) extraTsLines.push(`import '${mod}';`);
|
|
4931
5248
|
for (const [mod, name] of existingTs.defaults) {
|
|
4932
5249
|
if (generatedTs.defaults.has(mod)) continue;
|
|
5250
|
+
if (!identifierUsed(ignoreRegionText, name)) continue;
|
|
4933
5251
|
extraTsLines.push(`import ${name} from '${mod}';`);
|
|
4934
5252
|
}
|
|
4935
|
-
for (const [mod, name] of existingTs.namespaces)
|
|
5253
|
+
for (const [mod, name] of existingTs.namespaces) {
|
|
5254
|
+
if (!identifierUsed(ignoreRegionText, name)) continue;
|
|
5255
|
+
if (!generatedTs.namespaces.has(mod)) extraTsLines.push(`import * as ${name} from '${mod}';`);
|
|
5256
|
+
}
|
|
4936
5257
|
for (const [mod, existIds] of existingTs.named) {
|
|
4937
5258
|
const genIds = generatedTs.named.get(mod) ?? /* @__PURE__ */ new Set();
|
|
4938
5259
|
const genTypeIds = generatedTs.typeOnly.get(mod) ?? /* @__PURE__ */ new Set();
|
|
4939
|
-
const extra = [...existIds].filter((id) => !genIds.has(id) && !genTypeIds.has(id));
|
|
5260
|
+
const extra = [...existIds].filter((id) => !genIds.has(id) && !genTypeIds.has(id) && identifierUsed(ignoreRegionText, id));
|
|
4940
5261
|
if (extra.length > 0) extraTsLines.push(`import { ${extra.join(", ")} } from '${mod}';`);
|
|
4941
5262
|
}
|
|
4942
5263
|
for (const [mod, existIds] of existingTs.typeOnly) {
|
|
4943
5264
|
const genIds = generatedTs.typeOnly.get(mod) ?? /* @__PURE__ */ new Set();
|
|
4944
5265
|
const genNamedIds = generatedTs.named.get(mod) ?? /* @__PURE__ */ new Set();
|
|
4945
|
-
const extra = [...existIds].filter((id) => !genIds.has(id) && !genNamedIds.has(id));
|
|
5266
|
+
const extra = [...existIds].filter((id) => !genIds.has(id) && !genNamedIds.has(id) && identifierUsed(ignoreRegionText, id));
|
|
4946
5267
|
if (extra.length > 0) extraTsLines.push(`import type { ${extra.join(", ")} } from '${mod}';`);
|
|
4947
5268
|
}
|
|
4948
5269
|
const isPhpUseImport = (l) => /^use\s+[\w\\]+\\[\w\\]*(\s+as\s+\w+)?\s*;?$/.test(l);
|
|
4949
|
-
const existingPhpUse = new Set(
|
|
5270
|
+
const existingPhpUse = new Set(existingOutsideIgnore.filter(isPhpUseImport).map((l) => l.trim()));
|
|
4950
5271
|
const generatedPhpUse = new Set(resultLines.filter(isPhpUseImport).map((l) => l.trim()));
|
|
4951
5272
|
const extraPhpUse = [...existingPhpUse].filter((l) => !generatedPhpUse.has(l));
|
|
4952
5273
|
const rubyRequireRe = /^(require(?:_relative)?)\s+['"]([^'"]+)['"]\s*$/;
|
|
@@ -4961,7 +5282,7 @@ function spliceExtraImports(existingLines, resultLines) {
|
|
|
4961
5282
|
}
|
|
4962
5283
|
const seenExistingKeys = /* @__PURE__ */ new Set();
|
|
4963
5284
|
const extraRubyRequire = [];
|
|
4964
|
-
for (const l of
|
|
5285
|
+
for (const l of existingOutsideIgnore) {
|
|
4965
5286
|
const key = rubyRequireKey(l);
|
|
4966
5287
|
if (!key) continue;
|
|
4967
5288
|
if (generatedRubyRequireKeys.has(key)) continue;
|
|
@@ -5174,7 +5495,11 @@ function collectReferencedNames(services, models) {
|
|
|
5174
5495
|
for (const err of op.errors) if (err.type) collectFromTypeRef(err.type);
|
|
5175
5496
|
if (op.successResponses) for (const sr of op.successResponses) collectFromTypeRef(sr.type);
|
|
5176
5497
|
}
|
|
5177
|
-
for (const model of models)
|
|
5498
|
+
for (const model of models) {
|
|
5499
|
+
if (isDiscriminatedModel(model)) referencedModels.add(model.name);
|
|
5500
|
+
const disc = model.discriminator;
|
|
5501
|
+
if (disc?.mapping) for (const variantName of Object.values(disc.mapping)) referencedModels.add(variantName);
|
|
5502
|
+
}
|
|
5178
5503
|
const modelsByName = new Map(models.map((m) => [m.name, m]));
|
|
5179
5504
|
const visited = /* @__PURE__ */ new Set();
|
|
5180
5505
|
const queue = [...referencedModels];
|
|
@@ -5489,6 +5814,7 @@ async function readManifest(dir) {
|
|
|
5489
5814
|
}
|
|
5490
5815
|
/** Write `.oagen-manifest.json` to a directory with sorted paths. */
|
|
5491
5816
|
async function writeManifest(dir, opts) {
|
|
5817
|
+
const manifestPath = path$1.join(dir, MANIFEST_FILENAME);
|
|
5492
5818
|
const manifest = {
|
|
5493
5819
|
version: MANIFEST_VERSION,
|
|
5494
5820
|
language: opts.language,
|
|
@@ -5503,10 +5829,16 @@ async function writeManifest(dir, opts) {
|
|
|
5503
5829
|
files: [...new Set(opts.files)].sort(),
|
|
5504
5830
|
...opts.operations !== void 0 ? { operations: opts.operations } : {}
|
|
5505
5831
|
};
|
|
5506
|
-
const
|
|
5832
|
+
const previous = await readManifest(dir);
|
|
5833
|
+
if (previous && sameManifestExceptGeneratedAt(previous, manifest)) manifest.generatedAt = previous.generatedAt;
|
|
5507
5834
|
await fs.mkdir(dir, { recursive: true });
|
|
5508
5835
|
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
5509
5836
|
}
|
|
5837
|
+
function sameManifestExceptGeneratedAt(a, b) {
|
|
5838
|
+
const { generatedAt: _aGeneratedAt, ...aComparable } = a;
|
|
5839
|
+
const { generatedAt: _bGeneratedAt, ...bComparable } = b;
|
|
5840
|
+
return JSON.stringify(aComparable) === JSON.stringify(bComparable);
|
|
5841
|
+
}
|
|
5510
5842
|
/** Return the set of previous paths no longer present in the current emission. */
|
|
5511
5843
|
function computeStalePaths(prev, currentPaths) {
|
|
5512
5844
|
const current = new Set(currentPaths);
|