@workos/oagen 0.4.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/README.md +0 -1
- package/dist/cli/index.mjs +471 -49
- 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-DcY93sii.mjs → diff-CltJMcwG.mjs} +376 -42
- package/dist/diff-CltJMcwG.mjs.map +1 -0
- package/dist/index.d.mts +45 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +95 -3
- package/dist/index.mjs.map +1 -1
- package/dist/scripts/smoke/baseline.mjs +93 -2
- package/dist/scripts/smoke/baseline.mjs.map +1 -1
- package/dist/smoke.d.mts +2 -0
- package/dist/smoke.d.mts.map +1 -1
- package/dist/smoke.mjs +93 -2
- package/dist/smoke.mjs.map +1 -1
- package/dist/tree-sitter-C53w_0WE.mjs.map +1 -1
- package/dist/{types-D1tMqLVB.d.mts → types-C7g5sLuX.d.mts} +8 -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/docs/agents/architecture.md +1 -1
- package/docs/architecture/emitter-contract.md +1 -2
- package/docs/architecture/pipeline.md +0 -1
- package/docs/core/minimal-emitter.md +0 -4
- package/docs/core/quickstart.md +0 -1
- package/examples/reference-emitter/README.md +0 -1
- package/examples/reference-emitter/src/emitter.ts +0 -5
- package/package.json +1 -1
- package/dist/diff-DcY93sii.mjs.map +0 -1
- package/dist/types-D1tMqLVB.d.mts.map +0 -1
- package/examples/reference-emitter/src/generators/config.ts +0 -19
|
@@ -94,7 +94,7 @@ function deriveMethodName(op, _service) {
|
|
|
94
94
|
const termIdx = segments.lastIndexOf(staticSegments[staticSegments.length - 1]);
|
|
95
95
|
if (segments.slice(penIdx + 1, termIdx).some((s) => s.startsWith("{"))) resource = `${toSnake(singularize(penultimate))}_${resource}`;
|
|
96
96
|
}
|
|
97
|
-
const noun = trailingParam ? singularize(resource) : resource;
|
|
97
|
+
const noun = trailingParam || verb === "create" ? singularize(resource) : resource;
|
|
98
98
|
return `${verb}_${verb === "list" ? resource : noun}`;
|
|
99
99
|
}
|
|
100
100
|
/**
|
|
@@ -120,7 +120,7 @@ function resolveOperations(spec, hints, mountRules) {
|
|
|
120
120
|
defaults: sh.defaults ?? {},
|
|
121
121
|
inferFromClient: sh.inferFromClient ?? [],
|
|
122
122
|
exposedParams: sh.exposedParams ?? [],
|
|
123
|
-
optionalParams: [],
|
|
123
|
+
optionalParams: sh.optionalParams ?? [],
|
|
124
124
|
responseModelName: resolveResponseModelName(op)
|
|
125
125
|
}));
|
|
126
126
|
resolved.push({
|
|
@@ -128,7 +128,10 @@ function resolveOperations(spec, hints, mountRules) {
|
|
|
128
128
|
service,
|
|
129
129
|
methodName,
|
|
130
130
|
mountOn,
|
|
131
|
-
wrappers
|
|
131
|
+
wrappers,
|
|
132
|
+
defaults: hint?.defaults ?? {},
|
|
133
|
+
inferFromClient: hint?.inferFromClient ?? [],
|
|
134
|
+
urlBuilder: hint?.urlBuilder ?? false
|
|
132
135
|
});
|
|
133
136
|
}
|
|
134
137
|
return resolved;
|
|
@@ -673,6 +676,41 @@ function findPrecedingKdoc(children, index, source) {
|
|
|
673
676
|
}
|
|
674
677
|
return null;
|
|
675
678
|
}
|
|
679
|
+
function extractKotlinClassMembers(classBody, source) {
|
|
680
|
+
const members = [];
|
|
681
|
+
const children = classBody.namedChildren;
|
|
682
|
+
for (let i = 0; i < children.length; i++) {
|
|
683
|
+
const child = children[i];
|
|
684
|
+
let memberName = null;
|
|
685
|
+
if (child.type === "property_declaration") memberName = child.children.find((c) => c.type === "variable_declaration")?.children.find((c) => c.type === "simple_identifier")?.text ?? null;
|
|
686
|
+
else if (child.type === "function_declaration") memberName = child.children.find((c) => c.type === "simple_identifier")?.text ?? null;
|
|
687
|
+
else if (child.type === "companion_object") memberName = "companion";
|
|
688
|
+
if (!memberName) continue;
|
|
689
|
+
let endIdx = child.endIndex;
|
|
690
|
+
if (child.type === "property_declaration") while (i + 1 < children.length) {
|
|
691
|
+
const next = children[i + 1];
|
|
692
|
+
if (next.type === "getter" || next.type === "setter") {
|
|
693
|
+
endIdx = next.endIndex;
|
|
694
|
+
i++;
|
|
695
|
+
} else break;
|
|
696
|
+
}
|
|
697
|
+
let startIdx = child.startIndex;
|
|
698
|
+
for (let k = i - 1; k >= 0; k--) {
|
|
699
|
+
const prev = children[k];
|
|
700
|
+
if (prev.type === "multiline_comment") {
|
|
701
|
+
if (source.slice(prev.startIndex, prev.endIndex).startsWith("/**")) startIdx = prev.startIndex;
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
if (prev.type === "line_comment") continue;
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
members.push({
|
|
708
|
+
key: memberName,
|
|
709
|
+
text: source.slice(startIdx, endIdx)
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
return members;
|
|
713
|
+
}
|
|
676
714
|
const kotlinMergeAdapter = {
|
|
677
715
|
language: "kotlin",
|
|
678
716
|
grammarModule: "tree-sitter-kotlin",
|
|
@@ -713,6 +751,33 @@ const kotlinMergeAdapter = {
|
|
|
713
751
|
renderImports(imports) {
|
|
714
752
|
return imports.map((entry) => entry.text);
|
|
715
753
|
},
|
|
754
|
+
shouldSkipDeepMerge(_symbolName, existingMemberKeys, newMembers) {
|
|
755
|
+
const newText = newMembers.map((m) => m.text).join("\n");
|
|
756
|
+
for (const match of newText.matchAll(/this\.(\w+)/g)) {
|
|
757
|
+
const propName = match[1];
|
|
758
|
+
if (propName && !existingMemberKeys.has(propName)) return true;
|
|
759
|
+
}
|
|
760
|
+
return false;
|
|
761
|
+
},
|
|
762
|
+
extractMembers(tree, source) {
|
|
763
|
+
const result = /* @__PURE__ */ new Map();
|
|
764
|
+
for (const child of tree.rootNode.children) {
|
|
765
|
+
if (child.type !== "class_declaration") continue;
|
|
766
|
+
const nameNode = child.children.find((c) => c.type === "type_identifier");
|
|
767
|
+
if (!nameNode) continue;
|
|
768
|
+
const body = child.children.find((c) => c.type === "class_body");
|
|
769
|
+
if (!body) continue;
|
|
770
|
+
const members = extractKotlinClassMembers(body, source);
|
|
771
|
+
const firstMember = body.firstNamedChild;
|
|
772
|
+
const memberIndent = firstMember ? " ".repeat(firstMember.startPosition.column) : void 0;
|
|
773
|
+
result.set(nameNode.text, {
|
|
774
|
+
members,
|
|
775
|
+
bodyEndLine: body.endPosition.row,
|
|
776
|
+
memberIndent
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
return result;
|
|
780
|
+
},
|
|
716
781
|
extractDocstrings(tree, source) {
|
|
717
782
|
const result = /* @__PURE__ */ new Map();
|
|
718
783
|
const rootChildren = tree.rootNode.children;
|
|
@@ -1496,6 +1561,30 @@ function getMergeAdapter(language) {
|
|
|
1496
1561
|
* Supports any language with a tree-sitter grammar package.
|
|
1497
1562
|
*/
|
|
1498
1563
|
const parserCache = /* @__PURE__ */ new Map();
|
|
1564
|
+
/**
|
|
1565
|
+
* When replacing an existing docstring with a generated one, carry over any
|
|
1566
|
+
* `@deprecated` lines from the existing doc that aren't present in the
|
|
1567
|
+
* generated doc. This prevents hand-written deprecation notices from being
|
|
1568
|
+
* silently dropped during docstring refresh.
|
|
1569
|
+
*/
|
|
1570
|
+
function preserveDeprecatedTags(existingDoc, generatedDoc) {
|
|
1571
|
+
if (!existingDoc.includes("@deprecated")) return generatedDoc;
|
|
1572
|
+
if (generatedDoc.includes("@deprecated")) return generatedDoc;
|
|
1573
|
+
const deprecatedParts = [];
|
|
1574
|
+
for (const line of existingDoc.split("\n")) if (/@deprecated\b/.test(line)) {
|
|
1575
|
+
const match = line.match(/@deprecated\b.*/);
|
|
1576
|
+
if (match) {
|
|
1577
|
+
const cleaned = match[0].replace(/\s*\*\/\s*$/, "");
|
|
1578
|
+
deprecatedParts.push(` * ${cleaned}`);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
if (deprecatedParts.length === 0) return generatedDoc;
|
|
1582
|
+
const closingIdx = generatedDoc.lastIndexOf("*/");
|
|
1583
|
+
if (closingIdx === -1) return generatedDoc;
|
|
1584
|
+
const before = generatedDoc.slice(0, closingIdx);
|
|
1585
|
+
const after = generatedDoc.slice(closingIdx);
|
|
1586
|
+
return before + deprecatedParts.join("\n") + "\n " + after;
|
|
1587
|
+
}
|
|
1499
1588
|
function findIgnoredRegions(source) {
|
|
1500
1589
|
const regions = [];
|
|
1501
1590
|
const startTag = "@oagen-ignore-start";
|
|
@@ -1609,10 +1698,21 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
1609
1698
|
}
|
|
1610
1699
|
for (const anchor of existingStatements.importAnchors) lastImportEndIndex = existingContent.slice(0, existingContent.indexOf(anchor)).split("\n").length - 1 + anchor.split("\n").length - 1;
|
|
1611
1700
|
if (lastImportEndIndex === -1 && existingStatements.importInsertionAnchor) lastImportEndIndex = existingContent.slice(0, existingContent.indexOf(existingStatements.importInsertionAnchor)).split("\n").length - 1 + existingStatements.importInsertionAnchor.split("\n").length - 1;
|
|
1701
|
+
const existingExportedNames = /* @__PURE__ */ new Set();
|
|
1612
1702
|
for (const stmt of existingStatements.statements) {
|
|
1613
|
-
if (stmt.kind === "reexport")
|
|
1614
|
-
|
|
1615
|
-
|
|
1703
|
+
if (stmt.kind === "reexport") {
|
|
1704
|
+
existingReexports.add(adapter.normalizeReexport ? adapter.normalizeReexport(stmt.text.trim()) : stmt.text.trim());
|
|
1705
|
+
const braceMatch = stmt.text.match(/\{([^}]+)\}/);
|
|
1706
|
+
if (braceMatch) for (const part of braceMatch[1].split(",")) {
|
|
1707
|
+
const segments = part.trim().split(/\s+as\s+/);
|
|
1708
|
+
const exportedName = (segments[1] ?? segments[0]).trim();
|
|
1709
|
+
if (exportedName) existingExportedNames.add(exportedName);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
if (stmt.key) {
|
|
1713
|
+
existingKeys.add(stmt.key);
|
|
1714
|
+
existingExportedNames.add(stmt.key);
|
|
1715
|
+
} else existingUnnamedTexts.add(stmt.text.trim());
|
|
1616
1716
|
}
|
|
1617
1717
|
const headerLine = header.trim();
|
|
1618
1718
|
const newImports = [];
|
|
@@ -1672,6 +1772,17 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
1672
1772
|
preserved++;
|
|
1673
1773
|
continue;
|
|
1674
1774
|
}
|
|
1775
|
+
const braceMatch = stmt.text.match(/\{([^}]+)\}/);
|
|
1776
|
+
if (braceMatch) {
|
|
1777
|
+
const exportedNames = braceMatch[1].split(",").map((n) => {
|
|
1778
|
+
const parts = n.trim().split(/\s+as\s+/);
|
|
1779
|
+
return (parts[1] ?? parts[0]).trim();
|
|
1780
|
+
}).filter(Boolean);
|
|
1781
|
+
if (exportedNames.length > 0 && exportedNames.every((n) => existingExportedNames.has(n))) {
|
|
1782
|
+
preserved++;
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1675
1786
|
}
|
|
1676
1787
|
if (stmt.key && (existingKeys.has(stmt.key) || existingImportedNames.has(stmt.key))) {
|
|
1677
1788
|
preserved++;
|
|
@@ -1709,10 +1820,10 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
1709
1820
|
if (newMembers.length > 0) {
|
|
1710
1821
|
if (adapter.shouldSkipDeepMerge?.(symbolName, existingMemberKeys, newMembers)) continue;
|
|
1711
1822
|
const indent = existSymbol.memberIndent ?? " ";
|
|
1712
|
-
const insertText = newMembers.map((m) => indent + m.text).join("\n");
|
|
1823
|
+
const insertText = newMembers.map((m) => indent + m.text).join("\n\n");
|
|
1713
1824
|
insertions.push({
|
|
1714
1825
|
line: existSymbol.bodyEndLine,
|
|
1715
|
-
text: insertText
|
|
1826
|
+
text: "\n" + insertText
|
|
1716
1827
|
});
|
|
1717
1828
|
deepAdded += newMembers.length;
|
|
1718
1829
|
}
|
|
@@ -1773,10 +1884,11 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
1773
1884
|
const genDoc = genInfo.docstring && genInfo.docstring.text.trim() !== headerLine ? genInfo.docstring : null;
|
|
1774
1885
|
if (genDoc) if (existInfo.docstring) {
|
|
1775
1886
|
if (!existInfo.docstring.text.includes("@oagen-ignore") && existInfo.docstring.text !== genDoc.text) {
|
|
1887
|
+
const newText = preserveDeprecatedTags(existInfo.docstring.text, genDoc.text);
|
|
1776
1888
|
edits.push({
|
|
1777
1889
|
start: existInfo.docstring.startIndex,
|
|
1778
1890
|
end: existInfo.docstring.endIndex,
|
|
1779
|
-
newText
|
|
1891
|
+
newText
|
|
1780
1892
|
});
|
|
1781
1893
|
docstringUpdates++;
|
|
1782
1894
|
}
|
|
@@ -1793,14 +1905,16 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
1793
1905
|
const matchedExistMembers = /* @__PURE__ */ new Set();
|
|
1794
1906
|
for (const [memberName, genMember] of genInfo.members) {
|
|
1795
1907
|
const existMember = existInfo.members.get(memberName);
|
|
1796
|
-
if (!existMember
|
|
1908
|
+
if (!existMember) continue;
|
|
1797
1909
|
matchedExistMembers.add(memberName);
|
|
1910
|
+
if (!genMember.docstring) continue;
|
|
1798
1911
|
if (existMember.docstring) {
|
|
1799
1912
|
if (!existMember.docstring.text.includes("@oagen-ignore") && existMember.docstring.text !== genMember.docstring.text) {
|
|
1913
|
+
const newText = preserveDeprecatedTags(existMember.docstring.text, genMember.docstring.text);
|
|
1800
1914
|
edits.push({
|
|
1801
1915
|
start: existMember.docstring.startIndex,
|
|
1802
1916
|
end: existMember.docstring.endIndex,
|
|
1803
|
-
newText
|
|
1917
|
+
newText
|
|
1804
1918
|
});
|
|
1805
1919
|
docstringUpdates++;
|
|
1806
1920
|
}
|
|
@@ -1815,34 +1929,59 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
1815
1929
|
docstringUpdates++;
|
|
1816
1930
|
}
|
|
1817
1931
|
}
|
|
1818
|
-
|
|
1932
|
+
const unmatchedGenByFp = /* @__PURE__ */ new Map();
|
|
1933
|
+
for (const [genName, genMember] of genInfo.members) {
|
|
1934
|
+
if (matchedExistMembers.has(genName)) continue;
|
|
1819
1935
|
if (!genMember.docstring || !genMember.urlFingerprint) continue;
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1936
|
+
if (existInfo.members.has(genName)) continue;
|
|
1937
|
+
const bucket = unmatchedGenByFp.get(genMember.urlFingerprint) ?? [];
|
|
1938
|
+
bucket.push({
|
|
1939
|
+
name: genName,
|
|
1940
|
+
member: genMember
|
|
1941
|
+
});
|
|
1942
|
+
unmatchedGenByFp.set(genMember.urlFingerprint, bucket);
|
|
1943
|
+
}
|
|
1944
|
+
const unmatchedExistByFp = /* @__PURE__ */ new Map();
|
|
1945
|
+
for (const [existName, existMember] of existInfo.members) {
|
|
1946
|
+
if (matchedExistMembers.has(existName)) continue;
|
|
1947
|
+
if (!existMember.urlFingerprint) continue;
|
|
1948
|
+
const bucket = unmatchedExistByFp.get(existMember.urlFingerprint) ?? [];
|
|
1949
|
+
bucket.push({
|
|
1950
|
+
name: existName,
|
|
1951
|
+
member: existMember
|
|
1952
|
+
});
|
|
1953
|
+
unmatchedExistByFp.set(existMember.urlFingerprint, bucket);
|
|
1954
|
+
}
|
|
1955
|
+
for (const [fp, genBucket] of unmatchedGenByFp) {
|
|
1956
|
+
if (genBucket.length !== 1) continue;
|
|
1957
|
+
const existBucket = unmatchedExistByFp.get(fp);
|
|
1958
|
+
if (!existBucket || existBucket.length !== 1) continue;
|
|
1959
|
+
const genMember = genBucket[0].member;
|
|
1960
|
+
const existEntry = existBucket[0];
|
|
1961
|
+
const existMember = existEntry.member;
|
|
1962
|
+
if (!genMember.docstring) continue;
|
|
1963
|
+
if (existMember.docstring) {
|
|
1964
|
+
if (existMember.docstring.text.includes("@oagen-ignore")) continue;
|
|
1965
|
+
if (existMember.docstring.text !== genMember.docstring.text) {
|
|
1966
|
+
const newText = preserveDeprecatedTags(existMember.docstring.text, genMember.docstring.text);
|
|
1836
1967
|
edits.push({
|
|
1837
|
-
start:
|
|
1838
|
-
end:
|
|
1839
|
-
newText
|
|
1968
|
+
start: existMember.docstring.startIndex,
|
|
1969
|
+
end: existMember.docstring.endIndex,
|
|
1970
|
+
newText
|
|
1840
1971
|
});
|
|
1841
1972
|
docstringUpdates++;
|
|
1842
1973
|
}
|
|
1843
|
-
|
|
1844
|
-
|
|
1974
|
+
} else {
|
|
1975
|
+
const lineStart = existMember.declStartIndex - existMember.declColumn;
|
|
1976
|
+
const indent = " ".repeat(existMember.declColumn);
|
|
1977
|
+
edits.push({
|
|
1978
|
+
start: lineStart,
|
|
1979
|
+
end: lineStart,
|
|
1980
|
+
newText: indent + genMember.docstring.text + "\n"
|
|
1981
|
+
});
|
|
1982
|
+
docstringUpdates++;
|
|
1845
1983
|
}
|
|
1984
|
+
matchedExistMembers.add(existEntry.name);
|
|
1846
1985
|
}
|
|
1847
1986
|
}
|
|
1848
1987
|
if (edits.length > 0) {
|
|
@@ -2129,12 +2268,16 @@ function spliceExtraImports(existingLines, resultLines) {
|
|
|
2129
2268
|
const extraSimple = [...existingSimple].filter((l) => !generatedSimple.has(l));
|
|
2130
2269
|
const existingFrom = collectFromImports(existingLines);
|
|
2131
2270
|
const generatedFrom = collectFromImports(resultLines);
|
|
2271
|
+
const allGeneratedIds = /* @__PURE__ */ new Set();
|
|
2272
|
+
for (const ids of generatedFrom.values()) for (const id of ids) allGeneratedIds.add(id);
|
|
2132
2273
|
const extraFromLines = [];
|
|
2133
2274
|
for (const [mod, existIds] of existingFrom) {
|
|
2134
2275
|
const genIds = generatedFrom.get(mod);
|
|
2135
|
-
if (!genIds)
|
|
2136
|
-
|
|
2137
|
-
|
|
2276
|
+
if (!genIds) {
|
|
2277
|
+
const extra = [...existIds].filter((id) => !allGeneratedIds.has(id));
|
|
2278
|
+
if (extra.length > 0) extraFromLines.push(`from ${mod} import ${extra.join(", ")}`);
|
|
2279
|
+
} else {
|
|
2280
|
+
const extra = [...existIds].filter((id) => !genIds.has(id) && !allGeneratedIds.has(id));
|
|
2138
2281
|
if (extra.length > 0) extraFromLines.push(`from ${mod} import ${extra.join(", ")}`);
|
|
2139
2282
|
}
|
|
2140
2283
|
}
|
|
@@ -2161,7 +2304,9 @@ function buildEmitterContext(spec, options) {
|
|
|
2161
2304
|
outputDir: options.outputDir,
|
|
2162
2305
|
apiSurface: options.apiSurface,
|
|
2163
2306
|
overlayLookup: options.overlayLookup,
|
|
2164
|
-
resolvedOperations: resolveOperations(spec, options.operationHints, options.mountRules)
|
|
2307
|
+
resolvedOperations: resolveOperations(spec, options.operationHints, options.mountRules),
|
|
2308
|
+
targetDir: options.target,
|
|
2309
|
+
priorTargetManifestPaths: options.priorTargetManifestPaths
|
|
2165
2310
|
};
|
|
2166
2311
|
}
|
|
2167
2312
|
/**
|
|
@@ -2230,7 +2375,6 @@ function generateAllFiles(spec, emitter, ctx) {
|
|
|
2230
2375
|
...emitter.generateResources(spec.services, ctx),
|
|
2231
2376
|
...emitter.generateClient(spec, ctx),
|
|
2232
2377
|
...emitter.generateErrors(ctx),
|
|
2233
|
-
...emitter.generateConfig(ctx),
|
|
2234
2378
|
...emitter.generateTypeSignatures?.(reachableSpec, ctx) ?? [],
|
|
2235
2379
|
...emitter.generateTests(reachableSpec, ctx),
|
|
2236
2380
|
...emitter.generateManifest?.(spec, ctx) ?? []
|
|
@@ -2281,7 +2425,7 @@ function resolveImportPath(fromFilePath, importModulePath, knownPaths) {
|
|
|
2281
2425
|
*/
|
|
2282
2426
|
function isRootFile(filePath) {
|
|
2283
2427
|
if (filePath.includes("/fixtures/")) return false;
|
|
2284
|
-
if (filePath.endsWith(".spec.ts") || filePath.endsWith(".test.ts")) return
|
|
2428
|
+
if (filePath.endsWith(".spec.ts") || filePath.endsWith(".test.ts")) return true;
|
|
2285
2429
|
if (filePath.includes("/serializers/")) return false;
|
|
2286
2430
|
if (filePath.includes("/interfaces/") && !filePath.endsWith("/index.ts")) return false;
|
|
2287
2431
|
return true;
|
|
@@ -2418,7 +2562,10 @@ async function integrateGeneratedFiles(opts) {
|
|
|
2418
2562
|
for (const filePath of removals) try {
|
|
2419
2563
|
await fs.unlink(filePath);
|
|
2420
2564
|
} catch {}
|
|
2421
|
-
return
|
|
2565
|
+
return {
|
|
2566
|
+
...result,
|
|
2567
|
+
emittedPaths: resolved.map((f) => f.path)
|
|
2568
|
+
};
|
|
2422
2569
|
}
|
|
2423
2570
|
//#endregion
|
|
2424
2571
|
//#region src/engine/formatter.ts
|
|
@@ -2453,9 +2600,141 @@ async function formatTargetFiles(emitter, targetDir, filePaths) {
|
|
|
2453
2600
|
}
|
|
2454
2601
|
}
|
|
2455
2602
|
//#endregion
|
|
2603
|
+
//#region src/engine/manifest.ts
|
|
2604
|
+
/**
|
|
2605
|
+
* Prune-manifest support for `oagen generate`.
|
|
2606
|
+
*
|
|
2607
|
+
* The manifest records every auto-generated file emitted on the previous run,
|
|
2608
|
+
* relative to the directory it lives in (either the `--output` standalone dir
|
|
2609
|
+
* or the `--target` integration dir). On the next run we diff the previous
|
|
2610
|
+
* list against the current emission set and delete anything that's no longer
|
|
2611
|
+
* produced — preventing stale-file accumulation across regens.
|
|
2612
|
+
*
|
|
2613
|
+
* Design notes:
|
|
2614
|
+
* - The manifest is versioned so future format changes can be handled safely.
|
|
2615
|
+
* - Paths are stored sorted for deterministic diffs in git.
|
|
2616
|
+
* - Deletion is gated on a header guard when one is available: we only remove
|
|
2617
|
+
* files whose contents start with the auto-generated header. This protects
|
|
2618
|
+
* a hand-maintained file that happens to share a name with a previously
|
|
2619
|
+
* generated one (extremely rare, but worth catching).
|
|
2620
|
+
* - On first adoption (no previous manifest), pruning is skipped and a
|
|
2621
|
+
* one-line notice is printed — no surprise deletions.
|
|
2622
|
+
*/
|
|
2623
|
+
const MANIFEST_FILENAME = ".oagen-manifest.json";
|
|
2624
|
+
const MANIFEST_VERSION = 1;
|
|
2625
|
+
/** Read `.oagen-manifest.json` from a directory. Returns null if absent or malformed. */
|
|
2626
|
+
async function readManifest(dir) {
|
|
2627
|
+
const manifestPath = path$1.join(dir, MANIFEST_FILENAME);
|
|
2628
|
+
let raw;
|
|
2629
|
+
try {
|
|
2630
|
+
raw = await fs.readFile(manifestPath, "utf-8");
|
|
2631
|
+
} catch {
|
|
2632
|
+
return null;
|
|
2633
|
+
}
|
|
2634
|
+
try {
|
|
2635
|
+
const parsed = JSON.parse(raw);
|
|
2636
|
+
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;
|
|
2637
|
+
if (parsed.version > MANIFEST_VERSION) {
|
|
2638
|
+
console.warn(`[oagen] ${MANIFEST_FILENAME} schema version ${parsed.version} is newer than supported (${MANIFEST_VERSION}); ignoring for pruning.`);
|
|
2639
|
+
return null;
|
|
2640
|
+
}
|
|
2641
|
+
return parsed;
|
|
2642
|
+
} catch {
|
|
2643
|
+
return null;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
/** Write `.oagen-manifest.json` to a directory with sorted paths. */
|
|
2647
|
+
async function writeManifest(dir, opts) {
|
|
2648
|
+
const manifest = {
|
|
2649
|
+
version: MANIFEST_VERSION,
|
|
2650
|
+
language: opts.language,
|
|
2651
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2652
|
+
files: [...new Set(opts.files)].sort()
|
|
2653
|
+
};
|
|
2654
|
+
const manifestPath = path$1.join(dir, MANIFEST_FILENAME);
|
|
2655
|
+
await fs.mkdir(dir, { recursive: true });
|
|
2656
|
+
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
2657
|
+
}
|
|
2658
|
+
/** Return the set of previous paths no longer present in the current emission. */
|
|
2659
|
+
function computeStalePaths(prev, currentPaths) {
|
|
2660
|
+
const current = new Set(currentPaths);
|
|
2661
|
+
return prev.files.filter((p) => !current.has(p)).sort();
|
|
2662
|
+
}
|
|
2663
|
+
/**
|
|
2664
|
+
* Delete stale files from `dir`.
|
|
2665
|
+
*
|
|
2666
|
+
* When `header` is provided, a file is only deleted if its contents start with
|
|
2667
|
+
* that header — so hand-maintained files that somehow collide with a previously
|
|
2668
|
+
* generated path can't be clobbered. When `header` is omitted, the guard is
|
|
2669
|
+
* skipped (useful for non-source artifacts like fixture JSON).
|
|
2670
|
+
*
|
|
2671
|
+
* Returns lists for reporting. Empty parent directories are removed after
|
|
2672
|
+
* deletion, up to (but not including) `dir` itself.
|
|
2673
|
+
*/
|
|
2674
|
+
async function pruneStaleFiles(dir, paths, opts = {}) {
|
|
2675
|
+
const pruned = [];
|
|
2676
|
+
const preserved = [];
|
|
2677
|
+
const missing = [];
|
|
2678
|
+
for (const relPath of paths) {
|
|
2679
|
+
const fullPath = path$1.join(dir, relPath);
|
|
2680
|
+
let content;
|
|
2681
|
+
try {
|
|
2682
|
+
content = await fs.readFile(fullPath, "utf-8");
|
|
2683
|
+
} catch {
|
|
2684
|
+
missing.push(relPath);
|
|
2685
|
+
continue;
|
|
2686
|
+
}
|
|
2687
|
+
if (opts.header && !content.startsWith(opts.header)) {
|
|
2688
|
+
preserved.push(relPath);
|
|
2689
|
+
continue;
|
|
2690
|
+
}
|
|
2691
|
+
try {
|
|
2692
|
+
await fs.unlink(fullPath);
|
|
2693
|
+
pruned.push(relPath);
|
|
2694
|
+
} catch {
|
|
2695
|
+
missing.push(relPath);
|
|
2696
|
+
continue;
|
|
2697
|
+
}
|
|
2698
|
+
await removeEmptyParents(path$1.dirname(fullPath), dir);
|
|
2699
|
+
}
|
|
2700
|
+
return {
|
|
2701
|
+
pruned,
|
|
2702
|
+
preserved,
|
|
2703
|
+
missing
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
/**
|
|
2707
|
+
* Walk up from `startDir` removing empty directories until we hit `stopDir`
|
|
2708
|
+
* (exclusive) or a non-empty directory. Silently ignores errors.
|
|
2709
|
+
*/
|
|
2710
|
+
async function removeEmptyParents(startDir, stopDir) {
|
|
2711
|
+
let current = path$1.resolve(startDir);
|
|
2712
|
+
const stop = path$1.resolve(stopDir);
|
|
2713
|
+
while (current.startsWith(stop + path$1.sep) && current !== stop) {
|
|
2714
|
+
let entries;
|
|
2715
|
+
try {
|
|
2716
|
+
entries = await fs.readdir(current);
|
|
2717
|
+
} catch {
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
if (entries.length > 0) return;
|
|
2721
|
+
try {
|
|
2722
|
+
await fs.rmdir(current);
|
|
2723
|
+
} catch {
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
current = path$1.dirname(current);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
//#endregion
|
|
2456
2730
|
//#region src/engine/orchestrator.ts
|
|
2457
2731
|
async function generate(spec, emitter, options) {
|
|
2458
|
-
const
|
|
2732
|
+
const targetManifestForCtx = options.target && !options.noPrune ? await readManifest(options.target) : null;
|
|
2733
|
+
const priorTargetManifestPaths = targetManifestForCtx ? new Set(targetManifestForCtx.files) : void 0;
|
|
2734
|
+
const { files: withHeaders, header } = generateFiles(spec, emitter, {
|
|
2735
|
+
...options,
|
|
2736
|
+
priorTargetManifestPaths
|
|
2737
|
+
});
|
|
2459
2738
|
if (options.dryRun) {
|
|
2460
2739
|
if (options.target) {
|
|
2461
2740
|
console.log(`\nTarget integration (${options.target}):`);
|
|
@@ -2463,13 +2742,25 @@ async function generate(spec, emitter, options) {
|
|
|
2463
2742
|
}
|
|
2464
2743
|
return withHeaders;
|
|
2465
2744
|
}
|
|
2745
|
+
const outputPrevManifest = options.noPrune ? null : await readManifest(options.outputDir);
|
|
2466
2746
|
const writeResult = await writeFiles(withHeaders, options.outputDir, {
|
|
2467
2747
|
language: emitter.language,
|
|
2468
2748
|
header
|
|
2469
2749
|
});
|
|
2470
2750
|
if (writeResult.merged.length > 0) console.log(`Merged into ${writeResult.merged.length} existing files (additive only)`);
|
|
2471
2751
|
if (writeResult.ignored.length > 0) console.log(`Ignored ${writeResult.ignored.length} files (@oagen-ignore-file)`);
|
|
2752
|
+
const outputEmittedPaths = withHeaders.map((f) => f.path);
|
|
2753
|
+
await applyManifestPrune({
|
|
2754
|
+
dir: options.outputDir,
|
|
2755
|
+
label: "Output",
|
|
2756
|
+
prevManifest: outputPrevManifest,
|
|
2757
|
+
currentPaths: outputEmittedPaths,
|
|
2758
|
+
language: emitter.language,
|
|
2759
|
+
header,
|
|
2760
|
+
noPrune: options.noPrune
|
|
2761
|
+
});
|
|
2472
2762
|
if (options.target) {
|
|
2763
|
+
const targetPrevManifest = targetManifestForCtx;
|
|
2473
2764
|
const targetResult = await integrateGeneratedFiles({
|
|
2474
2765
|
files: withHeaders,
|
|
2475
2766
|
language: emitter.language,
|
|
@@ -2479,6 +2770,15 @@ async function generate(spec, emitter, options) {
|
|
|
2479
2770
|
if (targetResult.written.length > 0) console.log(`Target: created ${targetResult.written.length} new files`);
|
|
2480
2771
|
if (targetResult.merged.length > 0) console.log(`Target: merged into ${targetResult.merged.length} existing files (additive only)`);
|
|
2481
2772
|
if (targetResult.skipped.length > 0) console.log(`Target: skipped ${targetResult.skipped.length} files (excluded or no grammar)`);
|
|
2773
|
+
await applyManifestPrune({
|
|
2774
|
+
dir: options.target,
|
|
2775
|
+
label: "Target",
|
|
2776
|
+
prevManifest: targetPrevManifest,
|
|
2777
|
+
currentPaths: targetResult.emittedPaths,
|
|
2778
|
+
language: emitter.language,
|
|
2779
|
+
header,
|
|
2780
|
+
noPrune: options.noPrune
|
|
2781
|
+
});
|
|
2482
2782
|
const allTargetFiles = [
|
|
2483
2783
|
...targetResult.written,
|
|
2484
2784
|
...targetResult.merged,
|
|
@@ -2488,6 +2788,40 @@ async function generate(spec, emitter, options) {
|
|
|
2488
2788
|
}
|
|
2489
2789
|
return withHeaders;
|
|
2490
2790
|
}
|
|
2791
|
+
/**
|
|
2792
|
+
* Prune files recorded in the previous manifest but absent from the current emission,
|
|
2793
|
+
* then write the fresh manifest. Safe on first adoption (no previous manifest).
|
|
2794
|
+
*/
|
|
2795
|
+
async function applyManifestPrune(opts) {
|
|
2796
|
+
if (opts.noPrune) {
|
|
2797
|
+
await writeManifest(opts.dir, {
|
|
2798
|
+
language: opts.language,
|
|
2799
|
+
files: opts.currentPaths
|
|
2800
|
+
});
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2803
|
+
if (!opts.prevManifest) {
|
|
2804
|
+
console.log(`${opts.label}: no ${MANIFEST_FILENAME} found — skipping prune (this baseline manifest will be written now).`);
|
|
2805
|
+
await writeManifest(opts.dir, {
|
|
2806
|
+
language: opts.language,
|
|
2807
|
+
files: opts.currentPaths
|
|
2808
|
+
});
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2811
|
+
const stale = computeStalePaths(opts.prevManifest, opts.currentPaths);
|
|
2812
|
+
if (stale.length > 0) {
|
|
2813
|
+
const { pruned, preserved } = await pruneStaleFiles(opts.dir, stale, { header: opts.header });
|
|
2814
|
+
if (pruned.length > 0) console.log(`${opts.label}: pruned ${pruned.length} stale file${pruned.length === 1 ? "" : "s"}`);
|
|
2815
|
+
if (preserved.length > 0) {
|
|
2816
|
+
console.log(`${opts.label}: preserved ${preserved.length} file${preserved.length === 1 ? "" : "s"} in previous manifest but lacking the auto-generated header (possibly hand-edited).`);
|
|
2817
|
+
for (const p of preserved) console.log(` ${p}`);
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
await writeManifest(opts.dir, {
|
|
2821
|
+
language: opts.language,
|
|
2822
|
+
files: opts.currentPaths
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2491
2825
|
//#endregion
|
|
2492
2826
|
//#region src/engine/registry.ts
|
|
2493
2827
|
const emitters = /* @__PURE__ */ new Map();
|
|
@@ -2848,4 +3182,4 @@ function summarize(changes) {
|
|
|
2848
3182
|
//#endregion
|
|
2849
3183
|
export { integrateGeneratedFiles as a, applyFileHeaders as c, generateAllFiles as d, generateFiles as f, generate as i, buildEmitterContext as l, resolveOperations as m, getEmitter as n, mapFilesForTargetIntegration as o, deriveMethodName as p, registerEmitter as r, treeShakeFiles as s, diffSpecs as t, collectReferencedNames as u };
|
|
2850
3184
|
|
|
2851
|
-
//# sourceMappingURL=diff-
|
|
3185
|
+
//# sourceMappingURL=diff-CltJMcwG.mjs.map
|