@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
|
@@ -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({
|
|
@@ -130,7 +130,8 @@ function resolveOperations(spec, hints, mountRules) {
|
|
|
130
130
|
mountOn,
|
|
131
131
|
wrappers,
|
|
132
132
|
defaults: hint?.defaults ?? {},
|
|
133
|
-
inferFromClient: hint?.inferFromClient ?? []
|
|
133
|
+
inferFromClient: hint?.inferFromClient ?? [],
|
|
134
|
+
urlBuilder: hint?.urlBuilder ?? false
|
|
134
135
|
});
|
|
135
136
|
}
|
|
136
137
|
return resolved;
|
|
@@ -675,6 +676,41 @@ function findPrecedingKdoc(children, index, source) {
|
|
|
675
676
|
}
|
|
676
677
|
return null;
|
|
677
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
|
+
}
|
|
678
714
|
const kotlinMergeAdapter = {
|
|
679
715
|
language: "kotlin",
|
|
680
716
|
grammarModule: "tree-sitter-kotlin",
|
|
@@ -715,6 +751,33 @@ const kotlinMergeAdapter = {
|
|
|
715
751
|
renderImports(imports) {
|
|
716
752
|
return imports.map((entry) => entry.text);
|
|
717
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
|
+
},
|
|
718
781
|
extractDocstrings(tree, source) {
|
|
719
782
|
const result = /* @__PURE__ */ new Map();
|
|
720
783
|
const rootChildren = tree.rootNode.children;
|
|
@@ -897,6 +960,7 @@ const nodeMergeAdapter = {
|
|
|
897
960
|
const newText = newMembers.map((m) => m.text).join("\n");
|
|
898
961
|
for (const match of newText.matchAll(/this\.(\w+)/g)) {
|
|
899
962
|
const propName = match[1];
|
|
963
|
+
if (propName === "workos") continue;
|
|
900
964
|
if (propName && !existingMemberKeys.has(propName)) return true;
|
|
901
965
|
}
|
|
902
966
|
return false;
|
|
@@ -1546,6 +1610,40 @@ function buildIgnoredSymbolNames(docstrings, regions) {
|
|
|
1546
1610
|
for (const [name, info] of docstrings) if (regions.some((r) => info.declStartIndex >= r.startIndex && info.declStartIndex <= r.endIndex)) ignored.add(name);
|
|
1547
1611
|
return ignored;
|
|
1548
1612
|
}
|
|
1613
|
+
function clampIndex(value, contentLength) {
|
|
1614
|
+
if (!Number.isFinite(value)) return 0;
|
|
1615
|
+
if (value < 0) return 0;
|
|
1616
|
+
if (value > contentLength) return contentLength;
|
|
1617
|
+
return value;
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Resolve the declaration line start for insertion using the current content,
|
|
1621
|
+
* instead of relying on parser column arithmetic. This avoids misplaced inserts
|
|
1622
|
+
* when parser columns/offsets drift.
|
|
1623
|
+
*/
|
|
1624
|
+
function declarationLineStart(content, declStartIndex) {
|
|
1625
|
+
const idx = clampIndex(declStartIndex, content.length);
|
|
1626
|
+
const prevNewline = content.lastIndexOf("\n", Math.max(0, idx - 1));
|
|
1627
|
+
return prevNewline === -1 ? 0 : prevNewline + 1;
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Resolve a docstring range by searching for the nearest matching text before
|
|
1631
|
+
* the declaration start. Falls back to parser-provided offsets when search fails.
|
|
1632
|
+
*/
|
|
1633
|
+
function resolveDocRange(content, docText, declStartIndex, fallbackStart, fallbackEnd) {
|
|
1634
|
+
const safeDecl = clampIndex(declStartIndex, content.length);
|
|
1635
|
+
const safeFallbackStart = clampIndex(fallbackStart, content.length);
|
|
1636
|
+
const safeFallbackEnd = clampIndex(fallbackEnd, content.length);
|
|
1637
|
+
const found = content.lastIndexOf(docText, safeDecl);
|
|
1638
|
+
if (found !== -1) return {
|
|
1639
|
+
start: found,
|
|
1640
|
+
end: found + docText.length
|
|
1641
|
+
};
|
|
1642
|
+
return {
|
|
1643
|
+
start: safeFallbackStart,
|
|
1644
|
+
end: safeFallbackEnd
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1549
1647
|
/**
|
|
1550
1648
|
* Check if a tree-sitter grammar is configured for the given language.
|
|
1551
1649
|
*/
|
|
@@ -1594,15 +1692,17 @@ async function extractClassEndLines(source, language) {
|
|
|
1594
1692
|
if (source[body ? body.endIndex - 1 : node.endIndex - 1] === "}") map.set(name, { bodyEndLine: endRow });
|
|
1595
1693
|
else map.set(name, { bodyEndLine: endRow + 1 });
|
|
1596
1694
|
}
|
|
1597
|
-
|
|
1598
|
-
if (
|
|
1599
|
-
if (
|
|
1600
|
-
for (const inner of
|
|
1695
|
+
function walk(node) {
|
|
1696
|
+
if (node.type === "class" || node.type === "class_definition" || node.type === "class_declaration") processClassNode(node);
|
|
1697
|
+
if (node.type === "decorated_definition") {
|
|
1698
|
+
for (const inner of node.namedChildren) if (inner.type === "class_definition") processClassNode(inner);
|
|
1601
1699
|
}
|
|
1602
|
-
if (
|
|
1603
|
-
for (const inner of
|
|
1700
|
+
if (node.type === "export_statement") {
|
|
1701
|
+
for (const inner of node.namedChildren) if (inner.type === "class_declaration") processClassNode(inner);
|
|
1604
1702
|
}
|
|
1703
|
+
for (const child of node.namedChildren) walk(child);
|
|
1605
1704
|
}
|
|
1705
|
+
for (const child of tree.rootNode.children) walk(child);
|
|
1606
1706
|
return map;
|
|
1607
1707
|
}
|
|
1608
1708
|
/**
|
|
@@ -1757,10 +1857,10 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
1757
1857
|
if (newMembers.length > 0) {
|
|
1758
1858
|
if (adapter.shouldSkipDeepMerge?.(symbolName, existingMemberKeys, newMembers)) continue;
|
|
1759
1859
|
const indent = existSymbol.memberIndent ?? " ";
|
|
1760
|
-
const insertText = newMembers.map((m) => indent + m.text).join("\n");
|
|
1860
|
+
const insertText = newMembers.map((m) => indent + m.text).join("\n\n");
|
|
1761
1861
|
insertions.push({
|
|
1762
1862
|
line: existSymbol.bodyEndLine,
|
|
1763
|
-
text: insertText
|
|
1863
|
+
text: "\n" + insertText
|
|
1764
1864
|
});
|
|
1765
1865
|
deepAdded += newMembers.length;
|
|
1766
1866
|
}
|
|
@@ -1822,15 +1922,16 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
1822
1922
|
if (genDoc) if (existInfo.docstring) {
|
|
1823
1923
|
if (!existInfo.docstring.text.includes("@oagen-ignore") && existInfo.docstring.text !== genDoc.text) {
|
|
1824
1924
|
const newText = preserveDeprecatedTags(existInfo.docstring.text, genDoc.text);
|
|
1925
|
+
const range = resolveDocRange(result, existInfo.docstring.text, existInfo.declStartIndex, existInfo.docstring.startIndex, existInfo.docstring.endIndex);
|
|
1825
1926
|
edits.push({
|
|
1826
|
-
start:
|
|
1827
|
-
end:
|
|
1927
|
+
start: range.start,
|
|
1928
|
+
end: range.end,
|
|
1828
1929
|
newText
|
|
1829
1930
|
});
|
|
1830
1931
|
docstringUpdates++;
|
|
1831
1932
|
}
|
|
1832
1933
|
} else {
|
|
1833
|
-
const lineStart = existInfo.declStartIndex
|
|
1934
|
+
const lineStart = declarationLineStart(result, existInfo.declStartIndex);
|
|
1834
1935
|
const indent = " ".repeat(existInfo.declColumn);
|
|
1835
1936
|
edits.push({
|
|
1836
1937
|
start: lineStart,
|
|
@@ -1848,15 +1949,16 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
1848
1949
|
if (existMember.docstring) {
|
|
1849
1950
|
if (!existMember.docstring.text.includes("@oagen-ignore") && existMember.docstring.text !== genMember.docstring.text) {
|
|
1850
1951
|
const newText = preserveDeprecatedTags(existMember.docstring.text, genMember.docstring.text);
|
|
1952
|
+
const range = resolveDocRange(result, existMember.docstring.text, existMember.declStartIndex, existMember.docstring.startIndex, existMember.docstring.endIndex);
|
|
1851
1953
|
edits.push({
|
|
1852
|
-
start:
|
|
1853
|
-
end:
|
|
1954
|
+
start: range.start,
|
|
1955
|
+
end: range.end,
|
|
1854
1956
|
newText
|
|
1855
1957
|
});
|
|
1856
1958
|
docstringUpdates++;
|
|
1857
1959
|
}
|
|
1858
1960
|
} else {
|
|
1859
|
-
const lineStart = existMember.declStartIndex
|
|
1961
|
+
const lineStart = declarationLineStart(result, existMember.declStartIndex);
|
|
1860
1962
|
const indent = " ".repeat(existMember.declColumn);
|
|
1861
1963
|
edits.push({
|
|
1862
1964
|
start: lineStart,
|
|
@@ -1901,15 +2003,16 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
1901
2003
|
if (existMember.docstring.text.includes("@oagen-ignore")) continue;
|
|
1902
2004
|
if (existMember.docstring.text !== genMember.docstring.text) {
|
|
1903
2005
|
const newText = preserveDeprecatedTags(existMember.docstring.text, genMember.docstring.text);
|
|
2006
|
+
const range = resolveDocRange(result, existMember.docstring.text, existMember.declStartIndex, existMember.docstring.startIndex, existMember.docstring.endIndex);
|
|
1904
2007
|
edits.push({
|
|
1905
|
-
start:
|
|
1906
|
-
end:
|
|
2008
|
+
start: range.start,
|
|
2009
|
+
end: range.end,
|
|
1907
2010
|
newText
|
|
1908
2011
|
});
|
|
1909
2012
|
docstringUpdates++;
|
|
1910
2013
|
}
|
|
1911
2014
|
} else {
|
|
1912
|
-
const lineStart = existMember.declStartIndex
|
|
2015
|
+
const lineStart = declarationLineStart(result, existMember.declStartIndex);
|
|
1913
2016
|
const indent = " ".repeat(existMember.declColumn);
|
|
1914
2017
|
edits.push({
|
|
1915
2018
|
start: lineStart,
|
|
@@ -2082,7 +2185,7 @@ async function overwriteWithPreservedRegions(existingContent, generatedContent,
|
|
|
2082
2185
|
});
|
|
2083
2186
|
}
|
|
2084
2187
|
if (blocks.length === 0) return generatedContent;
|
|
2085
|
-
const result =
|
|
2188
|
+
const result = stripIgnoreRegions(generatedLines);
|
|
2086
2189
|
spliceExtraImports(existingLines, result);
|
|
2087
2190
|
const classEndLines = await extractClassEndLines(result.join("\n"), language);
|
|
2088
2191
|
const blocksByClass = /* @__PURE__ */ new Map();
|
|
@@ -2149,10 +2252,33 @@ async function overwriteWithPreservedRegions(existingContent, generatedContent,
|
|
|
2149
2252
|
for (const ins of insertions) result.splice(ins.line, 0, ...ins.content);
|
|
2150
2253
|
return result.join("\n");
|
|
2151
2254
|
}
|
|
2255
|
+
/**
|
|
2256
|
+
* Remove every `@oagen-ignore-start`..`@oagen-ignore-end` region from the
|
|
2257
|
+
* supplied lines, collapsing stretches of blank lines that get left behind.
|
|
2258
|
+
* Used to drop generator-emitted seed blocks before splicing the on-disk
|
|
2259
|
+
* versions back in — otherwise every regen doubles up.
|
|
2260
|
+
*/
|
|
2261
|
+
function stripIgnoreRegions(lines) {
|
|
2262
|
+
const out = [];
|
|
2263
|
+
let skipping = false;
|
|
2264
|
+
for (const line of lines) {
|
|
2265
|
+
if (!skipping && line.includes("@oagen-ignore-start")) {
|
|
2266
|
+
skipping = true;
|
|
2267
|
+
while (out.length > 0 && out[out.length - 1].trim() === "") out.pop();
|
|
2268
|
+
continue;
|
|
2269
|
+
}
|
|
2270
|
+
if (skipping) {
|
|
2271
|
+
if (line.includes("@oagen-ignore-end")) skipping = false;
|
|
2272
|
+
continue;
|
|
2273
|
+
}
|
|
2274
|
+
out.push(line);
|
|
2275
|
+
}
|
|
2276
|
+
return out;
|
|
2277
|
+
}
|
|
2152
2278
|
/** Scan backwards from `lineIdx` to find the nearest class declaration name. */
|
|
2153
2279
|
function findContainingClass(lines, lineIdx) {
|
|
2154
2280
|
for (let j = lineIdx - 1; j >= 0; j--) {
|
|
2155
|
-
const m = lines[j].match(
|
|
2281
|
+
const m = lines[j].match(/^\s*(?:export\s+)?class\s+(\w+)/);
|
|
2156
2282
|
if (m) return m[1];
|
|
2157
2283
|
}
|
|
2158
2284
|
return "";
|
|
@@ -2162,7 +2288,7 @@ function findContainingClass(lines, lineIdx) {
|
|
|
2162
2288
|
* by scanning backwards from `beforeLine`.
|
|
2163
2289
|
*/
|
|
2164
2290
|
function findClassDeclarationStart(lines, className, beforeLine) {
|
|
2165
|
-
const classPattern = new RegExp(
|
|
2291
|
+
const classPattern = new RegExp(`^\\s*(?:export\\s+)?class\\s+${className}\\b`);
|
|
2166
2292
|
for (let i = beforeLine - 1; i >= 0; i--) {
|
|
2167
2293
|
if (!classPattern.test(lines[i])) continue;
|
|
2168
2294
|
let start = i;
|
|
@@ -2241,7 +2367,9 @@ function buildEmitterContext(spec, options) {
|
|
|
2241
2367
|
outputDir: options.outputDir,
|
|
2242
2368
|
apiSurface: options.apiSurface,
|
|
2243
2369
|
overlayLookup: options.overlayLookup,
|
|
2244
|
-
resolvedOperations: resolveOperations(spec, options.operationHints, options.mountRules)
|
|
2370
|
+
resolvedOperations: resolveOperations(spec, options.operationHints, options.mountRules),
|
|
2371
|
+
targetDir: options.target,
|
|
2372
|
+
priorTargetManifestPaths: options.priorTargetManifestPaths
|
|
2245
2373
|
};
|
|
2246
2374
|
}
|
|
2247
2375
|
/**
|
|
@@ -2360,7 +2488,7 @@ function resolveImportPath(fromFilePath, importModulePath, knownPaths) {
|
|
|
2360
2488
|
*/
|
|
2361
2489
|
function isRootFile(filePath) {
|
|
2362
2490
|
if (filePath.includes("/fixtures/")) return false;
|
|
2363
|
-
if (filePath.endsWith(".spec.ts") || filePath.endsWith(".test.ts")) return
|
|
2491
|
+
if (filePath.endsWith(".spec.ts") || filePath.endsWith(".test.ts")) return true;
|
|
2364
2492
|
if (filePath.includes("/serializers/")) return false;
|
|
2365
2493
|
if (filePath.includes("/interfaces/") && !filePath.endsWith("/index.ts")) return false;
|
|
2366
2494
|
return true;
|
|
@@ -2489,7 +2617,8 @@ async function resolveFileToDirectoryConflicts(files, targetDir) {
|
|
|
2489
2617
|
};
|
|
2490
2618
|
}
|
|
2491
2619
|
async function integrateGeneratedFiles(opts) {
|
|
2492
|
-
const
|
|
2620
|
+
const mapped = mapFilesForTargetIntegration(opts.files, opts.language);
|
|
2621
|
+
const { files: resolved, removals } = await resolveFileToDirectoryConflicts(await treeShakeFiles(mapped, opts.language), opts.targetDir);
|
|
2493
2622
|
const result = await writeFiles(resolved, opts.targetDir, {
|
|
2494
2623
|
language: opts.language,
|
|
2495
2624
|
header: opts.header
|
|
@@ -2497,7 +2626,10 @@ async function integrateGeneratedFiles(opts) {
|
|
|
2497
2626
|
for (const filePath of removals) try {
|
|
2498
2627
|
await fs.unlink(filePath);
|
|
2499
2628
|
} catch {}
|
|
2500
|
-
return
|
|
2629
|
+
return {
|
|
2630
|
+
...result,
|
|
2631
|
+
emittedPaths: mapped.map((f) => f.path)
|
|
2632
|
+
};
|
|
2501
2633
|
}
|
|
2502
2634
|
//#endregion
|
|
2503
2635
|
//#region src/engine/formatter.ts
|
|
@@ -2520,6 +2652,7 @@ async function formatTargetFiles(emitter, targetDir, filePaths) {
|
|
|
2520
2652
|
if (!formatCmd) return;
|
|
2521
2653
|
const absolutePaths = filePaths.map((f) => path$1.resolve(targetDir, f));
|
|
2522
2654
|
const batchSize = formatCmd.batchSize ?? 100;
|
|
2655
|
+
process.stderr.write(`[oagen] formatting ${absolutePaths.length} file(s) with ${formatCmd.cmd} (batch ${batchSize})\n`);
|
|
2523
2656
|
for (let i = 0; i < absolutePaths.length; i += batchSize) {
|
|
2524
2657
|
const batch = absolutePaths.slice(i, i + batchSize);
|
|
2525
2658
|
try {
|
|
@@ -2528,13 +2661,148 @@ async function formatTargetFiles(emitter, targetDir, filePaths) {
|
|
|
2528
2661
|
maxBuffer: 10 * 1024 * 1024,
|
|
2529
2662
|
timeout: 12e4
|
|
2530
2663
|
});
|
|
2531
|
-
} catch {
|
|
2664
|
+
} catch (err) {
|
|
2665
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2666
|
+
process.stderr.write(`[oagen] formatter batch failed: ${msg}\n`);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
//#endregion
|
|
2671
|
+
//#region src/engine/manifest.ts
|
|
2672
|
+
/**
|
|
2673
|
+
* Prune-manifest support for `oagen generate`.
|
|
2674
|
+
*
|
|
2675
|
+
* The manifest records every auto-generated file emitted on the previous run,
|
|
2676
|
+
* relative to the directory it lives in (either the `--output` standalone dir
|
|
2677
|
+
* or the `--target` integration dir). On the next run we diff the previous
|
|
2678
|
+
* list against the current emission set and delete anything that's no longer
|
|
2679
|
+
* produced — preventing stale-file accumulation across regens.
|
|
2680
|
+
*
|
|
2681
|
+
* Design notes:
|
|
2682
|
+
* - The manifest is versioned so future format changes can be handled safely.
|
|
2683
|
+
* - Paths are stored sorted for deterministic diffs in git.
|
|
2684
|
+
* - Deletion is gated on a header guard when one is available: we only remove
|
|
2685
|
+
* files whose contents start with the auto-generated header. This protects
|
|
2686
|
+
* a hand-maintained file that happens to share a name with a previously
|
|
2687
|
+
* generated one (extremely rare, but worth catching).
|
|
2688
|
+
* - On first adoption (no previous manifest), pruning is skipped and a
|
|
2689
|
+
* one-line notice is printed — no surprise deletions.
|
|
2690
|
+
*/
|
|
2691
|
+
const MANIFEST_FILENAME = ".oagen-manifest.json";
|
|
2692
|
+
const MANIFEST_VERSION = 1;
|
|
2693
|
+
/** Read `.oagen-manifest.json` from a directory. Returns null if absent or malformed. */
|
|
2694
|
+
async function readManifest(dir) {
|
|
2695
|
+
const manifestPath = path$1.join(dir, MANIFEST_FILENAME);
|
|
2696
|
+
let raw;
|
|
2697
|
+
try {
|
|
2698
|
+
raw = await fs.readFile(manifestPath, "utf-8");
|
|
2699
|
+
} catch {
|
|
2700
|
+
return null;
|
|
2701
|
+
}
|
|
2702
|
+
try {
|
|
2703
|
+
const parsed = JSON.parse(raw);
|
|
2704
|
+
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;
|
|
2705
|
+
if (parsed.version > MANIFEST_VERSION) {
|
|
2706
|
+
console.warn(`[oagen] ${MANIFEST_FILENAME} schema version ${parsed.version} is newer than supported (${MANIFEST_VERSION}); ignoring for pruning.`);
|
|
2707
|
+
return null;
|
|
2708
|
+
}
|
|
2709
|
+
return parsed;
|
|
2710
|
+
} catch {
|
|
2711
|
+
return null;
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
/** Write `.oagen-manifest.json` to a directory with sorted paths. */
|
|
2715
|
+
async function writeManifest(dir, opts) {
|
|
2716
|
+
const manifest = {
|
|
2717
|
+
version: MANIFEST_VERSION,
|
|
2718
|
+
language: opts.language,
|
|
2719
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2720
|
+
files: [...new Set(opts.files)].sort()
|
|
2721
|
+
};
|
|
2722
|
+
const manifestPath = path$1.join(dir, MANIFEST_FILENAME);
|
|
2723
|
+
await fs.mkdir(dir, { recursive: true });
|
|
2724
|
+
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
2725
|
+
}
|
|
2726
|
+
/** Return the set of previous paths no longer present in the current emission. */
|
|
2727
|
+
function computeStalePaths(prev, currentPaths) {
|
|
2728
|
+
const current = new Set(currentPaths);
|
|
2729
|
+
return prev.files.filter((p) => !current.has(p)).sort();
|
|
2730
|
+
}
|
|
2731
|
+
/**
|
|
2732
|
+
* Delete stale files from `dir`.
|
|
2733
|
+
*
|
|
2734
|
+
* When `header` is provided, a file is only deleted if its contents start with
|
|
2735
|
+
* that header — so hand-maintained files that somehow collide with a previously
|
|
2736
|
+
* generated path can't be clobbered. When `header` is omitted, the guard is
|
|
2737
|
+
* skipped (useful for non-source artifacts like fixture JSON).
|
|
2738
|
+
*
|
|
2739
|
+
* Returns lists for reporting. Empty parent directories are removed after
|
|
2740
|
+
* deletion, up to (but not including) `dir` itself.
|
|
2741
|
+
*/
|
|
2742
|
+
async function pruneStaleFiles(dir, paths, opts = {}) {
|
|
2743
|
+
const pruned = [];
|
|
2744
|
+
const preserved = [];
|
|
2745
|
+
const missing = [];
|
|
2746
|
+
for (const relPath of paths) {
|
|
2747
|
+
const fullPath = path$1.join(dir, relPath);
|
|
2748
|
+
let content;
|
|
2749
|
+
try {
|
|
2750
|
+
content = await fs.readFile(fullPath, "utf-8");
|
|
2751
|
+
} catch {
|
|
2752
|
+
missing.push(relPath);
|
|
2753
|
+
continue;
|
|
2754
|
+
}
|
|
2755
|
+
if (opts.header && !content.startsWith(opts.header)) {
|
|
2756
|
+
preserved.push(relPath);
|
|
2757
|
+
continue;
|
|
2758
|
+
}
|
|
2759
|
+
try {
|
|
2760
|
+
await fs.unlink(fullPath);
|
|
2761
|
+
pruned.push(relPath);
|
|
2762
|
+
} catch {
|
|
2763
|
+
missing.push(relPath);
|
|
2764
|
+
continue;
|
|
2765
|
+
}
|
|
2766
|
+
await removeEmptyParents(path$1.dirname(fullPath), dir);
|
|
2767
|
+
}
|
|
2768
|
+
return {
|
|
2769
|
+
pruned,
|
|
2770
|
+
preserved,
|
|
2771
|
+
missing
|
|
2772
|
+
};
|
|
2773
|
+
}
|
|
2774
|
+
/**
|
|
2775
|
+
* Walk up from `startDir` removing empty directories until we hit `stopDir`
|
|
2776
|
+
* (exclusive) or a non-empty directory. Silently ignores errors.
|
|
2777
|
+
*/
|
|
2778
|
+
async function removeEmptyParents(startDir, stopDir) {
|
|
2779
|
+
let current = path$1.resolve(startDir);
|
|
2780
|
+
const stop = path$1.resolve(stopDir);
|
|
2781
|
+
while (current.startsWith(stop + path$1.sep) && current !== stop) {
|
|
2782
|
+
let entries;
|
|
2783
|
+
try {
|
|
2784
|
+
entries = await fs.readdir(current);
|
|
2785
|
+
} catch {
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
2788
|
+
if (entries.length > 0) return;
|
|
2789
|
+
try {
|
|
2790
|
+
await fs.rmdir(current);
|
|
2791
|
+
} catch {
|
|
2792
|
+
return;
|
|
2793
|
+
}
|
|
2794
|
+
current = path$1.dirname(current);
|
|
2532
2795
|
}
|
|
2533
2796
|
}
|
|
2534
2797
|
//#endregion
|
|
2535
2798
|
//#region src/engine/orchestrator.ts
|
|
2536
2799
|
async function generate(spec, emitter, options) {
|
|
2537
|
-
const
|
|
2800
|
+
const targetManifestForCtx = options.target && !options.noPrune ? await readManifest(options.target) : null;
|
|
2801
|
+
const priorTargetManifestPaths = targetManifestForCtx ? new Set(targetManifestForCtx.files) : void 0;
|
|
2802
|
+
const { files: withHeaders, header } = generateFiles(spec, emitter, {
|
|
2803
|
+
...options,
|
|
2804
|
+
priorTargetManifestPaths
|
|
2805
|
+
});
|
|
2538
2806
|
if (options.dryRun) {
|
|
2539
2807
|
if (options.target) {
|
|
2540
2808
|
console.log(`\nTarget integration (${options.target}):`);
|
|
@@ -2542,13 +2810,25 @@ async function generate(spec, emitter, options) {
|
|
|
2542
2810
|
}
|
|
2543
2811
|
return withHeaders;
|
|
2544
2812
|
}
|
|
2813
|
+
const outputPrevManifest = options.noPrune ? null : await readManifest(options.outputDir);
|
|
2545
2814
|
const writeResult = await writeFiles(withHeaders, options.outputDir, {
|
|
2546
2815
|
language: emitter.language,
|
|
2547
2816
|
header
|
|
2548
2817
|
});
|
|
2549
2818
|
if (writeResult.merged.length > 0) console.log(`Merged into ${writeResult.merged.length} existing files (additive only)`);
|
|
2550
2819
|
if (writeResult.ignored.length > 0) console.log(`Ignored ${writeResult.ignored.length} files (@oagen-ignore-file)`);
|
|
2820
|
+
const outputEmittedPaths = withHeaders.map((f) => f.path);
|
|
2821
|
+
await applyManifestPrune({
|
|
2822
|
+
dir: options.outputDir,
|
|
2823
|
+
label: "Output",
|
|
2824
|
+
prevManifest: outputPrevManifest,
|
|
2825
|
+
currentPaths: outputEmittedPaths,
|
|
2826
|
+
language: emitter.language,
|
|
2827
|
+
header,
|
|
2828
|
+
noPrune: options.noPrune
|
|
2829
|
+
});
|
|
2551
2830
|
if (options.target) {
|
|
2831
|
+
const targetPrevManifest = targetManifestForCtx;
|
|
2552
2832
|
const targetResult = await integrateGeneratedFiles({
|
|
2553
2833
|
files: withHeaders,
|
|
2554
2834
|
language: emitter.language,
|
|
@@ -2558,6 +2838,15 @@ async function generate(spec, emitter, options) {
|
|
|
2558
2838
|
if (targetResult.written.length > 0) console.log(`Target: created ${targetResult.written.length} new files`);
|
|
2559
2839
|
if (targetResult.merged.length > 0) console.log(`Target: merged into ${targetResult.merged.length} existing files (additive only)`);
|
|
2560
2840
|
if (targetResult.skipped.length > 0) console.log(`Target: skipped ${targetResult.skipped.length} files (excluded or no grammar)`);
|
|
2841
|
+
await applyManifestPrune({
|
|
2842
|
+
dir: options.target,
|
|
2843
|
+
label: "Target",
|
|
2844
|
+
prevManifest: targetPrevManifest,
|
|
2845
|
+
currentPaths: targetResult.emittedPaths,
|
|
2846
|
+
language: emitter.language,
|
|
2847
|
+
header,
|
|
2848
|
+
noPrune: options.noPrune
|
|
2849
|
+
});
|
|
2561
2850
|
const allTargetFiles = [
|
|
2562
2851
|
...targetResult.written,
|
|
2563
2852
|
...targetResult.merged,
|
|
@@ -2567,6 +2856,40 @@ async function generate(spec, emitter, options) {
|
|
|
2567
2856
|
}
|
|
2568
2857
|
return withHeaders;
|
|
2569
2858
|
}
|
|
2859
|
+
/**
|
|
2860
|
+
* Prune files recorded in the previous manifest but absent from the current emission,
|
|
2861
|
+
* then write the fresh manifest. Safe on first adoption (no previous manifest).
|
|
2862
|
+
*/
|
|
2863
|
+
async function applyManifestPrune(opts) {
|
|
2864
|
+
if (opts.noPrune) {
|
|
2865
|
+
await writeManifest(opts.dir, {
|
|
2866
|
+
language: opts.language,
|
|
2867
|
+
files: opts.currentPaths
|
|
2868
|
+
});
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
if (!opts.prevManifest) {
|
|
2872
|
+
console.log(`${opts.label}: no ${MANIFEST_FILENAME} found — skipping prune (this baseline manifest will be written now).`);
|
|
2873
|
+
await writeManifest(opts.dir, {
|
|
2874
|
+
language: opts.language,
|
|
2875
|
+
files: opts.currentPaths
|
|
2876
|
+
});
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
const stale = computeStalePaths(opts.prevManifest, opts.currentPaths);
|
|
2880
|
+
if (stale.length > 0) {
|
|
2881
|
+
const { pruned, preserved } = await pruneStaleFiles(opts.dir, stale, { header: opts.header });
|
|
2882
|
+
if (pruned.length > 0) console.log(`${opts.label}: pruned ${pruned.length} stale file${pruned.length === 1 ? "" : "s"}`);
|
|
2883
|
+
if (preserved.length > 0) {
|
|
2884
|
+
console.log(`${opts.label}: preserved ${preserved.length} file${preserved.length === 1 ? "" : "s"} in previous manifest but lacking the auto-generated header (possibly hand-edited).`);
|
|
2885
|
+
for (const p of preserved) console.log(` ${p}`);
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
await writeManifest(opts.dir, {
|
|
2889
|
+
language: opts.language,
|
|
2890
|
+
files: opts.currentPaths
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2570
2893
|
//#endregion
|
|
2571
2894
|
//#region src/engine/registry.ts
|
|
2572
2895
|
const emitters = /* @__PURE__ */ new Map();
|
|
@@ -2927,4 +3250,4 @@ function summarize(changes) {
|
|
|
2927
3250
|
//#endregion
|
|
2928
3251
|
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 };
|
|
2929
3252
|
|
|
2930
|
-
//# sourceMappingURL=diff-
|
|
3253
|
+
//# sourceMappingURL=diff-BYadQQV7.mjs.map
|