@workos/oagen 0.5.0 → 0.6.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 +357 -12
- package/dist/cli/index.mjs.map +1 -1
- package/dist/compat/index.d.mts +1 -1
- package/dist/compat/index.mjs +10 -1
- package/dist/compat/index.mjs.map +1 -1
- package/dist/{diff-CSy3g-Bq.mjs → diff-CltJMcwG.mjs} +265 -10
- package/dist/diff-CltJMcwG.mjs.map +1 -0
- package/dist/index.d.mts +41 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +92 -2
- package/dist/index.mjs.map +1 -1
- package/dist/scripts/smoke/baseline.mjs +90 -1
- package/dist/scripts/smoke/baseline.mjs.map +1 -1
- package/dist/smoke.mjs +90 -1
- package/dist/smoke.mjs.map +1 -1
- package/dist/{types-D84LjrVG.d.mts → types-C7g5sLuX.d.mts} +6 -1
- package/dist/types-C7g5sLuX.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
|
@@ -679,6 +679,86 @@ function buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet)
|
|
|
679
679
|
example: fieldSchema.example
|
|
680
680
|
};
|
|
681
681
|
}
|
|
682
|
+
/**
|
|
683
|
+
* If every non-null oneOf/anyOf variant is a string-valued const, return the
|
|
684
|
+
* list of those const values so the union can collapse into a single enum.
|
|
685
|
+
* Returns null when any variant isn't a pure string-const (model ref,
|
|
686
|
+
* number const, nullable, etc.).
|
|
687
|
+
*/
|
|
688
|
+
function collectLiteralStringConsts(variants) {
|
|
689
|
+
if (variants.length === 0) return null;
|
|
690
|
+
const values = [];
|
|
691
|
+
for (const v of variants) {
|
|
692
|
+
if (typeof v.const === "string") {
|
|
693
|
+
values.push(v.const);
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
if (Array.isArray(v.enum) && v.enum.length === 1 && typeof v.enum[0] === "string") {
|
|
697
|
+
values.push(v.enum[0]);
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
return values;
|
|
703
|
+
}
|
|
704
|
+
/** Build an EnumRef the same way the regular enum path does for naming. */
|
|
705
|
+
function buildSyntheticEnumRef(values, contextName, parentModelName) {
|
|
706
|
+
const baseName = toPascalCase(contextName ?? "UnknownEnum");
|
|
707
|
+
const cleanParent = parentModelName ? stripListItemMarkers(parentModelName) : void 0;
|
|
708
|
+
return {
|
|
709
|
+
kind: "enum",
|
|
710
|
+
name: cleanParent && !baseName.startsWith(cleanParent) ? `${cleanParent}${baseName}` : baseName,
|
|
711
|
+
values
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Detect an implicit discriminator on an oneOf where every variant is an
|
|
716
|
+
* object schema that pins the same property to a const value. Returns
|
|
717
|
+
* { property, mapping } so the IR can represent the union as discriminated
|
|
718
|
+
* without needing an explicit `discriminator:` key in the spec.
|
|
719
|
+
*
|
|
720
|
+
* Both shapes are supported:
|
|
721
|
+
* { properties: { event: { const: "x" } }, required: ["event"] }
|
|
722
|
+
* { properties: { event: { type: "string", enum: ["x"] } } }
|
|
723
|
+
*
|
|
724
|
+
* Returns null when any variant doesn't fit the pattern or when variants
|
|
725
|
+
* disagree on which property carries the discriminator.
|
|
726
|
+
*/
|
|
727
|
+
function detectConstPropertyDiscriminator(variants) {
|
|
728
|
+
if (variants.length < 2) return null;
|
|
729
|
+
let candidateProperty = null;
|
|
730
|
+
const candidates = Object.keys(variants[0]?.properties ?? {});
|
|
731
|
+
for (const propName of candidates) if (variants.every((v) => getConstPropertyValue(v, propName) !== null)) {
|
|
732
|
+
candidateProperty = propName;
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
if (!candidateProperty) return null;
|
|
736
|
+
const mapping = {};
|
|
737
|
+
for (const v of variants) {
|
|
738
|
+
const value = getConstPropertyValue(v, candidateProperty);
|
|
739
|
+
if (value === null) return null;
|
|
740
|
+
const variantName = resolveVariantModelName(v);
|
|
741
|
+
if (!variantName) return null;
|
|
742
|
+
mapping[value] = variantName;
|
|
743
|
+
}
|
|
744
|
+
return {
|
|
745
|
+
property: candidateProperty,
|
|
746
|
+
mapping
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function getConstPropertyValue(schema, property) {
|
|
750
|
+
const prop = schema.properties?.[property];
|
|
751
|
+
if (!prop) return null;
|
|
752
|
+
if (typeof prop.const === "string") return prop.const;
|
|
753
|
+
if (Array.isArray(prop.enum) && prop.enum.length === 1 && typeof prop.enum[0] === "string") return prop.enum[0];
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
function resolveVariantModelName(schema) {
|
|
757
|
+
if (schema.$ref) return resolveSchemaName(schema.$ref.replace(/^#\/components\/schemas\//, ""));
|
|
758
|
+
const title = typeof schema["title"] === "string" ? schema["title"] : null;
|
|
759
|
+
if (title) return resolveSchemaName(title);
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
682
762
|
function extractEnum(name, schema) {
|
|
683
763
|
return {
|
|
684
764
|
name,
|
|
@@ -902,6 +982,15 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
|
|
|
902
982
|
kind: "nullable",
|
|
903
983
|
inner: schemaToTypeRef(nonNullVariants[0], contextName, parentModelName)
|
|
904
984
|
};
|
|
985
|
+
const literalStrings = collectLiteralStringConsts(nonNullVariants);
|
|
986
|
+
if (literalStrings !== null && literalStrings.length >= 2) {
|
|
987
|
+
const enumRef = buildSyntheticEnumRef(literalStrings, contextName, parentModelName);
|
|
988
|
+
return nullVariant ? {
|
|
989
|
+
kind: "nullable",
|
|
990
|
+
inner: enumRef
|
|
991
|
+
} : enumRef;
|
|
992
|
+
}
|
|
993
|
+
const syntheticDiscriminator = !schema.discriminator && compositionKind === "oneOf" ? detectConstPropertyDiscriminator(nonNullVariants) : null;
|
|
905
994
|
const variants = rawVariants.filter((v) => v.type !== "null").map((v) => schemaToTypeRef(v, contextName, parentModelName));
|
|
906
995
|
const hasNull = !!nullVariant;
|
|
907
996
|
const union = {
|
|
@@ -911,7 +1000,7 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
|
|
|
911
1000
|
...schema.discriminator ? { discriminator: {
|
|
912
1001
|
property: schema.discriminator.propertyName,
|
|
913
1002
|
mapping: Object.fromEntries(Object.entries(schema.discriminator.mapping ?? {}).map(([k, v]) => [k, v.replace(/^#\/components\/schemas\//, "")]))
|
|
914
|
-
} } : {}
|
|
1003
|
+
} } : syntheticDiscriminator ? { discriminator: syntheticDiscriminator } : {}
|
|
915
1004
|
};
|
|
916
1005
|
return hasNull ? {
|
|
917
1006
|
kind: "nullable",
|
|
@@ -2598,6 +2687,41 @@ function findPrecedingKdoc(children, index, source) {
|
|
|
2598
2687
|
}
|
|
2599
2688
|
return null;
|
|
2600
2689
|
}
|
|
2690
|
+
function extractKotlinClassMembers(classBody, source) {
|
|
2691
|
+
const members = [];
|
|
2692
|
+
const children = classBody.namedChildren;
|
|
2693
|
+
for (let i = 0; i < children.length; i++) {
|
|
2694
|
+
const child = children[i];
|
|
2695
|
+
let memberName = null;
|
|
2696
|
+
if (child.type === "property_declaration") memberName = child.children.find((c) => c.type === "variable_declaration")?.children.find((c) => c.type === "simple_identifier")?.text ?? null;
|
|
2697
|
+
else if (child.type === "function_declaration") memberName = child.children.find((c) => c.type === "simple_identifier")?.text ?? null;
|
|
2698
|
+
else if (child.type === "companion_object") memberName = "companion";
|
|
2699
|
+
if (!memberName) continue;
|
|
2700
|
+
let endIdx = child.endIndex;
|
|
2701
|
+
if (child.type === "property_declaration") while (i + 1 < children.length) {
|
|
2702
|
+
const next = children[i + 1];
|
|
2703
|
+
if (next.type === "getter" || next.type === "setter") {
|
|
2704
|
+
endIdx = next.endIndex;
|
|
2705
|
+
i++;
|
|
2706
|
+
} else break;
|
|
2707
|
+
}
|
|
2708
|
+
let startIdx = child.startIndex;
|
|
2709
|
+
for (let k = i - 1; k >= 0; k--) {
|
|
2710
|
+
const prev = children[k];
|
|
2711
|
+
if (prev.type === "multiline_comment") {
|
|
2712
|
+
if (source.slice(prev.startIndex, prev.endIndex).startsWith("/**")) startIdx = prev.startIndex;
|
|
2713
|
+
break;
|
|
2714
|
+
}
|
|
2715
|
+
if (prev.type === "line_comment") continue;
|
|
2716
|
+
break;
|
|
2717
|
+
}
|
|
2718
|
+
members.push({
|
|
2719
|
+
key: memberName,
|
|
2720
|
+
text: source.slice(startIdx, endIdx)
|
|
2721
|
+
});
|
|
2722
|
+
}
|
|
2723
|
+
return members;
|
|
2724
|
+
}
|
|
2601
2725
|
const kotlinMergeAdapter = {
|
|
2602
2726
|
language: "kotlin",
|
|
2603
2727
|
grammarModule: "tree-sitter-kotlin",
|
|
@@ -2638,6 +2762,33 @@ const kotlinMergeAdapter = {
|
|
|
2638
2762
|
renderImports(imports) {
|
|
2639
2763
|
return imports.map((entry) => entry.text);
|
|
2640
2764
|
},
|
|
2765
|
+
shouldSkipDeepMerge(_symbolName, existingMemberKeys, newMembers) {
|
|
2766
|
+
const newText = newMembers.map((m) => m.text).join("\n");
|
|
2767
|
+
for (const match of newText.matchAll(/this\.(\w+)/g)) {
|
|
2768
|
+
const propName = match[1];
|
|
2769
|
+
if (propName && !existingMemberKeys.has(propName)) return true;
|
|
2770
|
+
}
|
|
2771
|
+
return false;
|
|
2772
|
+
},
|
|
2773
|
+
extractMembers(tree, source) {
|
|
2774
|
+
const result = /* @__PURE__ */ new Map();
|
|
2775
|
+
for (const child of tree.rootNode.children) {
|
|
2776
|
+
if (child.type !== "class_declaration") continue;
|
|
2777
|
+
const nameNode = child.children.find((c) => c.type === "type_identifier");
|
|
2778
|
+
if (!nameNode) continue;
|
|
2779
|
+
const body = child.children.find((c) => c.type === "class_body");
|
|
2780
|
+
if (!body) continue;
|
|
2781
|
+
const members = extractKotlinClassMembers(body, source);
|
|
2782
|
+
const firstMember = body.firstNamedChild;
|
|
2783
|
+
const memberIndent = firstMember ? " ".repeat(firstMember.startPosition.column) : void 0;
|
|
2784
|
+
result.set(nameNode.text, {
|
|
2785
|
+
members,
|
|
2786
|
+
bodyEndLine: body.endPosition.row,
|
|
2787
|
+
memberIndent
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
return result;
|
|
2791
|
+
},
|
|
2641
2792
|
extractDocstrings(tree, source) {
|
|
2642
2793
|
const result = /* @__PURE__ */ new Map();
|
|
2643
2794
|
const rootChildren = tree.rootNode.children;
|
|
@@ -3687,10 +3838,10 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
3687
3838
|
if (newMembers.length > 0) {
|
|
3688
3839
|
if (adapter.shouldSkipDeepMerge?.(symbolName, existingMemberKeys, newMembers)) continue;
|
|
3689
3840
|
const indent = existSymbol.memberIndent ?? " ";
|
|
3690
|
-
const insertText = newMembers.map((m) => indent + m.text).join("\n");
|
|
3841
|
+
const insertText = newMembers.map((m) => indent + m.text).join("\n\n");
|
|
3691
3842
|
insertions.push({
|
|
3692
3843
|
line: existSymbol.bodyEndLine,
|
|
3693
|
-
text: insertText
|
|
3844
|
+
text: "\n" + insertText
|
|
3694
3845
|
});
|
|
3695
3846
|
deepAdded += newMembers.length;
|
|
3696
3847
|
}
|
|
@@ -4251,7 +4402,7 @@ function deriveMethodName(op, _service) {
|
|
|
4251
4402
|
const termIdx = segments.lastIndexOf(staticSegments[staticSegments.length - 1]);
|
|
4252
4403
|
if (segments.slice(penIdx + 1, termIdx).some((s) => s.startsWith("{"))) resource = `${toSnake(singularize(penultimate))}_${resource}`;
|
|
4253
4404
|
}
|
|
4254
|
-
const noun = trailingParam ? singularize(resource) : resource;
|
|
4405
|
+
const noun = trailingParam || verb === "create" ? singularize(resource) : resource;
|
|
4255
4406
|
return `${verb}_${verb === "list" ? resource : noun}`;
|
|
4256
4407
|
}
|
|
4257
4408
|
/**
|
|
@@ -4277,7 +4428,7 @@ function resolveOperations(spec, hints, mountRules) {
|
|
|
4277
4428
|
defaults: sh.defaults ?? {},
|
|
4278
4429
|
inferFromClient: sh.inferFromClient ?? [],
|
|
4279
4430
|
exposedParams: sh.exposedParams ?? [],
|
|
4280
|
-
optionalParams: [],
|
|
4431
|
+
optionalParams: sh.optionalParams ?? [],
|
|
4281
4432
|
responseModelName: resolveResponseModelName(op)
|
|
4282
4433
|
}));
|
|
4283
4434
|
resolved.push({
|
|
@@ -4287,7 +4438,8 @@ function resolveOperations(spec, hints, mountRules) {
|
|
|
4287
4438
|
mountOn,
|
|
4288
4439
|
wrappers,
|
|
4289
4440
|
defaults: hint?.defaults ?? {},
|
|
4290
|
-
inferFromClient: hint?.inferFromClient ?? []
|
|
4441
|
+
inferFromClient: hint?.inferFromClient ?? [],
|
|
4442
|
+
urlBuilder: hint?.urlBuilder ?? false
|
|
4291
4443
|
});
|
|
4292
4444
|
}
|
|
4293
4445
|
return resolved;
|
|
@@ -4310,7 +4462,9 @@ function buildEmitterContext(spec, options) {
|
|
|
4310
4462
|
outputDir: options.outputDir,
|
|
4311
4463
|
apiSurface: options.apiSurface,
|
|
4312
4464
|
overlayLookup: options.overlayLookup,
|
|
4313
|
-
resolvedOperations: resolveOperations(spec, options.operationHints, options.mountRules)
|
|
4465
|
+
resolvedOperations: resolveOperations(spec, options.operationHints, options.mountRules),
|
|
4466
|
+
targetDir: options.target,
|
|
4467
|
+
priorTargetManifestPaths: options.priorTargetManifestPaths
|
|
4314
4468
|
};
|
|
4315
4469
|
}
|
|
4316
4470
|
/**
|
|
@@ -4429,7 +4583,7 @@ function resolveImportPath(fromFilePath, importModulePath, knownPaths) {
|
|
|
4429
4583
|
*/
|
|
4430
4584
|
function isRootFile(filePath) {
|
|
4431
4585
|
if (filePath.includes("/fixtures/")) return false;
|
|
4432
|
-
if (filePath.endsWith(".spec.ts") || filePath.endsWith(".test.ts")) return
|
|
4586
|
+
if (filePath.endsWith(".spec.ts") || filePath.endsWith(".test.ts")) return true;
|
|
4433
4587
|
if (filePath.includes("/serializers/")) return false;
|
|
4434
4588
|
if (filePath.includes("/interfaces/") && !filePath.endsWith("/index.ts")) return false;
|
|
4435
4589
|
return true;
|
|
@@ -4566,7 +4720,10 @@ async function integrateGeneratedFiles(opts) {
|
|
|
4566
4720
|
for (const filePath of removals) try {
|
|
4567
4721
|
await fs.unlink(filePath);
|
|
4568
4722
|
} catch {}
|
|
4569
|
-
return
|
|
4723
|
+
return {
|
|
4724
|
+
...result,
|
|
4725
|
+
emittedPaths: resolved.map((f) => f.path)
|
|
4726
|
+
};
|
|
4570
4727
|
}
|
|
4571
4728
|
//#endregion
|
|
4572
4729
|
//#region src/engine/formatter.ts
|
|
@@ -4601,9 +4758,141 @@ async function formatTargetFiles(emitter, targetDir, filePaths) {
|
|
|
4601
4758
|
}
|
|
4602
4759
|
}
|
|
4603
4760
|
//#endregion
|
|
4761
|
+
//#region src/engine/manifest.ts
|
|
4762
|
+
/**
|
|
4763
|
+
* Prune-manifest support for `oagen generate`.
|
|
4764
|
+
*
|
|
4765
|
+
* The manifest records every auto-generated file emitted on the previous run,
|
|
4766
|
+
* relative to the directory it lives in (either the `--output` standalone dir
|
|
4767
|
+
* or the `--target` integration dir). On the next run we diff the previous
|
|
4768
|
+
* list against the current emission set and delete anything that's no longer
|
|
4769
|
+
* produced — preventing stale-file accumulation across regens.
|
|
4770
|
+
*
|
|
4771
|
+
* Design notes:
|
|
4772
|
+
* - The manifest is versioned so future format changes can be handled safely.
|
|
4773
|
+
* - Paths are stored sorted for deterministic diffs in git.
|
|
4774
|
+
* - Deletion is gated on a header guard when one is available: we only remove
|
|
4775
|
+
* files whose contents start with the auto-generated header. This protects
|
|
4776
|
+
* a hand-maintained file that happens to share a name with a previously
|
|
4777
|
+
* generated one (extremely rare, but worth catching).
|
|
4778
|
+
* - On first adoption (no previous manifest), pruning is skipped and a
|
|
4779
|
+
* one-line notice is printed — no surprise deletions.
|
|
4780
|
+
*/
|
|
4781
|
+
const MANIFEST_FILENAME = ".oagen-manifest.json";
|
|
4782
|
+
const MANIFEST_VERSION = 1;
|
|
4783
|
+
/** Read `.oagen-manifest.json` from a directory. Returns null if absent or malformed. */
|
|
4784
|
+
async function readManifest(dir) {
|
|
4785
|
+
const manifestPath = path$1.join(dir, MANIFEST_FILENAME);
|
|
4786
|
+
let raw;
|
|
4787
|
+
try {
|
|
4788
|
+
raw = await fs.readFile(manifestPath, "utf-8");
|
|
4789
|
+
} catch {
|
|
4790
|
+
return null;
|
|
4791
|
+
}
|
|
4792
|
+
try {
|
|
4793
|
+
const parsed = JSON.parse(raw);
|
|
4794
|
+
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;
|
|
4795
|
+
if (parsed.version > MANIFEST_VERSION) {
|
|
4796
|
+
console.warn(`[oagen] ${MANIFEST_FILENAME} schema version ${parsed.version} is newer than supported (${MANIFEST_VERSION}); ignoring for pruning.`);
|
|
4797
|
+
return null;
|
|
4798
|
+
}
|
|
4799
|
+
return parsed;
|
|
4800
|
+
} catch {
|
|
4801
|
+
return null;
|
|
4802
|
+
}
|
|
4803
|
+
}
|
|
4804
|
+
/** Write `.oagen-manifest.json` to a directory with sorted paths. */
|
|
4805
|
+
async function writeManifest(dir, opts) {
|
|
4806
|
+
const manifest = {
|
|
4807
|
+
version: MANIFEST_VERSION,
|
|
4808
|
+
language: opts.language,
|
|
4809
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4810
|
+
files: [...new Set(opts.files)].sort()
|
|
4811
|
+
};
|
|
4812
|
+
const manifestPath = path$1.join(dir, MANIFEST_FILENAME);
|
|
4813
|
+
await fs.mkdir(dir, { recursive: true });
|
|
4814
|
+
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
4815
|
+
}
|
|
4816
|
+
/** Return the set of previous paths no longer present in the current emission. */
|
|
4817
|
+
function computeStalePaths(prev, currentPaths) {
|
|
4818
|
+
const current = new Set(currentPaths);
|
|
4819
|
+
return prev.files.filter((p) => !current.has(p)).sort();
|
|
4820
|
+
}
|
|
4821
|
+
/**
|
|
4822
|
+
* Delete stale files from `dir`.
|
|
4823
|
+
*
|
|
4824
|
+
* When `header` is provided, a file is only deleted if its contents start with
|
|
4825
|
+
* that header — so hand-maintained files that somehow collide with a previously
|
|
4826
|
+
* generated path can't be clobbered. When `header` is omitted, the guard is
|
|
4827
|
+
* skipped (useful for non-source artifacts like fixture JSON).
|
|
4828
|
+
*
|
|
4829
|
+
* Returns lists for reporting. Empty parent directories are removed after
|
|
4830
|
+
* deletion, up to (but not including) `dir` itself.
|
|
4831
|
+
*/
|
|
4832
|
+
async function pruneStaleFiles(dir, paths, opts = {}) {
|
|
4833
|
+
const pruned = [];
|
|
4834
|
+
const preserved = [];
|
|
4835
|
+
const missing = [];
|
|
4836
|
+
for (const relPath of paths) {
|
|
4837
|
+
const fullPath = path$1.join(dir, relPath);
|
|
4838
|
+
let content;
|
|
4839
|
+
try {
|
|
4840
|
+
content = await fs.readFile(fullPath, "utf-8");
|
|
4841
|
+
} catch {
|
|
4842
|
+
missing.push(relPath);
|
|
4843
|
+
continue;
|
|
4844
|
+
}
|
|
4845
|
+
if (opts.header && !content.startsWith(opts.header)) {
|
|
4846
|
+
preserved.push(relPath);
|
|
4847
|
+
continue;
|
|
4848
|
+
}
|
|
4849
|
+
try {
|
|
4850
|
+
await fs.unlink(fullPath);
|
|
4851
|
+
pruned.push(relPath);
|
|
4852
|
+
} catch {
|
|
4853
|
+
missing.push(relPath);
|
|
4854
|
+
continue;
|
|
4855
|
+
}
|
|
4856
|
+
await removeEmptyParents(path$1.dirname(fullPath), dir);
|
|
4857
|
+
}
|
|
4858
|
+
return {
|
|
4859
|
+
pruned,
|
|
4860
|
+
preserved,
|
|
4861
|
+
missing
|
|
4862
|
+
};
|
|
4863
|
+
}
|
|
4864
|
+
/**
|
|
4865
|
+
* Walk up from `startDir` removing empty directories until we hit `stopDir`
|
|
4866
|
+
* (exclusive) or a non-empty directory. Silently ignores errors.
|
|
4867
|
+
*/
|
|
4868
|
+
async function removeEmptyParents(startDir, stopDir) {
|
|
4869
|
+
let current = path$1.resolve(startDir);
|
|
4870
|
+
const stop = path$1.resolve(stopDir);
|
|
4871
|
+
while (current.startsWith(stop + path$1.sep) && current !== stop) {
|
|
4872
|
+
let entries;
|
|
4873
|
+
try {
|
|
4874
|
+
entries = await fs.readdir(current);
|
|
4875
|
+
} catch {
|
|
4876
|
+
return;
|
|
4877
|
+
}
|
|
4878
|
+
if (entries.length > 0) return;
|
|
4879
|
+
try {
|
|
4880
|
+
await fs.rmdir(current);
|
|
4881
|
+
} catch {
|
|
4882
|
+
return;
|
|
4883
|
+
}
|
|
4884
|
+
current = path$1.dirname(current);
|
|
4885
|
+
}
|
|
4886
|
+
}
|
|
4887
|
+
//#endregion
|
|
4604
4888
|
//#region src/engine/orchestrator.ts
|
|
4605
4889
|
async function generate(spec, emitter, options) {
|
|
4606
|
-
const
|
|
4890
|
+
const targetManifestForCtx = options.target && !options.noPrune ? await readManifest(options.target) : null;
|
|
4891
|
+
const priorTargetManifestPaths = targetManifestForCtx ? new Set(targetManifestForCtx.files) : void 0;
|
|
4892
|
+
const { files: withHeaders, header } = generateFiles(spec, emitter, {
|
|
4893
|
+
...options,
|
|
4894
|
+
priorTargetManifestPaths
|
|
4895
|
+
});
|
|
4607
4896
|
if (options.dryRun) {
|
|
4608
4897
|
if (options.target) {
|
|
4609
4898
|
console.log(`\nTarget integration (${options.target}):`);
|
|
@@ -4611,13 +4900,25 @@ async function generate(spec, emitter, options) {
|
|
|
4611
4900
|
}
|
|
4612
4901
|
return withHeaders;
|
|
4613
4902
|
}
|
|
4903
|
+
const outputPrevManifest = options.noPrune ? null : await readManifest(options.outputDir);
|
|
4614
4904
|
const writeResult = await writeFiles(withHeaders, options.outputDir, {
|
|
4615
4905
|
language: emitter.language,
|
|
4616
4906
|
header
|
|
4617
4907
|
});
|
|
4618
4908
|
if (writeResult.merged.length > 0) console.log(`Merged into ${writeResult.merged.length} existing files (additive only)`);
|
|
4619
4909
|
if (writeResult.ignored.length > 0) console.log(`Ignored ${writeResult.ignored.length} files (@oagen-ignore-file)`);
|
|
4910
|
+
const outputEmittedPaths = withHeaders.map((f) => f.path);
|
|
4911
|
+
await applyManifestPrune({
|
|
4912
|
+
dir: options.outputDir,
|
|
4913
|
+
label: "Output",
|
|
4914
|
+
prevManifest: outputPrevManifest,
|
|
4915
|
+
currentPaths: outputEmittedPaths,
|
|
4916
|
+
language: emitter.language,
|
|
4917
|
+
header,
|
|
4918
|
+
noPrune: options.noPrune
|
|
4919
|
+
});
|
|
4620
4920
|
if (options.target) {
|
|
4921
|
+
const targetPrevManifest = targetManifestForCtx;
|
|
4621
4922
|
const targetResult = await integrateGeneratedFiles({
|
|
4622
4923
|
files: withHeaders,
|
|
4623
4924
|
language: emitter.language,
|
|
@@ -4627,6 +4928,15 @@ async function generate(spec, emitter, options) {
|
|
|
4627
4928
|
if (targetResult.written.length > 0) console.log(`Target: created ${targetResult.written.length} new files`);
|
|
4628
4929
|
if (targetResult.merged.length > 0) console.log(`Target: merged into ${targetResult.merged.length} existing files (additive only)`);
|
|
4629
4930
|
if (targetResult.skipped.length > 0) console.log(`Target: skipped ${targetResult.skipped.length} files (excluded or no grammar)`);
|
|
4931
|
+
await applyManifestPrune({
|
|
4932
|
+
dir: options.target,
|
|
4933
|
+
label: "Target",
|
|
4934
|
+
prevManifest: targetPrevManifest,
|
|
4935
|
+
currentPaths: targetResult.emittedPaths,
|
|
4936
|
+
language: emitter.language,
|
|
4937
|
+
header,
|
|
4938
|
+
noPrune: options.noPrune
|
|
4939
|
+
});
|
|
4630
4940
|
const allTargetFiles = [
|
|
4631
4941
|
...targetResult.written,
|
|
4632
4942
|
...targetResult.merged,
|
|
@@ -4636,6 +4946,40 @@ async function generate(spec, emitter, options) {
|
|
|
4636
4946
|
}
|
|
4637
4947
|
return withHeaders;
|
|
4638
4948
|
}
|
|
4949
|
+
/**
|
|
4950
|
+
* Prune files recorded in the previous manifest but absent from the current emission,
|
|
4951
|
+
* then write the fresh manifest. Safe on first adoption (no previous manifest).
|
|
4952
|
+
*/
|
|
4953
|
+
async function applyManifestPrune(opts) {
|
|
4954
|
+
if (opts.noPrune) {
|
|
4955
|
+
await writeManifest(opts.dir, {
|
|
4956
|
+
language: opts.language,
|
|
4957
|
+
files: opts.currentPaths
|
|
4958
|
+
});
|
|
4959
|
+
return;
|
|
4960
|
+
}
|
|
4961
|
+
if (!opts.prevManifest) {
|
|
4962
|
+
console.log(`${opts.label}: no ${MANIFEST_FILENAME} found — skipping prune (this baseline manifest will be written now).`);
|
|
4963
|
+
await writeManifest(opts.dir, {
|
|
4964
|
+
language: opts.language,
|
|
4965
|
+
files: opts.currentPaths
|
|
4966
|
+
});
|
|
4967
|
+
return;
|
|
4968
|
+
}
|
|
4969
|
+
const stale = computeStalePaths(opts.prevManifest, opts.currentPaths);
|
|
4970
|
+
if (stale.length > 0) {
|
|
4971
|
+
const { pruned, preserved } = await pruneStaleFiles(opts.dir, stale, { header: opts.header });
|
|
4972
|
+
if (pruned.length > 0) console.log(`${opts.label}: pruned ${pruned.length} stale file${pruned.length === 1 ? "" : "s"}`);
|
|
4973
|
+
if (preserved.length > 0) {
|
|
4974
|
+
console.log(`${opts.label}: preserved ${preserved.length} file${preserved.length === 1 ? "" : "s"} in previous manifest but lacking the auto-generated header (possibly hand-edited).`);
|
|
4975
|
+
for (const p of preserved) console.log(` ${p}`);
|
|
4976
|
+
}
|
|
4977
|
+
}
|
|
4978
|
+
await writeManifest(opts.dir, {
|
|
4979
|
+
language: opts.language,
|
|
4980
|
+
files: opts.currentPaths
|
|
4981
|
+
});
|
|
4982
|
+
}
|
|
4639
4983
|
//#endregion
|
|
4640
4984
|
//#region src/engine/registry.ts
|
|
4641
4985
|
const emitters = /* @__PURE__ */ new Map();
|
|
@@ -5320,7 +5664,8 @@ async function generateCommand(opts) {
|
|
|
5320
5664
|
apiSurface,
|
|
5321
5665
|
overlayLookup,
|
|
5322
5666
|
operationHints: opts.operationHints,
|
|
5323
|
-
mountRules: opts.mountRules
|
|
5667
|
+
mountRules: opts.mountRules,
|
|
5668
|
+
noPrune: opts.prune === false
|
|
5324
5669
|
});
|
|
5325
5670
|
if (opts.dryRun) for (const f of files) console.log(f.path);
|
|
5326
5671
|
else {
|
|
@@ -7036,7 +7381,7 @@ program.command("parse").description("Parse an OpenAPI spec and output IR as JSO
|
|
|
7036
7381
|
}
|
|
7037
7382
|
return parseCommand(opts);
|
|
7038
7383
|
});
|
|
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) => {
|
|
7384
|
+
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
7385
|
opts.spec ??= process.env.OPENAPI_SPEC_PATH;
|
|
7041
7386
|
if (!opts.spec) {
|
|
7042
7387
|
console.error("error: --spec <path> or OPENAPI_SPEC_PATH env var is required");
|