@workos/oagen 0.29.3 → 0.30.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.
@@ -177,6 +177,43 @@ function resolveResponseModelName(op) {
177
177
  if (ref.kind === "nullable" && ref.inner.kind === "model") return ref.inner.name;
178
178
  return null;
179
179
  }
180
+ /**
181
+ * Find every pair of distinct paths that resolve to the same SDK method name.
182
+ *
183
+ * Emitters already refuse to generate on a collision, but they only run inside
184
+ * the per-language build matrix — so a spec addition that collides fails every
185
+ * matrix job minutes in, with no cheap way to catch it first. Surfacing the same
186
+ * check from `oagen resolve` turns that into a seconds-long precheck.
187
+ *
188
+ * Returns every collision rather than throwing on the first, so a spec that
189
+ * introduces several is fixed in one pass instead of one CI round-trip each.
190
+ *
191
+ * Callers that generate a subset of services must filter to the operations they
192
+ * will actually emit before calling this: a collision between two out-of-scope
193
+ * paths is not that run's problem.
194
+ */
195
+ function findResolvedMethodCollisions(resolved) {
196
+ const seen = /* @__PURE__ */ new Map();
197
+ const collisions = [];
198
+ for (const r of resolved) {
199
+ const key = `${r.mountOn}.${r.methodName}`;
200
+ const current = {
201
+ httpMethod: r.operation.httpMethod.toUpperCase(),
202
+ path: r.operation.path
203
+ };
204
+ const existing = seen.get(key);
205
+ if (existing && (existing.path !== current.path || existing.httpMethod !== current.httpMethod)) {
206
+ collisions.push({
207
+ key,
208
+ first: existing,
209
+ second: current
210
+ });
211
+ continue;
212
+ }
213
+ if (!existing) seen.set(key, current);
214
+ }
215
+ return collisions;
216
+ }
180
217
  //#endregion
181
218
  //#region src/engine/merge-adapters/url-fingerprint.ts
182
219
  /**
@@ -714,6 +751,7 @@ function extractKotlinClassMembers(classBody, source) {
714
751
  const children = classBody.namedChildren;
715
752
  for (let i = 0; i < children.length; i++) {
716
753
  const child = children[i];
754
+ const declIndex = i;
717
755
  let memberName = null;
718
756
  if (child.type === "property_declaration") memberName = child.children.find((c) => c.type === "variable_declaration")?.children.find((c) => c.type === "simple_identifier")?.text ?? null;
719
757
  else if (child.type === "function_declaration") memberName = child.children.find((c) => c.type === "simple_identifier")?.text ?? null;
@@ -728,7 +766,7 @@ function extractKotlinClassMembers(classBody, source) {
728
766
  } else break;
729
767
  }
730
768
  let startIdx = child.startIndex;
731
- for (let k = i - 1; k >= 0; k--) {
769
+ for (let k = declIndex - 1; k >= 0; k--) {
732
770
  const prev = children[k];
733
771
  if (prev.type === "multiline_comment") {
734
772
  if (source.slice(prev.startIndex, prev.endIndex).startsWith("/**")) startIdx = prev.startIndex;
@@ -797,6 +835,13 @@ const kotlinMergeAdapter = {
797
835
  isManagedMember(member) {
798
836
  return KOTLIN_MANAGED_ACCESSOR_DOC.test(member.text);
799
837
  },
838
+ importedNames(imp) {
839
+ const path = imp.text.replace(/^import\s+/, "").trim();
840
+ const alias = path.match(/\s+as\s+([A-Za-z_][A-Za-z0-9_]*)$/);
841
+ if (alias) return [alias[1]];
842
+ const last = path.split(".").pop();
843
+ return last && last !== "*" ? [last] : [];
844
+ },
800
845
  shouldSkipDeepMerge(_symbolName, existingMemberKeys, newMembers) {
801
846
  const newText = newMembers.map((m) => m.text).join("\n");
802
847
  for (const match of newText.matchAll(/this\.(\w+)/g)) {
@@ -1672,6 +1717,116 @@ function declarationLineStart(content, declStartIndex) {
1672
1717
  return prevNewline === -1 ? 0 : prevNewline + 1;
1673
1718
  }
1674
1719
  /**
1720
+ * Widen a member's byte range so deleting it removes whole lines.
1721
+ *
1722
+ * The raw range starts at the declaration (or its docstring), which sits after
1723
+ * the member's indentation — splicing it out verbatim leaves that indentation
1724
+ * glued to the following line. Extends backwards over the indentation and
1725
+ * forwards over the trailing newline, then absorbs the blank line the removal
1726
+ * would otherwise duplicate (or strand in front of the block's closing brace).
1727
+ */
1728
+ function expandPruneRange(source, rawStart, rawEnd) {
1729
+ let start = rawStart;
1730
+ while (start > 0 && (source[start - 1] === " " || source[start - 1] === " ")) start--;
1731
+ let end = rawEnd;
1732
+ while (end < source.length && (source[end] === " " || source[end] === " ")) end++;
1733
+ end = skipLineBreak(source, end);
1734
+ if (precededByBlankLine(source, start)) {
1735
+ if (startsWithLineBreak(source, end)) end = skipLineBreak(source, end);
1736
+ else if (/^[ \t]*\}/.test(source.slice(end, end + 40))) start = backOverLineBreak(source, start);
1737
+ }
1738
+ return {
1739
+ start,
1740
+ end
1741
+ };
1742
+ }
1743
+ function startsWithLineBreak(source, idx) {
1744
+ return source[idx] === "\n" || source[idx] === "\r" && source[idx + 1] === "\n";
1745
+ }
1746
+ function skipLineBreak(source, idx) {
1747
+ let i = idx;
1748
+ if (source[i] === "\r") i++;
1749
+ return source[i] === "\n" ? i + 1 : i;
1750
+ }
1751
+ function backOverLineBreak(source, idx) {
1752
+ let i = idx;
1753
+ if (source[i - 1] === "\n") i--;
1754
+ if (source[i - 1] === "\r") i--;
1755
+ return i;
1756
+ }
1757
+ /** Whether the line ending immediately before `idx` (a line start) is empty. */
1758
+ function precededByBlankLine(source, idx) {
1759
+ if (idx === 0 || source[idx - 1] !== "\n") return false;
1760
+ const priorLineEnd = backOverLineBreak(source, idx);
1761
+ return priorLineEnd > 0 && source[priorLineEnd - 1] === "\n";
1762
+ }
1763
+ function referencesIdentifier(source, name) {
1764
+ return new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(source);
1765
+ }
1766
+ /**
1767
+ * Blank out comments and string text, preserving offsets and line structure.
1768
+ *
1769
+ * An identifier lookup over raw source can't tell a live reference from a name
1770
+ * that merely survives in prose. That distinction decides whether an import
1771
+ * stays: keeping one whose only remaining mention is a KDoc leaves an import of
1772
+ * a class the generator no longer emits, which doesn't compile — the very
1773
+ * failure the prune exists to prevent.
1774
+ *
1775
+ * Interpolated expressions are the exception: `"${Agents.NAME}"` is code that
1776
+ * sits inside a string literal, and masking it would drop an import the file
1777
+ * genuinely uses. String extents are masked around those holes, not over them.
1778
+ *
1779
+ * Grammar node names come from the adapter's `urlFingerprintConfig`, which
1780
+ * already inventories exactly these three categories per language. Comments are
1781
+ * matched structurally instead — every grammar we support names them `*comment`.
1782
+ */
1783
+ function maskNonCode(tree, source, config) {
1784
+ const stringNodeTypes = new Set(config?.stringNodeTypes ?? []);
1785
+ const interpolationNodeTypes = new Set(config?.interpolationNodeTypes ?? []);
1786
+ const spans = [];
1787
+ const collectStringSpans = (node) => {
1788
+ const holes = [];
1789
+ const findHoles = (n) => {
1790
+ if (interpolationNodeTypes.has(n.type)) {
1791
+ holes.push([n.startIndex, n.endIndex]);
1792
+ return;
1793
+ }
1794
+ for (const child of n.children) findHoles(child);
1795
+ };
1796
+ findHoles(node);
1797
+ let cursor = node.startIndex;
1798
+ for (const [holeStart, holeEnd] of holes) {
1799
+ if (holeStart > cursor) spans.push([cursor, holeStart]);
1800
+ cursor = holeEnd;
1801
+ }
1802
+ if (cursor < node.endIndex) spans.push([cursor, node.endIndex]);
1803
+ };
1804
+ const walk = (node) => {
1805
+ if (node.type.includes("comment")) {
1806
+ spans.push([node.startIndex, node.endIndex]);
1807
+ return;
1808
+ }
1809
+ if (stringNodeTypes.has(node.type)) {
1810
+ collectStringSpans(node);
1811
+ return;
1812
+ }
1813
+ for (const child of node.children) walk(child);
1814
+ };
1815
+ walk(tree.rootNode);
1816
+ if (spans.length === 0) return source;
1817
+ let masked = "";
1818
+ let cursor = 0;
1819
+ for (const [spanStart, spanEnd] of spans) {
1820
+ if (spanStart < cursor) continue;
1821
+ masked += source.slice(cursor, spanStart) + source.slice(spanStart, spanEnd).replace(/[^\r\n]/g, " ");
1822
+ cursor = spanEnd;
1823
+ }
1824
+ return masked + source.slice(cursor);
1825
+ }
1826
+ function removeLines(source, lines) {
1827
+ return source.split("\n").filter((line) => !lines.has(line.trim())).join("\n");
1828
+ }
1829
+ /**
1675
1830
  * Resolve a docstring range by searching for the nearest matching text before
1676
1831
  * the declaration start. Falls back to parser-provided offsets when search fails.
1677
1832
  */
@@ -1886,6 +2041,7 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
1886
2041
  let deepAdded = 0;
1887
2042
  let deepPruned = 0;
1888
2043
  const insertions = [];
2044
+ const prunedTexts = [];
1889
2045
  if (adapter.extractMembers) {
1890
2046
  const parser = await getParser(language);
1891
2047
  const resultTree = safeParse(parser, result);
@@ -1914,10 +2070,9 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
1914
2070
  if (pruneEdits.length > 0) {
1915
2071
  pruneEdits.sort((a, b) => b.start - a.start);
1916
2072
  for (const edit of pruneEdits) {
1917
- let end = edit.end;
1918
- while (end < result.length && (result[end] === " " || result[end] === " ")) end++;
1919
- if (result[end] === "\n") end++;
1920
- result = result.slice(0, edit.start) + result.slice(end);
2073
+ const { start, end } = expandPruneRange(result, edit.start, edit.end);
2074
+ prunedTexts.push(result.slice(start, end));
2075
+ result = result.slice(0, start) + result.slice(end);
1921
2076
  }
1922
2077
  deepPruned = pruneEdits.length;
1923
2078
  const reTree = safeParse(parser, result);
@@ -1982,6 +2137,21 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
1982
2137
  result = lines.join("\n");
1983
2138
  }
1984
2139
  }
2140
+ if (prunedTexts.length > 0 && adapter.importedNames) {
2141
+ const prunedText = prunedTexts.join("\n");
2142
+ const codeOnly = maskNonCode(safeParse(await getParser(language), result), result, adapter.urlFingerprintConfig);
2143
+ const orphaned = /* @__PURE__ */ new Set();
2144
+ for (const imp of existingStatements.imports) {
2145
+ const names = adapter.importedNames(imp);
2146
+ if (names.length === 0) continue;
2147
+ if (!names.some((n) => referencesIdentifier(prunedText, n))) continue;
2148
+ const line = imp.text.trim();
2149
+ const withoutImport = removeLines(codeOnly, new Set([line]));
2150
+ if (names.some((n) => referencesIdentifier(withoutImport, n))) continue;
2151
+ orphaned.add(line);
2152
+ }
2153
+ if (orphaned.size > 0) result = removeLines(result, orphaned);
2154
+ }
1985
2155
  let docstringUpdates = 0;
1986
2156
  {
1987
2157
  const parser = await getParser(language);
@@ -4282,6 +4452,6 @@ function summarize(changes) {
4282
4452
  };
4283
4453
  }
4284
4454
  //#endregion
4285
- export { resolveMountTarget as _, registerEmitter as a, integrateGeneratedFiles as c, applyFileHeaders as d, buildEmitterContext as f, deriveMethodName as g, generateFiles as h, getEmitter as i, mapFilesForTargetIntegration as l, generateAllFiles as m, buildDirectionIndex as n, generate as o, collectReferencedNames as p, directionOf as r, MANIFEST_FILENAME as s, diffSpecs as t, treeShakeFiles as u, resolveOperations as v };
4455
+ export { findResolvedMethodCollisions as _, registerEmitter as a, integrateGeneratedFiles as c, applyFileHeaders as d, buildEmitterContext as f, deriveMethodName as g, generateFiles as h, getEmitter as i, mapFilesForTargetIntegration as l, generateAllFiles as m, buildDirectionIndex as n, generate as o, collectReferencedNames as p, directionOf as r, MANIFEST_FILENAME as s, diffSpecs as t, treeShakeFiles as u, resolveMountTarget as v, resolveOperations as y };
4286
4456
 
4287
- //# sourceMappingURL=diff-DqnHHXvr.mjs.map
4457
+ //# sourceMappingURL=diff-DMFiDAI6.mjs.map