@workos/oagen 0.5.0 → 0.7.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/dist/cli/index.mjs +580 -34
- package/dist/cli/index.mjs.map +1 -1
- package/dist/compat/index.d.mts +1 -1
- package/dist/compat/index.mjs +21 -3
- package/dist/compat/index.mjs.map +1 -1
- package/dist/{diff-CSy3g-Bq.mjs → diff-BYadQQV7.mjs} +353 -30
- package/dist/diff-BYadQQV7.mjs.map +1 -0
- package/dist/index.d.mts +42 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +227 -4
- package/dist/index.mjs.map +1 -1
- package/dist/scripts/smoke/baseline.mjs +225 -3
- package/dist/scripts/smoke/baseline.mjs.map +1 -1
- package/dist/smoke.d.mts +33 -0
- package/dist/smoke.d.mts.map +1 -1
- package/dist/smoke.mjs +225 -3
- package/dist/smoke.mjs.map +1 -1
- package/dist/tree-sitter-C53w_0WE.mjs.map +1 -1
- package/dist/{types-D84LjrVG.d.mts → types-DJMbCtIO.d.mts} +40 -2
- package/dist/types-DJMbCtIO.d.mts.map +1 -0
- package/dist/verify/index.d.mts +1 -1
- package/dist/verify/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/diff-CSy3g-Bq.mjs.map +0 -1
- package/dist/types-D84LjrVG.d.mts.map +0 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -600,7 +600,7 @@ function findSchemaByName(pascalName, schemas) {
|
|
|
600
600
|
for (const [k, v] of Object.entries(schemas)) if (resolveSchemaName(k) === pascalName) return v;
|
|
601
601
|
return null;
|
|
602
602
|
}
|
|
603
|
-
/** Walk into a schema to find a field's sub-schema, resolving allOf. */
|
|
603
|
+
/** Walk into a schema to find a field's sub-schema, resolving allOf and oneOf/anyOf. */
|
|
604
604
|
function findNestedFieldSchema(fieldName, parentSchema, schemas) {
|
|
605
605
|
if (parentSchema.properties?.[fieldName]) return parentSchema.properties[fieldName];
|
|
606
606
|
if (parentSchema.allOf) for (const sub of parentSchema.allOf) {
|
|
@@ -612,6 +612,15 @@ function findNestedFieldSchema(fieldName, parentSchema, schemas) {
|
|
|
612
612
|
}
|
|
613
613
|
if (resolved.properties?.[fieldName]) return resolved.properties[fieldName];
|
|
614
614
|
}
|
|
615
|
+
for (const variant of [...parentSchema.oneOf ?? [], ...parentSchema.anyOf ?? []]) {
|
|
616
|
+
let resolved = variant;
|
|
617
|
+
if (variant.$ref) {
|
|
618
|
+
const segments = variant.$ref.split("/");
|
|
619
|
+
const refName = segments[segments.length - 1];
|
|
620
|
+
if (refName && schemas[refName]) resolved = schemas[refName];
|
|
621
|
+
}
|
|
622
|
+
if (resolved.properties?.[fieldName]) return resolved.properties[fieldName];
|
|
623
|
+
}
|
|
615
624
|
return null;
|
|
616
625
|
}
|
|
617
626
|
/** Extract a model (and nested models) from an inline field schema. */
|
|
@@ -679,6 +688,86 @@ function buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet)
|
|
|
679
688
|
example: fieldSchema.example
|
|
680
689
|
};
|
|
681
690
|
}
|
|
691
|
+
/**
|
|
692
|
+
* If every non-null oneOf/anyOf variant is a string-valued const, return the
|
|
693
|
+
* list of those const values so the union can collapse into a single enum.
|
|
694
|
+
* Returns null when any variant isn't a pure string-const (model ref,
|
|
695
|
+
* number const, nullable, etc.).
|
|
696
|
+
*/
|
|
697
|
+
function collectLiteralStringConsts(variants) {
|
|
698
|
+
if (variants.length === 0) return null;
|
|
699
|
+
const values = [];
|
|
700
|
+
for (const v of variants) {
|
|
701
|
+
if (typeof v.const === "string") {
|
|
702
|
+
values.push(v.const);
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (Array.isArray(v.enum) && v.enum.length === 1 && typeof v.enum[0] === "string") {
|
|
706
|
+
values.push(v.enum[0]);
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
return values;
|
|
712
|
+
}
|
|
713
|
+
/** Build an EnumRef the same way the regular enum path does for naming. */
|
|
714
|
+
function buildSyntheticEnumRef(values, contextName, parentModelName) {
|
|
715
|
+
const baseName = toPascalCase(contextName ?? "UnknownEnum");
|
|
716
|
+
const cleanParent = parentModelName ? stripListItemMarkers(parentModelName) : void 0;
|
|
717
|
+
return {
|
|
718
|
+
kind: "enum",
|
|
719
|
+
name: cleanParent && !baseName.startsWith(cleanParent) ? `${cleanParent}${baseName}` : baseName,
|
|
720
|
+
values
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Detect an implicit discriminator on an oneOf where every variant is an
|
|
725
|
+
* object schema that pins the same property to a const value. Returns
|
|
726
|
+
* { property, mapping } so the IR can represent the union as discriminated
|
|
727
|
+
* without needing an explicit `discriminator:` key in the spec.
|
|
728
|
+
*
|
|
729
|
+
* Both shapes are supported:
|
|
730
|
+
* { properties: { event: { const: "x" } }, required: ["event"] }
|
|
731
|
+
* { properties: { event: { type: "string", enum: ["x"] } } }
|
|
732
|
+
*
|
|
733
|
+
* Returns null when any variant doesn't fit the pattern or when variants
|
|
734
|
+
* disagree on which property carries the discriminator.
|
|
735
|
+
*/
|
|
736
|
+
function detectConstPropertyDiscriminator(variants) {
|
|
737
|
+
if (variants.length < 2) return null;
|
|
738
|
+
let candidateProperty = null;
|
|
739
|
+
const candidates = Object.keys(variants[0]?.properties ?? {});
|
|
740
|
+
for (const propName of candidates) if (variants.every((v) => getConstPropertyValue(v, propName) !== null)) {
|
|
741
|
+
candidateProperty = propName;
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
if (!candidateProperty) return null;
|
|
745
|
+
const mapping = {};
|
|
746
|
+
for (const v of variants) {
|
|
747
|
+
const value = getConstPropertyValue(v, candidateProperty);
|
|
748
|
+
if (value === null) return null;
|
|
749
|
+
const variantName = resolveVariantModelName(v);
|
|
750
|
+
if (!variantName) return null;
|
|
751
|
+
mapping[value] = variantName;
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
property: candidateProperty,
|
|
755
|
+
mapping
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
function getConstPropertyValue(schema, property) {
|
|
759
|
+
const prop = schema.properties?.[property];
|
|
760
|
+
if (!prop) return null;
|
|
761
|
+
if (typeof prop.const === "string") return prop.const;
|
|
762
|
+
if (Array.isArray(prop.enum) && prop.enum.length === 1 && typeof prop.enum[0] === "string") return prop.enum[0];
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
function resolveVariantModelName(schema) {
|
|
766
|
+
if (schema.$ref) return resolveSchemaName(schema.$ref.replace(/^#\/components\/schemas\//, ""));
|
|
767
|
+
const title = typeof schema["title"] === "string" ? schema["title"] : null;
|
|
768
|
+
if (title) return resolveSchemaName(title);
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
682
771
|
function extractEnum(name, schema) {
|
|
683
772
|
return {
|
|
684
773
|
name,
|
|
@@ -697,6 +786,19 @@ function extractModel(name, schema, schemas) {
|
|
|
697
786
|
if (!fieldSchema) continue;
|
|
698
787
|
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
|
|
699
788
|
}
|
|
789
|
+
if (fields.length === 0 && (schema.oneOf || schema.anyOf)) {
|
|
790
|
+
const variants = schema.oneOf ?? schema.anyOf ?? [];
|
|
791
|
+
const emptyRequired = /* @__PURE__ */ new Set();
|
|
792
|
+
const seenFieldNames = /* @__PURE__ */ new Set();
|
|
793
|
+
for (const variant of variants) {
|
|
794
|
+
if (!variant.properties) continue;
|
|
795
|
+
for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
|
|
796
|
+
if (!fieldSchema || seenFieldNames.has(fieldName)) continue;
|
|
797
|
+
seenFieldNames.add(fieldName);
|
|
798
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
700
802
|
if (schema.additionalProperties && typeof schema.additionalProperties === "object" && schema.properties) {
|
|
701
803
|
if (Object.keys(schema.additionalProperties).length > 0) {
|
|
702
804
|
const valueType = schemaToTypeRef(schema.additionalProperties, "additionalProperties", name);
|
|
@@ -736,6 +838,18 @@ function extractAllOfModel(name, schema, schemas) {
|
|
|
736
838
|
});
|
|
737
839
|
if (f.required) requiredSet.add(f.name);
|
|
738
840
|
}
|
|
841
|
+
} else if (resolved.oneOf || resolved.anyOf) {
|
|
842
|
+
const variants = resolved.oneOf ?? resolved.anyOf ?? [];
|
|
843
|
+
const emptyRequired = /* @__PURE__ */ new Set();
|
|
844
|
+
const seenFieldNames = new Set(fields.map((f) => f.name));
|
|
845
|
+
for (const variant of variants) {
|
|
846
|
+
if (!variant.properties) continue;
|
|
847
|
+
for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
|
|
848
|
+
if (!fieldSchema || seenFieldNames.has(fieldName)) continue;
|
|
849
|
+
seenFieldNames.add(fieldName);
|
|
850
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
|
|
851
|
+
}
|
|
852
|
+
}
|
|
739
853
|
} else {
|
|
740
854
|
if (resolved.required) for (const r of resolved.required) requiredSet.add(r);
|
|
741
855
|
if (resolved.properties) {
|
|
@@ -902,6 +1016,15 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
|
|
|
902
1016
|
kind: "nullable",
|
|
903
1017
|
inner: schemaToTypeRef(nonNullVariants[0], contextName, parentModelName)
|
|
904
1018
|
};
|
|
1019
|
+
const literalStrings = collectLiteralStringConsts(nonNullVariants);
|
|
1020
|
+
if (literalStrings !== null && literalStrings.length >= 2) {
|
|
1021
|
+
const enumRef = buildSyntheticEnumRef(literalStrings, contextName, parentModelName);
|
|
1022
|
+
return nullVariant ? {
|
|
1023
|
+
kind: "nullable",
|
|
1024
|
+
inner: enumRef
|
|
1025
|
+
} : enumRef;
|
|
1026
|
+
}
|
|
1027
|
+
const syntheticDiscriminator = !schema.discriminator && compositionKind === "oneOf" ? detectConstPropertyDiscriminator(nonNullVariants) : null;
|
|
905
1028
|
const variants = rawVariants.filter((v) => v.type !== "null").map((v) => schemaToTypeRef(v, contextName, parentModelName));
|
|
906
1029
|
const hasNull = !!nullVariant;
|
|
907
1030
|
const union = {
|
|
@@ -911,7 +1034,7 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
|
|
|
911
1034
|
...schema.discriminator ? { discriminator: {
|
|
912
1035
|
property: schema.discriminator.propertyName,
|
|
913
1036
|
mapping: Object.fromEntries(Object.entries(schema.discriminator.mapping ?? {}).map(([k, v]) => [k, v.replace(/^#\/components\/schemas\//, "")]))
|
|
914
|
-
} } : {}
|
|
1037
|
+
} } : syntheticDiscriminator ? { discriminator: syntheticDiscriminator } : {}
|
|
915
1038
|
};
|
|
916
1039
|
return hasNull ? {
|
|
917
1040
|
kind: "nullable",
|
|
@@ -1583,6 +1706,94 @@ function inferDeeperContext(path) {
|
|
|
1583
1706
|
if (nonParams.length >= 3) return toPascalCase(nonParams.slice(1, -1).join("_"));
|
|
1584
1707
|
return null;
|
|
1585
1708
|
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Extract mutually-exclusive parameter groups from the `x-mutually-exclusive-parameter-groups`
|
|
1711
|
+
* operation extension. Cross-references grouped parameter names against the already-extracted
|
|
1712
|
+
* IR parameter arrays so emitters get object-identity references.
|
|
1713
|
+
*/
|
|
1714
|
+
function extractParameterGroups(op, allIRParams, operationContext) {
|
|
1715
|
+
const raw = op["x-mutually-exclusive-parameter-groups"];
|
|
1716
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1717
|
+
const paramByName = /* @__PURE__ */ new Map();
|
|
1718
|
+
for (const p of allIRParams) paramByName.set(p.name, p);
|
|
1719
|
+
const groups = [];
|
|
1720
|
+
for (const [groupName, groupDef] of Object.entries(raw)) {
|
|
1721
|
+
if (!groupDef || typeof groupDef !== "object") throw new Error(`Malformed x-mutually-exclusive-parameter-groups.${groupName} in ${operationContext}: expected an object with "optional" and "variants".`);
|
|
1722
|
+
if (typeof groupDef.optional !== "boolean") throw new Error(`Malformed x-mutually-exclusive-parameter-groups.${groupName}.optional in ${operationContext}: expected a boolean.`);
|
|
1723
|
+
if (!groupDef.variants || typeof groupDef.variants !== "object") throw new Error(`Malformed x-mutually-exclusive-parameter-groups.${groupName}.variants in ${operationContext}: expected an object mapping variant names to parameter name arrays.`);
|
|
1724
|
+
if (Object.keys(groupDef.variants).length === 0) throw new Error(`Malformed x-mutually-exclusive-parameter-groups.${groupName} in ${operationContext}: group has zero variants.`);
|
|
1725
|
+
const variants = Object.entries(groupDef.variants).map(([variantName, paramNames]) => {
|
|
1726
|
+
if (!Array.isArray(paramNames) || paramNames.length === 0) throw new Error(`Malformed x-mutually-exclusive-parameter-groups.${groupName}.variants.${variantName} in ${operationContext}: expected a non-empty array of parameter names.`);
|
|
1727
|
+
return {
|
|
1728
|
+
name: variantName,
|
|
1729
|
+
parameters: paramNames.map((pName) => {
|
|
1730
|
+
const irParam = paramByName.get(pName);
|
|
1731
|
+
if (!irParam) throw new Error(`x-mutually-exclusive-parameter-groups.${groupName}.variants.${variantName} references parameter "${pName}" which does not exist in the operation's parameters[] (${operationContext}).`);
|
|
1732
|
+
return irParam;
|
|
1733
|
+
})
|
|
1734
|
+
};
|
|
1735
|
+
});
|
|
1736
|
+
groups.push({
|
|
1737
|
+
name: groupName,
|
|
1738
|
+
optional: groupDef.optional,
|
|
1739
|
+
variants
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
return groups.length > 0 ? groups : void 0;
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Extract mutually-exclusive body parameter groups from the
|
|
1746
|
+
* `x-mutually-exclusive-body-groups` operation extension. Unlike query
|
|
1747
|
+
* parameter groups (whose parameters live in operation.queryParams), body
|
|
1748
|
+
* group parameters are synthetic — built from the oneOf variant schemas in
|
|
1749
|
+
* the request body. Emitters use the group's presence to generate sum-type
|
|
1750
|
+
* interfaces and custom JSON marshalling instead of flat optional fields.
|
|
1751
|
+
*/
|
|
1752
|
+
function extractBodyParameterGroups(op, operationContext) {
|
|
1753
|
+
const raw = op["x-mutually-exclusive-body-groups"];
|
|
1754
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1755
|
+
const bodySchema = op.requestBody?.content?.["application/json"]?.schema;
|
|
1756
|
+
const variantFieldSchemas = /* @__PURE__ */ new Map();
|
|
1757
|
+
if (bodySchema) {
|
|
1758
|
+
for (const sub of bodySchema.allOf ?? []) for (const variant of sub.oneOf ?? []) if (variant.properties) {
|
|
1759
|
+
for (const [name, fieldSchema] of Object.entries(variant.properties)) if (fieldSchema) variantFieldSchemas.set(name, fieldSchema);
|
|
1760
|
+
}
|
|
1761
|
+
for (const variant of bodySchema.oneOf ?? []) if (variant.properties) {
|
|
1762
|
+
for (const [name, fieldSchema] of Object.entries(variant.properties)) if (fieldSchema) variantFieldSchemas.set(name, fieldSchema);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
const groups = [];
|
|
1766
|
+
for (const [groupName, groupDef] of Object.entries(raw)) {
|
|
1767
|
+
if (!groupDef || typeof groupDef !== "object") continue;
|
|
1768
|
+
if (typeof groupDef.optional !== "boolean") continue;
|
|
1769
|
+
if (!groupDef.variants || typeof groupDef.variants !== "object") continue;
|
|
1770
|
+
if (Object.keys(groupDef.variants).length === 0) continue;
|
|
1771
|
+
const variants = Object.entries(groupDef.variants).map(([variantName, paramNames]) => {
|
|
1772
|
+
if (!Array.isArray(paramNames) || paramNames.length === 0) throw new Error(`Malformed x-mutually-exclusive-body-groups.${groupName}.variants.${variantName} in ${operationContext}: expected a non-empty array of parameter names.`);
|
|
1773
|
+
return {
|
|
1774
|
+
name: variantName,
|
|
1775
|
+
parameters: paramNames.map((pName) => {
|
|
1776
|
+
const fieldSchema = variantFieldSchemas.get(pName);
|
|
1777
|
+
return {
|
|
1778
|
+
name: pName,
|
|
1779
|
+
type: fieldSchema ? schemaToTypeRef(fieldSchema, toPascalCase(pName)) : {
|
|
1780
|
+
kind: "primitive",
|
|
1781
|
+
type: "string"
|
|
1782
|
+
},
|
|
1783
|
+
required: false,
|
|
1784
|
+
description: fieldSchema?.description
|
|
1785
|
+
};
|
|
1786
|
+
})
|
|
1787
|
+
};
|
|
1788
|
+
});
|
|
1789
|
+
groups.push({
|
|
1790
|
+
name: groupName,
|
|
1791
|
+
optional: groupDef.optional,
|
|
1792
|
+
variants
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
return groups.length > 0 ? groups : void 0;
|
|
1796
|
+
}
|
|
1586
1797
|
function buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName) {
|
|
1587
1798
|
const allParams = [...pathLevelParams, ...op.parameters ?? []];
|
|
1588
1799
|
const hasIdempotencyHeader = allParams.some((p) => p.in === "header" && p.name.toLowerCase() === "idempotency-key");
|
|
@@ -1608,6 +1819,16 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
|
|
|
1608
1819
|
};
|
|
1609
1820
|
} else if (paginationFromParams) pagination = paginationFromParams;
|
|
1610
1821
|
const security = extractOperationSecurity(op.security);
|
|
1822
|
+
const allIRParams = [
|
|
1823
|
+
...pathParams,
|
|
1824
|
+
...queryParams,
|
|
1825
|
+
...headerParams,
|
|
1826
|
+
...cookieParams
|
|
1827
|
+
];
|
|
1828
|
+
const opLabel = op.operationId ?? `${method.toUpperCase()} ${path}`;
|
|
1829
|
+
const queryParamGroups = extractParameterGroups(op, allIRParams, opLabel);
|
|
1830
|
+
const bodyParamGroups = extractBodyParameterGroups(op, opLabel);
|
|
1831
|
+
const parameterGroups = queryParamGroups || bodyParamGroups ? [...queryParamGroups ?? [], ...bodyParamGroups ?? []] : void 0;
|
|
1611
1832
|
return {
|
|
1612
1833
|
operation: {
|
|
1613
1834
|
name: inferOperationName(method, path, op.operationId, operationIdTransform),
|
|
@@ -1627,7 +1848,8 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
|
|
|
1627
1848
|
injectIdempotencyKey: hasIdempotencyHeader,
|
|
1628
1849
|
deprecated: op.deprecated || void 0,
|
|
1629
1850
|
async: op["x-oagen-async"],
|
|
1630
|
-
security
|
|
1851
|
+
security,
|
|
1852
|
+
parameterGroups
|
|
1631
1853
|
},
|
|
1632
1854
|
inlineModels
|
|
1633
1855
|
};
|
|
@@ -2598,6 +2820,41 @@ function findPrecedingKdoc(children, index, source) {
|
|
|
2598
2820
|
}
|
|
2599
2821
|
return null;
|
|
2600
2822
|
}
|
|
2823
|
+
function extractKotlinClassMembers(classBody, source) {
|
|
2824
|
+
const members = [];
|
|
2825
|
+
const children = classBody.namedChildren;
|
|
2826
|
+
for (let i = 0; i < children.length; i++) {
|
|
2827
|
+
const child = children[i];
|
|
2828
|
+
let memberName = null;
|
|
2829
|
+
if (child.type === "property_declaration") memberName = child.children.find((c) => c.type === "variable_declaration")?.children.find((c) => c.type === "simple_identifier")?.text ?? null;
|
|
2830
|
+
else if (child.type === "function_declaration") memberName = child.children.find((c) => c.type === "simple_identifier")?.text ?? null;
|
|
2831
|
+
else if (child.type === "companion_object") memberName = "companion";
|
|
2832
|
+
if (!memberName) continue;
|
|
2833
|
+
let endIdx = child.endIndex;
|
|
2834
|
+
if (child.type === "property_declaration") while (i + 1 < children.length) {
|
|
2835
|
+
const next = children[i + 1];
|
|
2836
|
+
if (next.type === "getter" || next.type === "setter") {
|
|
2837
|
+
endIdx = next.endIndex;
|
|
2838
|
+
i++;
|
|
2839
|
+
} else break;
|
|
2840
|
+
}
|
|
2841
|
+
let startIdx = child.startIndex;
|
|
2842
|
+
for (let k = i - 1; k >= 0; k--) {
|
|
2843
|
+
const prev = children[k];
|
|
2844
|
+
if (prev.type === "multiline_comment") {
|
|
2845
|
+
if (source.slice(prev.startIndex, prev.endIndex).startsWith("/**")) startIdx = prev.startIndex;
|
|
2846
|
+
break;
|
|
2847
|
+
}
|
|
2848
|
+
if (prev.type === "line_comment") continue;
|
|
2849
|
+
break;
|
|
2850
|
+
}
|
|
2851
|
+
members.push({
|
|
2852
|
+
key: memberName,
|
|
2853
|
+
text: source.slice(startIdx, endIdx)
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
return members;
|
|
2857
|
+
}
|
|
2601
2858
|
const kotlinMergeAdapter = {
|
|
2602
2859
|
language: "kotlin",
|
|
2603
2860
|
grammarModule: "tree-sitter-kotlin",
|
|
@@ -2638,6 +2895,33 @@ const kotlinMergeAdapter = {
|
|
|
2638
2895
|
renderImports(imports) {
|
|
2639
2896
|
return imports.map((entry) => entry.text);
|
|
2640
2897
|
},
|
|
2898
|
+
shouldSkipDeepMerge(_symbolName, existingMemberKeys, newMembers) {
|
|
2899
|
+
const newText = newMembers.map((m) => m.text).join("\n");
|
|
2900
|
+
for (const match of newText.matchAll(/this\.(\w+)/g)) {
|
|
2901
|
+
const propName = match[1];
|
|
2902
|
+
if (propName && !existingMemberKeys.has(propName)) return true;
|
|
2903
|
+
}
|
|
2904
|
+
return false;
|
|
2905
|
+
},
|
|
2906
|
+
extractMembers(tree, source) {
|
|
2907
|
+
const result = /* @__PURE__ */ new Map();
|
|
2908
|
+
for (const child of tree.rootNode.children) {
|
|
2909
|
+
if (child.type !== "class_declaration") continue;
|
|
2910
|
+
const nameNode = child.children.find((c) => c.type === "type_identifier");
|
|
2911
|
+
if (!nameNode) continue;
|
|
2912
|
+
const body = child.children.find((c) => c.type === "class_body");
|
|
2913
|
+
if (!body) continue;
|
|
2914
|
+
const members = extractKotlinClassMembers(body, source);
|
|
2915
|
+
const firstMember = body.firstNamedChild;
|
|
2916
|
+
const memberIndent = firstMember ? " ".repeat(firstMember.startPosition.column) : void 0;
|
|
2917
|
+
result.set(nameNode.text, {
|
|
2918
|
+
members,
|
|
2919
|
+
bodyEndLine: body.endPosition.row,
|
|
2920
|
+
memberIndent
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2923
|
+
return result;
|
|
2924
|
+
},
|
|
2641
2925
|
extractDocstrings(tree, source) {
|
|
2642
2926
|
const result = /* @__PURE__ */ new Map();
|
|
2643
2927
|
const rootChildren = tree.rootNode.children;
|
|
@@ -2820,6 +3104,7 @@ const nodeMergeAdapter = {
|
|
|
2820
3104
|
const newText = newMembers.map((m) => m.text).join("\n");
|
|
2821
3105
|
for (const match of newText.matchAll(/this\.(\w+)/g)) {
|
|
2822
3106
|
const propName = match[1];
|
|
3107
|
+
if (propName === "workos") continue;
|
|
2823
3108
|
if (propName && !existingMemberKeys.has(propName)) return true;
|
|
2824
3109
|
}
|
|
2825
3110
|
return false;
|
|
@@ -3476,6 +3761,40 @@ function buildIgnoredSymbolNames(docstrings, regions) {
|
|
|
3476
3761
|
for (const [name, info] of docstrings) if (regions.some((r) => info.declStartIndex >= r.startIndex && info.declStartIndex <= r.endIndex)) ignored.add(name);
|
|
3477
3762
|
return ignored;
|
|
3478
3763
|
}
|
|
3764
|
+
function clampIndex(value, contentLength) {
|
|
3765
|
+
if (!Number.isFinite(value)) return 0;
|
|
3766
|
+
if (value < 0) return 0;
|
|
3767
|
+
if (value > contentLength) return contentLength;
|
|
3768
|
+
return value;
|
|
3769
|
+
}
|
|
3770
|
+
/**
|
|
3771
|
+
* Resolve the declaration line start for insertion using the current content,
|
|
3772
|
+
* instead of relying on parser column arithmetic. This avoids misplaced inserts
|
|
3773
|
+
* when parser columns/offsets drift.
|
|
3774
|
+
*/
|
|
3775
|
+
function declarationLineStart(content, declStartIndex) {
|
|
3776
|
+
const idx = clampIndex(declStartIndex, content.length);
|
|
3777
|
+
const prevNewline = content.lastIndexOf("\n", Math.max(0, idx - 1));
|
|
3778
|
+
return prevNewline === -1 ? 0 : prevNewline + 1;
|
|
3779
|
+
}
|
|
3780
|
+
/**
|
|
3781
|
+
* Resolve a docstring range by searching for the nearest matching text before
|
|
3782
|
+
* the declaration start. Falls back to parser-provided offsets when search fails.
|
|
3783
|
+
*/
|
|
3784
|
+
function resolveDocRange(content, docText, declStartIndex, fallbackStart, fallbackEnd) {
|
|
3785
|
+
const safeDecl = clampIndex(declStartIndex, content.length);
|
|
3786
|
+
const safeFallbackStart = clampIndex(fallbackStart, content.length);
|
|
3787
|
+
const safeFallbackEnd = clampIndex(fallbackEnd, content.length);
|
|
3788
|
+
const found = content.lastIndexOf(docText, safeDecl);
|
|
3789
|
+
if (found !== -1) return {
|
|
3790
|
+
start: found,
|
|
3791
|
+
end: found + docText.length
|
|
3792
|
+
};
|
|
3793
|
+
return {
|
|
3794
|
+
start: safeFallbackStart,
|
|
3795
|
+
end: safeFallbackEnd
|
|
3796
|
+
};
|
|
3797
|
+
}
|
|
3479
3798
|
/**
|
|
3480
3799
|
* Check if a tree-sitter grammar is configured for the given language.
|
|
3481
3800
|
*/
|
|
@@ -3524,15 +3843,17 @@ async function extractClassEndLines(source, language) {
|
|
|
3524
3843
|
if (source[body ? body.endIndex - 1 : node.endIndex - 1] === "}") map.set(name, { bodyEndLine: endRow });
|
|
3525
3844
|
else map.set(name, { bodyEndLine: endRow + 1 });
|
|
3526
3845
|
}
|
|
3527
|
-
|
|
3528
|
-
if (
|
|
3529
|
-
if (
|
|
3530
|
-
for (const inner of
|
|
3846
|
+
function walk(node) {
|
|
3847
|
+
if (node.type === "class" || node.type === "class_definition" || node.type === "class_declaration") processClassNode(node);
|
|
3848
|
+
if (node.type === "decorated_definition") {
|
|
3849
|
+
for (const inner of node.namedChildren) if (inner.type === "class_definition") processClassNode(inner);
|
|
3531
3850
|
}
|
|
3532
|
-
if (
|
|
3533
|
-
for (const inner of
|
|
3851
|
+
if (node.type === "export_statement") {
|
|
3852
|
+
for (const inner of node.namedChildren) if (inner.type === "class_declaration") processClassNode(inner);
|
|
3534
3853
|
}
|
|
3854
|
+
for (const child of node.namedChildren) walk(child);
|
|
3535
3855
|
}
|
|
3856
|
+
for (const child of tree.rootNode.children) walk(child);
|
|
3536
3857
|
return map;
|
|
3537
3858
|
}
|
|
3538
3859
|
/**
|
|
@@ -3687,10 +4008,10 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
3687
4008
|
if (newMembers.length > 0) {
|
|
3688
4009
|
if (adapter.shouldSkipDeepMerge?.(symbolName, existingMemberKeys, newMembers)) continue;
|
|
3689
4010
|
const indent = existSymbol.memberIndent ?? " ";
|
|
3690
|
-
const insertText = newMembers.map((m) => indent + m.text).join("\n");
|
|
4011
|
+
const insertText = newMembers.map((m) => indent + m.text).join("\n\n");
|
|
3691
4012
|
insertions.push({
|
|
3692
4013
|
line: existSymbol.bodyEndLine,
|
|
3693
|
-
text: insertText
|
|
4014
|
+
text: "\n" + insertText
|
|
3694
4015
|
});
|
|
3695
4016
|
deepAdded += newMembers.length;
|
|
3696
4017
|
}
|
|
@@ -3752,15 +4073,16 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
3752
4073
|
if (genDoc) if (existInfo.docstring) {
|
|
3753
4074
|
if (!existInfo.docstring.text.includes("@oagen-ignore") && existInfo.docstring.text !== genDoc.text) {
|
|
3754
4075
|
const newText = preserveDeprecatedTags(existInfo.docstring.text, genDoc.text);
|
|
4076
|
+
const range = resolveDocRange(result, existInfo.docstring.text, existInfo.declStartIndex, existInfo.docstring.startIndex, existInfo.docstring.endIndex);
|
|
3755
4077
|
edits.push({
|
|
3756
|
-
start:
|
|
3757
|
-
end:
|
|
4078
|
+
start: range.start,
|
|
4079
|
+
end: range.end,
|
|
3758
4080
|
newText
|
|
3759
4081
|
});
|
|
3760
4082
|
docstringUpdates++;
|
|
3761
4083
|
}
|
|
3762
4084
|
} else {
|
|
3763
|
-
const lineStart = existInfo.declStartIndex
|
|
4085
|
+
const lineStart = declarationLineStart(result, existInfo.declStartIndex);
|
|
3764
4086
|
const indent = " ".repeat(existInfo.declColumn);
|
|
3765
4087
|
edits.push({
|
|
3766
4088
|
start: lineStart,
|
|
@@ -3778,15 +4100,16 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
3778
4100
|
if (existMember.docstring) {
|
|
3779
4101
|
if (!existMember.docstring.text.includes("@oagen-ignore") && existMember.docstring.text !== genMember.docstring.text) {
|
|
3780
4102
|
const newText = preserveDeprecatedTags(existMember.docstring.text, genMember.docstring.text);
|
|
4103
|
+
const range = resolveDocRange(result, existMember.docstring.text, existMember.declStartIndex, existMember.docstring.startIndex, existMember.docstring.endIndex);
|
|
3781
4104
|
edits.push({
|
|
3782
|
-
start:
|
|
3783
|
-
end:
|
|
4105
|
+
start: range.start,
|
|
4106
|
+
end: range.end,
|
|
3784
4107
|
newText
|
|
3785
4108
|
});
|
|
3786
4109
|
docstringUpdates++;
|
|
3787
4110
|
}
|
|
3788
4111
|
} else {
|
|
3789
|
-
const lineStart = existMember.declStartIndex
|
|
4112
|
+
const lineStart = declarationLineStart(result, existMember.declStartIndex);
|
|
3790
4113
|
const indent = " ".repeat(existMember.declColumn);
|
|
3791
4114
|
edits.push({
|
|
3792
4115
|
start: lineStart,
|
|
@@ -3831,15 +4154,16 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
3831
4154
|
if (existMember.docstring.text.includes("@oagen-ignore")) continue;
|
|
3832
4155
|
if (existMember.docstring.text !== genMember.docstring.text) {
|
|
3833
4156
|
const newText = preserveDeprecatedTags(existMember.docstring.text, genMember.docstring.text);
|
|
4157
|
+
const range = resolveDocRange(result, existMember.docstring.text, existMember.declStartIndex, existMember.docstring.startIndex, existMember.docstring.endIndex);
|
|
3834
4158
|
edits.push({
|
|
3835
|
-
start:
|
|
3836
|
-
end:
|
|
4159
|
+
start: range.start,
|
|
4160
|
+
end: range.end,
|
|
3837
4161
|
newText
|
|
3838
4162
|
});
|
|
3839
4163
|
docstringUpdates++;
|
|
3840
4164
|
}
|
|
3841
4165
|
} else {
|
|
3842
|
-
const lineStart = existMember.declStartIndex
|
|
4166
|
+
const lineStart = declarationLineStart(result, existMember.declStartIndex);
|
|
3843
4167
|
const indent = " ".repeat(existMember.declColumn);
|
|
3844
4168
|
edits.push({
|
|
3845
4169
|
start: lineStart,
|
|
@@ -4012,7 +4336,7 @@ async function overwriteWithPreservedRegions(existingContent, generatedContent,
|
|
|
4012
4336
|
});
|
|
4013
4337
|
}
|
|
4014
4338
|
if (blocks.length === 0) return generatedContent;
|
|
4015
|
-
const result =
|
|
4339
|
+
const result = stripIgnoreRegions(generatedLines);
|
|
4016
4340
|
spliceExtraImports(existingLines, result);
|
|
4017
4341
|
const classEndLines = await extractClassEndLines(result.join("\n"), language);
|
|
4018
4342
|
const blocksByClass = /* @__PURE__ */ new Map();
|
|
@@ -4079,10 +4403,33 @@ async function overwriteWithPreservedRegions(existingContent, generatedContent,
|
|
|
4079
4403
|
for (const ins of insertions) result.splice(ins.line, 0, ...ins.content);
|
|
4080
4404
|
return result.join("\n");
|
|
4081
4405
|
}
|
|
4406
|
+
/**
|
|
4407
|
+
* Remove every `@oagen-ignore-start`..`@oagen-ignore-end` region from the
|
|
4408
|
+
* supplied lines, collapsing stretches of blank lines that get left behind.
|
|
4409
|
+
* Used to drop generator-emitted seed blocks before splicing the on-disk
|
|
4410
|
+
* versions back in — otherwise every regen doubles up.
|
|
4411
|
+
*/
|
|
4412
|
+
function stripIgnoreRegions(lines) {
|
|
4413
|
+
const out = [];
|
|
4414
|
+
let skipping = false;
|
|
4415
|
+
for (const line of lines) {
|
|
4416
|
+
if (!skipping && line.includes("@oagen-ignore-start")) {
|
|
4417
|
+
skipping = true;
|
|
4418
|
+
while (out.length > 0 && out[out.length - 1].trim() === "") out.pop();
|
|
4419
|
+
continue;
|
|
4420
|
+
}
|
|
4421
|
+
if (skipping) {
|
|
4422
|
+
if (line.includes("@oagen-ignore-end")) skipping = false;
|
|
4423
|
+
continue;
|
|
4424
|
+
}
|
|
4425
|
+
out.push(line);
|
|
4426
|
+
}
|
|
4427
|
+
return out;
|
|
4428
|
+
}
|
|
4082
4429
|
/** Scan backwards from `lineIdx` to find the nearest class declaration name. */
|
|
4083
4430
|
function findContainingClass(lines, lineIdx) {
|
|
4084
4431
|
for (let j = lineIdx - 1; j >= 0; j--) {
|
|
4085
|
-
const m = lines[j].match(
|
|
4432
|
+
const m = lines[j].match(/^\s*(?:export\s+)?class\s+(\w+)/);
|
|
4086
4433
|
if (m) return m[1];
|
|
4087
4434
|
}
|
|
4088
4435
|
return "";
|
|
@@ -4092,7 +4439,7 @@ function findContainingClass(lines, lineIdx) {
|
|
|
4092
4439
|
* by scanning backwards from `beforeLine`.
|
|
4093
4440
|
*/
|
|
4094
4441
|
function findClassDeclarationStart(lines, className, beforeLine) {
|
|
4095
|
-
const classPattern = new RegExp(
|
|
4442
|
+
const classPattern = new RegExp(`^\\s*(?:export\\s+)?class\\s+${className}\\b`);
|
|
4096
4443
|
for (let i = beforeLine - 1; i >= 0; i--) {
|
|
4097
4444
|
if (!classPattern.test(lines[i])) continue;
|
|
4098
4445
|
let start = i;
|
|
@@ -4251,7 +4598,7 @@ function deriveMethodName(op, _service) {
|
|
|
4251
4598
|
const termIdx = segments.lastIndexOf(staticSegments[staticSegments.length - 1]);
|
|
4252
4599
|
if (segments.slice(penIdx + 1, termIdx).some((s) => s.startsWith("{"))) resource = `${toSnake(singularize(penultimate))}_${resource}`;
|
|
4253
4600
|
}
|
|
4254
|
-
const noun = trailingParam ? singularize(resource) : resource;
|
|
4601
|
+
const noun = trailingParam || verb === "create" ? singularize(resource) : resource;
|
|
4255
4602
|
return `${verb}_${verb === "list" ? resource : noun}`;
|
|
4256
4603
|
}
|
|
4257
4604
|
/**
|
|
@@ -4277,7 +4624,7 @@ function resolveOperations(spec, hints, mountRules) {
|
|
|
4277
4624
|
defaults: sh.defaults ?? {},
|
|
4278
4625
|
inferFromClient: sh.inferFromClient ?? [],
|
|
4279
4626
|
exposedParams: sh.exposedParams ?? [],
|
|
4280
|
-
optionalParams: [],
|
|
4627
|
+
optionalParams: sh.optionalParams ?? [],
|
|
4281
4628
|
responseModelName: resolveResponseModelName(op)
|
|
4282
4629
|
}));
|
|
4283
4630
|
resolved.push({
|
|
@@ -4287,7 +4634,8 @@ function resolveOperations(spec, hints, mountRules) {
|
|
|
4287
4634
|
mountOn,
|
|
4288
4635
|
wrappers,
|
|
4289
4636
|
defaults: hint?.defaults ?? {},
|
|
4290
|
-
inferFromClient: hint?.inferFromClient ?? []
|
|
4637
|
+
inferFromClient: hint?.inferFromClient ?? [],
|
|
4638
|
+
urlBuilder: hint?.urlBuilder ?? false
|
|
4291
4639
|
});
|
|
4292
4640
|
}
|
|
4293
4641
|
return resolved;
|
|
@@ -4310,7 +4658,9 @@ function buildEmitterContext(spec, options) {
|
|
|
4310
4658
|
outputDir: options.outputDir,
|
|
4311
4659
|
apiSurface: options.apiSurface,
|
|
4312
4660
|
overlayLookup: options.overlayLookup,
|
|
4313
|
-
resolvedOperations: resolveOperations(spec, options.operationHints, options.mountRules)
|
|
4661
|
+
resolvedOperations: resolveOperations(spec, options.operationHints, options.mountRules),
|
|
4662
|
+
targetDir: options.target,
|
|
4663
|
+
priorTargetManifestPaths: options.priorTargetManifestPaths
|
|
4314
4664
|
};
|
|
4315
4665
|
}
|
|
4316
4666
|
/**
|
|
@@ -4429,7 +4779,7 @@ function resolveImportPath(fromFilePath, importModulePath, knownPaths) {
|
|
|
4429
4779
|
*/
|
|
4430
4780
|
function isRootFile(filePath) {
|
|
4431
4781
|
if (filePath.includes("/fixtures/")) return false;
|
|
4432
|
-
if (filePath.endsWith(".spec.ts") || filePath.endsWith(".test.ts")) return
|
|
4782
|
+
if (filePath.endsWith(".spec.ts") || filePath.endsWith(".test.ts")) return true;
|
|
4433
4783
|
if (filePath.includes("/serializers/")) return false;
|
|
4434
4784
|
if (filePath.includes("/interfaces/") && !filePath.endsWith("/index.ts")) return false;
|
|
4435
4785
|
return true;
|
|
@@ -4558,7 +4908,8 @@ async function resolveFileToDirectoryConflicts(files, targetDir) {
|
|
|
4558
4908
|
};
|
|
4559
4909
|
}
|
|
4560
4910
|
async function integrateGeneratedFiles(opts) {
|
|
4561
|
-
const
|
|
4911
|
+
const mapped = mapFilesForTargetIntegration(opts.files, opts.language);
|
|
4912
|
+
const { files: resolved, removals } = await resolveFileToDirectoryConflicts(await treeShakeFiles(mapped, opts.language), opts.targetDir);
|
|
4562
4913
|
const result = await writeFiles(resolved, opts.targetDir, {
|
|
4563
4914
|
language: opts.language,
|
|
4564
4915
|
header: opts.header
|
|
@@ -4566,7 +4917,10 @@ async function integrateGeneratedFiles(opts) {
|
|
|
4566
4917
|
for (const filePath of removals) try {
|
|
4567
4918
|
await fs.unlink(filePath);
|
|
4568
4919
|
} catch {}
|
|
4569
|
-
return
|
|
4920
|
+
return {
|
|
4921
|
+
...result,
|
|
4922
|
+
emittedPaths: mapped.map((f) => f.path)
|
|
4923
|
+
};
|
|
4570
4924
|
}
|
|
4571
4925
|
//#endregion
|
|
4572
4926
|
//#region src/engine/formatter.ts
|
|
@@ -4589,6 +4943,7 @@ async function formatTargetFiles(emitter, targetDir, filePaths) {
|
|
|
4589
4943
|
if (!formatCmd) return;
|
|
4590
4944
|
const absolutePaths = filePaths.map((f) => path$1.resolve(targetDir, f));
|
|
4591
4945
|
const batchSize = formatCmd.batchSize ?? 100;
|
|
4946
|
+
process.stderr.write(`[oagen] formatting ${absolutePaths.length} file(s) with ${formatCmd.cmd} (batch ${batchSize})\n`);
|
|
4592
4947
|
for (let i = 0; i < absolutePaths.length; i += batchSize) {
|
|
4593
4948
|
const batch = absolutePaths.slice(i, i + batchSize);
|
|
4594
4949
|
try {
|
|
@@ -4597,13 +4952,148 @@ async function formatTargetFiles(emitter, targetDir, filePaths) {
|
|
|
4597
4952
|
maxBuffer: 10 * 1024 * 1024,
|
|
4598
4953
|
timeout: 12e4
|
|
4599
4954
|
});
|
|
4600
|
-
} catch {
|
|
4955
|
+
} catch (err) {
|
|
4956
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4957
|
+
process.stderr.write(`[oagen] formatter batch failed: ${msg}\n`);
|
|
4958
|
+
}
|
|
4959
|
+
}
|
|
4960
|
+
}
|
|
4961
|
+
//#endregion
|
|
4962
|
+
//#region src/engine/manifest.ts
|
|
4963
|
+
/**
|
|
4964
|
+
* Prune-manifest support for `oagen generate`.
|
|
4965
|
+
*
|
|
4966
|
+
* The manifest records every auto-generated file emitted on the previous run,
|
|
4967
|
+
* relative to the directory it lives in (either the `--output` standalone dir
|
|
4968
|
+
* or the `--target` integration dir). On the next run we diff the previous
|
|
4969
|
+
* list against the current emission set and delete anything that's no longer
|
|
4970
|
+
* produced — preventing stale-file accumulation across regens.
|
|
4971
|
+
*
|
|
4972
|
+
* Design notes:
|
|
4973
|
+
* - The manifest is versioned so future format changes can be handled safely.
|
|
4974
|
+
* - Paths are stored sorted for deterministic diffs in git.
|
|
4975
|
+
* - Deletion is gated on a header guard when one is available: we only remove
|
|
4976
|
+
* files whose contents start with the auto-generated header. This protects
|
|
4977
|
+
* a hand-maintained file that happens to share a name with a previously
|
|
4978
|
+
* generated one (extremely rare, but worth catching).
|
|
4979
|
+
* - On first adoption (no previous manifest), pruning is skipped and a
|
|
4980
|
+
* one-line notice is printed — no surprise deletions.
|
|
4981
|
+
*/
|
|
4982
|
+
const MANIFEST_FILENAME = ".oagen-manifest.json";
|
|
4983
|
+
const MANIFEST_VERSION = 1;
|
|
4984
|
+
/** Read `.oagen-manifest.json` from a directory. Returns null if absent or malformed. */
|
|
4985
|
+
async function readManifest(dir) {
|
|
4986
|
+
const manifestPath = path$1.join(dir, MANIFEST_FILENAME);
|
|
4987
|
+
let raw;
|
|
4988
|
+
try {
|
|
4989
|
+
raw = await fs.readFile(manifestPath, "utf-8");
|
|
4990
|
+
} catch {
|
|
4991
|
+
return null;
|
|
4992
|
+
}
|
|
4993
|
+
try {
|
|
4994
|
+
const parsed = JSON.parse(raw);
|
|
4995
|
+
if (typeof parsed.version !== "number" || typeof parsed.language !== "string" || typeof parsed.generatedAt !== "string" || !Array.isArray(parsed.files) || !parsed.files.every((p) => typeof p === "string")) return null;
|
|
4996
|
+
if (parsed.version > MANIFEST_VERSION) {
|
|
4997
|
+
console.warn(`[oagen] ${MANIFEST_FILENAME} schema version ${parsed.version} is newer than supported (${MANIFEST_VERSION}); ignoring for pruning.`);
|
|
4998
|
+
return null;
|
|
4999
|
+
}
|
|
5000
|
+
return parsed;
|
|
5001
|
+
} catch {
|
|
5002
|
+
return null;
|
|
5003
|
+
}
|
|
5004
|
+
}
|
|
5005
|
+
/** Write `.oagen-manifest.json` to a directory with sorted paths. */
|
|
5006
|
+
async function writeManifest(dir, opts) {
|
|
5007
|
+
const manifest = {
|
|
5008
|
+
version: MANIFEST_VERSION,
|
|
5009
|
+
language: opts.language,
|
|
5010
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5011
|
+
files: [...new Set(opts.files)].sort()
|
|
5012
|
+
};
|
|
5013
|
+
const manifestPath = path$1.join(dir, MANIFEST_FILENAME);
|
|
5014
|
+
await fs.mkdir(dir, { recursive: true });
|
|
5015
|
+
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
5016
|
+
}
|
|
5017
|
+
/** Return the set of previous paths no longer present in the current emission. */
|
|
5018
|
+
function computeStalePaths(prev, currentPaths) {
|
|
5019
|
+
const current = new Set(currentPaths);
|
|
5020
|
+
return prev.files.filter((p) => !current.has(p)).sort();
|
|
5021
|
+
}
|
|
5022
|
+
/**
|
|
5023
|
+
* Delete stale files from `dir`.
|
|
5024
|
+
*
|
|
5025
|
+
* When `header` is provided, a file is only deleted if its contents start with
|
|
5026
|
+
* that header — so hand-maintained files that somehow collide with a previously
|
|
5027
|
+
* generated path can't be clobbered. When `header` is omitted, the guard is
|
|
5028
|
+
* skipped (useful for non-source artifacts like fixture JSON).
|
|
5029
|
+
*
|
|
5030
|
+
* Returns lists for reporting. Empty parent directories are removed after
|
|
5031
|
+
* deletion, up to (but not including) `dir` itself.
|
|
5032
|
+
*/
|
|
5033
|
+
async function pruneStaleFiles(dir, paths, opts = {}) {
|
|
5034
|
+
const pruned = [];
|
|
5035
|
+
const preserved = [];
|
|
5036
|
+
const missing = [];
|
|
5037
|
+
for (const relPath of paths) {
|
|
5038
|
+
const fullPath = path$1.join(dir, relPath);
|
|
5039
|
+
let content;
|
|
5040
|
+
try {
|
|
5041
|
+
content = await fs.readFile(fullPath, "utf-8");
|
|
5042
|
+
} catch {
|
|
5043
|
+
missing.push(relPath);
|
|
5044
|
+
continue;
|
|
5045
|
+
}
|
|
5046
|
+
if (opts.header && !content.startsWith(opts.header)) {
|
|
5047
|
+
preserved.push(relPath);
|
|
5048
|
+
continue;
|
|
5049
|
+
}
|
|
5050
|
+
try {
|
|
5051
|
+
await fs.unlink(fullPath);
|
|
5052
|
+
pruned.push(relPath);
|
|
5053
|
+
} catch {
|
|
5054
|
+
missing.push(relPath);
|
|
5055
|
+
continue;
|
|
5056
|
+
}
|
|
5057
|
+
await removeEmptyParents(path$1.dirname(fullPath), dir);
|
|
5058
|
+
}
|
|
5059
|
+
return {
|
|
5060
|
+
pruned,
|
|
5061
|
+
preserved,
|
|
5062
|
+
missing
|
|
5063
|
+
};
|
|
5064
|
+
}
|
|
5065
|
+
/**
|
|
5066
|
+
* Walk up from `startDir` removing empty directories until we hit `stopDir`
|
|
5067
|
+
* (exclusive) or a non-empty directory. Silently ignores errors.
|
|
5068
|
+
*/
|
|
5069
|
+
async function removeEmptyParents(startDir, stopDir) {
|
|
5070
|
+
let current = path$1.resolve(startDir);
|
|
5071
|
+
const stop = path$1.resolve(stopDir);
|
|
5072
|
+
while (current.startsWith(stop + path$1.sep) && current !== stop) {
|
|
5073
|
+
let entries;
|
|
5074
|
+
try {
|
|
5075
|
+
entries = await fs.readdir(current);
|
|
5076
|
+
} catch {
|
|
5077
|
+
return;
|
|
5078
|
+
}
|
|
5079
|
+
if (entries.length > 0) return;
|
|
5080
|
+
try {
|
|
5081
|
+
await fs.rmdir(current);
|
|
5082
|
+
} catch {
|
|
5083
|
+
return;
|
|
5084
|
+
}
|
|
5085
|
+
current = path$1.dirname(current);
|
|
4601
5086
|
}
|
|
4602
5087
|
}
|
|
4603
5088
|
//#endregion
|
|
4604
5089
|
//#region src/engine/orchestrator.ts
|
|
4605
5090
|
async function generate(spec, emitter, options) {
|
|
4606
|
-
const
|
|
5091
|
+
const targetManifestForCtx = options.target && !options.noPrune ? await readManifest(options.target) : null;
|
|
5092
|
+
const priorTargetManifestPaths = targetManifestForCtx ? new Set(targetManifestForCtx.files) : void 0;
|
|
5093
|
+
const { files: withHeaders, header } = generateFiles(spec, emitter, {
|
|
5094
|
+
...options,
|
|
5095
|
+
priorTargetManifestPaths
|
|
5096
|
+
});
|
|
4607
5097
|
if (options.dryRun) {
|
|
4608
5098
|
if (options.target) {
|
|
4609
5099
|
console.log(`\nTarget integration (${options.target}):`);
|
|
@@ -4611,13 +5101,25 @@ async function generate(spec, emitter, options) {
|
|
|
4611
5101
|
}
|
|
4612
5102
|
return withHeaders;
|
|
4613
5103
|
}
|
|
5104
|
+
const outputPrevManifest = options.noPrune ? null : await readManifest(options.outputDir);
|
|
4614
5105
|
const writeResult = await writeFiles(withHeaders, options.outputDir, {
|
|
4615
5106
|
language: emitter.language,
|
|
4616
5107
|
header
|
|
4617
5108
|
});
|
|
4618
5109
|
if (writeResult.merged.length > 0) console.log(`Merged into ${writeResult.merged.length} existing files (additive only)`);
|
|
4619
5110
|
if (writeResult.ignored.length > 0) console.log(`Ignored ${writeResult.ignored.length} files (@oagen-ignore-file)`);
|
|
5111
|
+
const outputEmittedPaths = withHeaders.map((f) => f.path);
|
|
5112
|
+
await applyManifestPrune({
|
|
5113
|
+
dir: options.outputDir,
|
|
5114
|
+
label: "Output",
|
|
5115
|
+
prevManifest: outputPrevManifest,
|
|
5116
|
+
currentPaths: outputEmittedPaths,
|
|
5117
|
+
language: emitter.language,
|
|
5118
|
+
header,
|
|
5119
|
+
noPrune: options.noPrune
|
|
5120
|
+
});
|
|
4620
5121
|
if (options.target) {
|
|
5122
|
+
const targetPrevManifest = targetManifestForCtx;
|
|
4621
5123
|
const targetResult = await integrateGeneratedFiles({
|
|
4622
5124
|
files: withHeaders,
|
|
4623
5125
|
language: emitter.language,
|
|
@@ -4627,6 +5129,15 @@ async function generate(spec, emitter, options) {
|
|
|
4627
5129
|
if (targetResult.written.length > 0) console.log(`Target: created ${targetResult.written.length} new files`);
|
|
4628
5130
|
if (targetResult.merged.length > 0) console.log(`Target: merged into ${targetResult.merged.length} existing files (additive only)`);
|
|
4629
5131
|
if (targetResult.skipped.length > 0) console.log(`Target: skipped ${targetResult.skipped.length} files (excluded or no grammar)`);
|
|
5132
|
+
await applyManifestPrune({
|
|
5133
|
+
dir: options.target,
|
|
5134
|
+
label: "Target",
|
|
5135
|
+
prevManifest: targetPrevManifest,
|
|
5136
|
+
currentPaths: targetResult.emittedPaths,
|
|
5137
|
+
language: emitter.language,
|
|
5138
|
+
header,
|
|
5139
|
+
noPrune: options.noPrune
|
|
5140
|
+
});
|
|
4630
5141
|
const allTargetFiles = [
|
|
4631
5142
|
...targetResult.written,
|
|
4632
5143
|
...targetResult.merged,
|
|
@@ -4636,6 +5147,40 @@ async function generate(spec, emitter, options) {
|
|
|
4636
5147
|
}
|
|
4637
5148
|
return withHeaders;
|
|
4638
5149
|
}
|
|
5150
|
+
/**
|
|
5151
|
+
* Prune files recorded in the previous manifest but absent from the current emission,
|
|
5152
|
+
* then write the fresh manifest. Safe on first adoption (no previous manifest).
|
|
5153
|
+
*/
|
|
5154
|
+
async function applyManifestPrune(opts) {
|
|
5155
|
+
if (opts.noPrune) {
|
|
5156
|
+
await writeManifest(opts.dir, {
|
|
5157
|
+
language: opts.language,
|
|
5158
|
+
files: opts.currentPaths
|
|
5159
|
+
});
|
|
5160
|
+
return;
|
|
5161
|
+
}
|
|
5162
|
+
if (!opts.prevManifest) {
|
|
5163
|
+
console.log(`${opts.label}: no ${MANIFEST_FILENAME} found — skipping prune (this baseline manifest will be written now).`);
|
|
5164
|
+
await writeManifest(opts.dir, {
|
|
5165
|
+
language: opts.language,
|
|
5166
|
+
files: opts.currentPaths
|
|
5167
|
+
});
|
|
5168
|
+
return;
|
|
5169
|
+
}
|
|
5170
|
+
const stale = computeStalePaths(opts.prevManifest, opts.currentPaths);
|
|
5171
|
+
if (stale.length > 0) {
|
|
5172
|
+
const { pruned, preserved } = await pruneStaleFiles(opts.dir, stale, { header: opts.header });
|
|
5173
|
+
if (pruned.length > 0) console.log(`${opts.label}: pruned ${pruned.length} stale file${pruned.length === 1 ? "" : "s"}`);
|
|
5174
|
+
if (preserved.length > 0) {
|
|
5175
|
+
console.log(`${opts.label}: preserved ${preserved.length} file${preserved.length === 1 ? "" : "s"} in previous manifest but lacking the auto-generated header (possibly hand-edited).`);
|
|
5176
|
+
for (const p of preserved) console.log(` ${p}`);
|
|
5177
|
+
}
|
|
5178
|
+
}
|
|
5179
|
+
await writeManifest(opts.dir, {
|
|
5180
|
+
language: opts.language,
|
|
5181
|
+
files: opts.currentPaths
|
|
5182
|
+
});
|
|
5183
|
+
}
|
|
4639
5184
|
//#endregion
|
|
4640
5185
|
//#region src/engine/registry.ts
|
|
4641
5186
|
const emitters = /* @__PURE__ */ new Map();
|
|
@@ -5320,7 +5865,8 @@ async function generateCommand(opts) {
|
|
|
5320
5865
|
apiSurface,
|
|
5321
5866
|
overlayLookup,
|
|
5322
5867
|
operationHints: opts.operationHints,
|
|
5323
|
-
mountRules: opts.mountRules
|
|
5868
|
+
mountRules: opts.mountRules,
|
|
5869
|
+
noPrune: opts.prune === false
|
|
5324
5870
|
});
|
|
5325
5871
|
if (opts.dryRun) for (const f of files) console.log(f.path);
|
|
5326
5872
|
else {
|
|
@@ -7036,7 +7582,7 @@ program.command("parse").description("Parse an OpenAPI spec and output IR as JSO
|
|
|
7036
7582
|
}
|
|
7037
7583
|
return parseCommand(opts);
|
|
7038
7584
|
});
|
|
7039
|
-
program.command("generate").description("Run a registered emitter against an OpenAPI spec").option("--spec <path>", "Path to OpenAPI spec file (or set OPENAPI_SPEC_PATH)").requiredOption("--lang <language>", "Target language").requiredOption("--output <dir>", "Output directory").option("--target <dir>", "Target directory for live SDK integration (merged output)").option("--namespace <name>", "SDK namespace/package name").option("--dry-run", "Preview files without writing").option("--api-surface <path>", "Path to baseline API surface JSON for compat overlay").option("--manifest <path>", "Path to smoke-manifest.json for method overlay").option("--no-compat-check", "Skip compat overlay even if --api-surface is provided").action((opts) => {
|
|
7585
|
+
program.command("generate").description("Run a registered emitter against an OpenAPI spec").option("--spec <path>", "Path to OpenAPI spec file (or set OPENAPI_SPEC_PATH)").requiredOption("--lang <language>", "Target language").requiredOption("--output <dir>", "Output directory").option("--target <dir>", "Target directory for live SDK integration (merged output)").option("--namespace <name>", "SDK namespace/package name").option("--dry-run", "Preview files without writing").option("--api-surface <path>", "Path to baseline API surface JSON for compat overlay").option("--manifest <path>", "Path to smoke-manifest.json for method overlay").option("--no-compat-check", "Skip compat overlay even if --api-surface is provided").option("--no-prune", "Skip deletion of stale files recorded in the previous .oagen-manifest.json").action((opts) => {
|
|
7040
7586
|
opts.spec ??= process.env.OPENAPI_SPEC_PATH;
|
|
7041
7587
|
if (!opts.spec) {
|
|
7042
7588
|
console.error("error: --spec <path> or OPENAPI_SPEC_PATH env var is required");
|