Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@contractkit/plugin-typescript

Package Overview
Dependencies
Maintainers
1
Versions
21
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@contractkit/plugin-typescript - npm Package Compare versions

Comparing version
0.17.5
to
0.18.0
+7
-7
.turbo/turbo-build$colon$ci.log
> @contractkit/plugin-typescript@0.17.5 build:ci /home/runner/work/ContractKit/ContractKit/packages/plugin-typescript
> @contractkit/plugin-typescript@0.18.0 build:ci /home/runner/work/ContractKit/ContractKit/packages/plugin-typescript
> eslint --max-warnings=0 && pnpm run build
> @contractkit/plugin-typescript@0.17.5 build /home/runner/work/ContractKit/ContractKit/packages/plugin-typescript
> @contractkit/plugin-typescript@0.18.0 build /home/runner/work/ContractKit/ContractKit/packages/plugin-typescript
> tsup src/index.ts --format esm --sourcemap --dts && tsc --emitDeclarationOnly --declaration

@@ -14,7 +14,7 @@

ESM Build start
ESM dist/index.js 126.39 KB
ESM dist/index.js.map 274.93 KB
ESM ⚡️ Build success in 349ms
ESM dist/index.js 135.58 KB
ESM dist/index.js.map 298.86 KB
ESM ⚡️ Build success in 308ms
DTS Build start
DTS ⚡️ Build success in 5193ms
DTS dist/index.d.ts 3.44 KB
DTS ⚡️ Build success in 5090ms
DTS dist/index.d.ts 3.67 KB
> @contractkit/plugin-typescript@0.17.5 test:ci /home/runner/work/ContractKit/ContractKit/packages/plugin-typescript
> @contractkit/plugin-typescript@0.18.0 test:ci /home/runner/work/ContractKit/ContractKit/packages/plugin-typescript
> vitest run --coverage

@@ -9,13 +9,13 @@

✓ tests/codegen-contract.test.ts (124 tests) 98ms
✓ tests/codegen-operation.test.ts (88 tests) 134ms
✓ tests/codegen-sdk.test.ts (123 tests) 127ms
✓ tests/codegen-server.test.ts (15 tests) 26ms
✓ tests/codegen-plain-types.test.ts (61 tests) 38ms
✓ tests/pipeline.test.ts (25 tests) 185ms
✓ tests/codegen-operation.test.ts (88 tests) 53ms
✓ tests/codegen-contract.test.ts (124 tests) 120ms
✓ tests/codegen-sdk.test.ts (131 tests) 76ms
✓ tests/codegen-plain-types.test.ts (61 tests) 37ms
✓ tests/pipeline.test.ts (25 tests) 142ms
✓ tests/codegen-server.test.ts (19 tests) 24ms
 Test Files  6 passed (6)
 Tests  436 passed (436)
 Start at  12:37:07
 Duration  6.66s (transform 3.78s, setup 0ms, import 13.89s, tests 608ms, environment 9ms)
 Tests  448 passed (448)
 Start at  16:22:10
 Duration  6.11s (transform 5.19s, setup 0ms, import 12.92s, tests 452ms, environment 1ms)

@@ -26,10 +26,10 @@  % Coverage report from v8

-------------------|---------|----------|---------|---------|-------------------
All files | 77.84 | 74.45 | 82.5 | 79.74 |
src | 77.54 | 74.08 | 81.92 | 79.4 |
All files | 80.98 | 76.16 | 84.16 | 83.21 |
src | 80.77 | 75.84 | 83.76 | 82.97 |
...n-contract.ts | 87.83 | 82.12 | 90.08 | 88.96 | ...1069,1074-1075
...-operation.ts | 78.18 | 74.26 | 78.08 | 79.69 | ...68-679,684-685
...lain-types.ts | 89.08 | 78.37 | 96.66 | 92.3 | ...31,243,249,301
codegen-sdk.ts | 88.75 | 83.6 | 85 | 91.49 | ...23,678,712-729
index.ts | 23.35 | 19.51 | 33.33 | 25.69 | ...97,415,418,421
path-utils.ts | 23.47 | 18.84 | 58.33 | 23.71 | ...1,54-55,80-187
codegen-sdk.ts | 88.26 | 83.01 | 87.14 | 91.22 | ...1087-1088,1091
index.ts | 57.28 | 46.93 | 59.25 | 61.27 | ...90,493,519,522
path-utils.ts | 36.52 | 27.53 | 75 | 38.14 | ...32-136,147-187
ts-render.ts | 82.19 | 85.39 | 72.22 | 87.09 | 56,90,126,157-165

@@ -36,0 +36,0 @@ tests | 90.38 | 90.9 | 89.28 | 93.47 |

# @contractkit/contractkit-plugin-typescript
## 0.18.0
### Minor Changes
- 6f8e3b6: Group TypeScript SDK clients by `keys.area` and `keys.subarea`. Files declaring `subarea` produce a leaf `<Area><Subarea>Client` exposed at `sdk.<area>.<subarea>`; area-only files (no subarea) inline their methods directly onto a synthesized `<Area>Client` and surface as `sdk.<area>.<method>`. Files with no area keep the legacy flat `sdk.<filename>` shape.
`{subarea}` is a new path-template variable on `output.clients` and `output.types`, enabling layouts like `src/{area}/{subarea}.client.ts`. Multiple area-level files merging into one client throw a codegen-time error if any method names collide — disambiguate with `sdk:` or move into a subarea.
Breaking: area-level files no longer emit a standalone `*.client.ts` (their methods live on the area client in `sdk.ts`). The `generateSdkAggregator` signature now takes a structured `SdkAggregatorInput` rather than `(clients, importPath?, className?)`.
## 0.17.5

@@ -4,0 +14,0 @@

@@ -26,5 +26,5 @@

<div class='fl pad1y space-right2'>
<span class="strong">77.84% </span>
<span class="strong">80.98% </span>
<span class="quiet">Statements</span>
<span class='fraction'>1732/2225</span>
<span class='fraction'>1921/2372</span>
</div>

@@ -34,5 +34,5 @@

<div class='fl pad1y space-right2'>
<span class="strong">74.45% </span>
<span class="strong">76.16% </span>
<span class="quiet">Branches</span>
<span class='fraction'>1119/1503</span>
<span class='fraction'>1198/1573</span>
</div>

@@ -42,5 +42,5 @@

<div class='fl pad1y space-right2'>
<span class="strong">82.5% </span>
<span class="strong">84.16% </span>
<span class="quiet">Functions</span>
<span class='fraction'>297/360</span>
<span class='fraction'>319/379</span>
</div>

@@ -50,5 +50,5 @@

<div class='fl pad1y space-right2'>
<span class="strong">79.74% </span>
<span class="strong">83.21% </span>
<span class="quiet">Lines</span>
<span class='fraction'>1508/1891</span>
<span class='fraction'>1681/2020</span>
</div>

@@ -68,3 +68,3 @@

</div>
<div class='status-line medium'></div>
<div class='status-line high'></div>
<div class="pad1">

@@ -87,14 +87,14 @@ <table class="coverage-summary">

<tbody><tr>
<td class="file medium" data-value="src"><a href="src/index.html">src</a></td>
<td data-value="77.54" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 77%"></div><div class="cover-empty" style="width: 23%"></div></div>
<td class="file high" data-value="src"><a href="src/index.html">src</a></td>
<td data-value="80.77" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 80%"></div><div class="cover-empty" style="width: 20%"></div></div>
</td>
<td data-value="77.54" class="pct medium">77.54%</td>
<td data-value="2173" class="abs medium">1685/2173</td>
<td data-value="74.08" class="pct medium">74.08%</td>
<td data-value="1470" class="abs medium">1089/1470</td>
<td data-value="81.92" class="pct high">81.92%</td>
<td data-value="332" class="abs high">272/332</td>
<td data-value="79.4" class="pct medium">79.4%</td>
<td data-value="1845" class="abs medium">1465/1845</td>
<td data-value="80.77" class="pct high">80.77%</td>
<td data-value="2320" class="abs high">1874/2320</td>
<td data-value="75.84" class="pct medium">75.84%</td>
<td data-value="1540" class="abs medium">1168/1540</td>
<td data-value="83.76" class="pct high">83.76%</td>
<td data-value="351" class="abs high">294/351</td>
<td data-value="82.97" class="pct high">82.97%</td>
<td data-value="1974" class="abs high">1638/1974</td>
</tr>

@@ -125,3 +125,3 @@

<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T12:37:14.475Z
at 2026-05-06T16:22:17.029Z
</div>

@@ -128,0 +128,0 @@ <script src="prettify.js"></script>

@@ -985,3 +985,3 @@

<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T12:37:14.475Z
at 2026-05-06T16:22:17.029Z
</div>

@@ -988,0 +988,0 @@ <script src="../prettify.js"></script>

@@ -26,5 +26,5 @@

<div class='fl pad1y space-right2'>
<span class="strong">77.54% </span>
<span class="strong">80.77% </span>
<span class="quiet">Statements</span>
<span class='fraction'>1685/2173</span>
<span class='fraction'>1874/2320</span>
</div>

@@ -34,5 +34,5 @@

<div class='fl pad1y space-right2'>
<span class="strong">74.08% </span>
<span class="strong">75.84% </span>
<span class="quiet">Branches</span>
<span class='fraction'>1089/1470</span>
<span class='fraction'>1168/1540</span>
</div>

@@ -42,5 +42,5 @@

<div class='fl pad1y space-right2'>
<span class="strong">81.92% </span>
<span class="strong">83.76% </span>
<span class="quiet">Functions</span>
<span class='fraction'>272/332</span>
<span class='fraction'>294/351</span>
</div>

@@ -50,5 +50,5 @@

<div class='fl pad1y space-right2'>
<span class="strong">79.4% </span>
<span class="strong">82.97% </span>
<span class="quiet">Lines</span>
<span class='fraction'>1465/1845</span>
<span class='fraction'>1638/1974</span>
</div>

@@ -68,3 +68,3 @@

</div>
<div class='status-line medium'></div>
<div class='status-line high'></div>
<div class="pad1">

@@ -133,28 +133,28 @@ <table class="coverage-summary">

<td class="file high" data-value="codegen-sdk.ts"><a href="codegen-sdk.ts.html">codegen-sdk.ts</a></td>
<td data-value="88.75" class="pic high">
<td data-value="88.26" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 88%"></div><div class="cover-empty" style="width: 12%"></div></div>
</td>
<td data-value="88.75" class="pct high">88.75%</td>
<td data-value="507" class="abs high">450/507</td>
<td data-value="83.6" class="pct high">83.6%</td>
<td data-value="311" class="abs high">260/311</td>
<td data-value="85" class="pct high">85%</td>
<td data-value="60" class="abs high">51/60</td>
<td data-value="91.49" class="pct high">91.49%</td>
<td data-value="447" class="abs high">409/447</td>
<td data-value="88.26" class="pct high">88.26%</td>
<td data-value="622" class="abs high">549/622</td>
<td data-value="83.01" class="pct high">83.01%</td>
<td data-value="365" class="abs high">303/365</td>
<td data-value="87.14" class="pct high">87.14%</td>
<td data-value="70" class="abs high">61/70</td>
<td data-value="91.22" class="pct high">91.22%</td>
<td data-value="547" class="abs high">499/547</td>
</tr>
<tr>
<td class="file low" data-value="index.ts"><a href="index.ts.html">index.ts</a></td>
<td data-value="23.35" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 23%"></div><div class="cover-empty" style="width: 77%"></div></div>
<td class="file medium" data-value="index.ts"><a href="index.ts.html">index.ts</a></td>
<td data-value="57.28" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 57%"></div><div class="cover-empty" style="width: 43%"></div></div>
</td>
<td data-value="23.35" class="pct low">23.35%</td>
<td data-value="167" class="abs low">39/167</td>
<td data-value="19.51" class="pct low">19.51%</td>
<td data-value="82" class="abs low">16/82</td>
<td data-value="33.33" class="pct low">33.33%</td>
<td data-value="18" class="abs low">6/18</td>
<td data-value="25.69" class="pct low">25.69%</td>
<td data-value="144" class="abs low">37/144</td>
<td data-value="57.28" class="pct medium">57.28%</td>
<td data-value="199" class="abs medium">114/199</td>
<td data-value="46.93" class="pct low">46.93%</td>
<td data-value="98" class="abs low">46/98</td>
<td data-value="59.25" class="pct medium">59.25%</td>
<td data-value="27" class="abs medium">16/27</td>
<td data-value="61.27" class="pct medium">61.27%</td>
<td data-value="173" class="abs medium">106/173</td>
</tr>

@@ -164,13 +164,13 @@

<td class="file low" data-value="path-utils.ts"><a href="path-utils.ts.html">path-utils.ts</a></td>
<td data-value="23.47" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 23%"></div><div class="cover-empty" style="width: 77%"></div></div>
<td data-value="36.52" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 36%"></div><div class="cover-empty" style="width: 64%"></div></div>
</td>
<td data-value="23.47" class="pct low">23.47%</td>
<td data-value="115" class="abs low">27/115</td>
<td data-value="18.84" class="pct low">18.84%</td>
<td data-value="69" class="abs low">13/69</td>
<td data-value="58.33" class="pct medium">58.33%</td>
<td data-value="12" class="abs medium">7/12</td>
<td data-value="23.71" class="pct low">23.71%</td>
<td data-value="97" class="abs low">23/97</td>
<td data-value="36.52" class="pct low">36.52%</td>
<td data-value="115" class="abs low">42/115</td>
<td data-value="27.53" class="pct low">27.53%</td>
<td data-value="69" class="abs low">19/69</td>
<td data-value="75" class="pct medium">75%</td>
<td data-value="12" class="abs medium">9/12</td>
<td data-value="38.14" class="pct low">38.14%</td>
<td data-value="97" class="abs low">37/97</td>
</tr>

@@ -201,3 +201,3 @@

<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T12:37:14.475Z
at 2026-05-06T16:22:17.029Z
</div>

@@ -204,0 +204,0 @@ <script src="../prettify.js"></script>

@@ -26,5 +26,5 @@

<div class='fl pad1y space-right2'>
<span class="strong">23.47% </span>
<span class="strong">36.52% </span>
<span class="quiet">Statements</span>
<span class='fraction'>27/115</span>
<span class='fraction'>42/115</span>
</div>

@@ -34,5 +34,5 @@

<div class='fl pad1y space-right2'>
<span class="strong">18.84% </span>
<span class="strong">27.53% </span>
<span class="quiet">Branches</span>
<span class='fraction'>13/69</span>
<span class='fraction'>19/69</span>
</div>

@@ -42,5 +42,5 @@

<div class='fl pad1y space-right2'>
<span class="strong">58.33% </span>
<span class="strong">75% </span>
<span class="quiet">Functions</span>
<span class='fraction'>7/12</span>
<span class='fraction'>9/12</span>
</div>

@@ -50,5 +50,5 @@

<div class='fl pad1y space-right2'>
<span class="strong">23.71% </span>
<span class="strong">38.14% </span>
<span class="quiet">Lines</span>
<span class='fraction'>23/97</span>
<span class='fraction'>37/97</span>
</div>

@@ -265,18 +265,18 @@

<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">48x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">63x</span>
<span class="cline-any cline-no">&nbsp;</span>

@@ -287,3 +287,3 @@ <span class="cline-any cline-no">&nbsp;</span>

<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-neutral">&nbsp;</span>

@@ -339,13 +339,13 @@ <span class="cline-any cline-neutral">&nbsp;</span>

<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>

@@ -383,2 +383,4 @@ <span class="cline-any cline-no">&nbsp;</span>

<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-no">&nbsp;</span>

@@ -388,9 +390,7 @@ <span class="cline-any cline-no">&nbsp;</span>

<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>

@@ -400,3 +400,3 @@ <span class="cline-any cline-neutral">&nbsp;</span>

<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>

@@ -524,3 +524,3 @@ <span class="cline-any cline-neutral">&nbsp;</span>

&nbsp;
export function <span class="fstat-no" title="function not covered" >computeSdkOutPath(</span>
export function computeSdkOutPath(
filePath: string,

@@ -530,14 +530,14 @@ rootDir: string,

commonRoot: string,
meta: Record&lt;string, string&gt; = <span class="branch-0 cbranch-no" title="branch not covered" >{},</span>
meta: Record&lt;string, string&gt; = {},
): string | null {
<span class="cstat-no" title="statement not covered" > if (!filePath.endsWith('.ck')) <span class="cstat-no" title="statement not covered" >return null;</span></span>
const baseName = <span class="cstat-no" title="statement not covered" >filePath.split('/').pop()!;</span>
const defaultOutName = <span class="cstat-no" title="statement not covered" >baseName.replace(/\.ck$/, '.client.ts');</span>
const <span class="cstat-no" title="statement not covered" >baseOutDir = resolve(rootDir);</span>
const <span class="cstat-no" title="statement not covered" >relDir = relative(commonRoot, dirname(filePath));</span>
const filename = <span class="cstat-no" title="statement not covered" >baseName.replace(/\.ck$/, '');</span>
<span class="missing-if-branch" title="if path not taken" >I</span>if (!filePath.endsWith('.ck')) <span class="cstat-no" title="statement not covered" >return null;</span>
const baseName = filePath.split('/').pop()!;
const defaultOutName = baseName.replace(/\.ck$/, '.client.ts');
const baseOutDir = resolve(rootDir);
const relDir = relative(commonRoot, dirname(filePath));
const filename = baseName.replace(/\.ck$/, '');
&nbsp;
<span class="cstat-no" title="statement not covered" > if (clientOutput &amp;&amp; TEMPLATE_VAR_RE.test(clientOutput)) {</span>
const resolved = <span class="cstat-no" title="statement not covered" >resolveTemplate(clientOutput, { filename, dir: relDir, ext: 'ck', ...meta });</span>
<span class="cstat-no" title="statement not covered" > if (includesFilename(resolved)) <span class="cstat-no" title="statement not covered" >return join(baseOutDir, resolved);</span></span>
<span class="missing-if-branch" title="else path not taken" >E</span>if (clientOutput &amp;&amp; TEMPLATE_VAR_RE.test(clientOutput)) {
const resolved = resolveTemplate(clientOutput, { filename, dir: relDir, ext: 'ck', ...meta });
<span class="missing-if-branch" title="else path not taken" >E</span>if (includesFilename(resolved)) return join(baseOutDir, resolved);
<span class="cstat-no" title="statement not covered" > return join(baseOutDir, resolved, defaultOutName);</span>

@@ -575,5 +575,5 @@ }

&nbsp;
export function <span class="fstat-no" title="function not covered" >generateBarrelFiles(c</span>ontractPaths: string[]): { outPath: string; content: string }[] {
const byDir = <span class="cstat-no" title="statement not covered" >new Map&lt;string, string[]&gt;();</span>
<span class="cstat-no" title="statement not covered" > for (const outPath of contractPaths) {</span>
export function generateBarrelFiles(contractPaths: string[]): { outPath: string; content: string }[] {
const byDir = new Map&lt;string, string[]&gt;();
for (const outPath of contractPaths) {
const <span class="cstat-no" title="statement not covered" >dir = dirname(outPath);</span>

@@ -584,4 +584,4 @@ const group = <span class="cstat-no" title="statement not covered" >byDir.get(dir) ?? [];</span>

}
const results: { outPath: string; content: string }[] = <span class="cstat-no" title="statement not covered" >[];</span>
<span class="cstat-no" title="statement not covered" > for (const [dir, files] of byDir) {</span>
const results: { outPath: string; content: string }[] = [];
for (const [dir, files] of byDir) {
const exports = <span class="cstat-no" title="statement not covered" >files</span>

@@ -593,3 +593,3 @@ .map(<span class="fstat-no" title="function not covered" >f =&gt; <span class="cstat-no" title="statement not covered" >`</span>export * from './${f.split('/').pop()!.replace(/\.ts$/, '.js')}';`)</span>

}
<span class="cstat-no" title="statement not covered" > return results;</span>
return results;
}

@@ -652,3 +652,3 @@ &nbsp;

<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T12:37:14.475Z
at 2026-05-06T16:22:17.029Z
</div>

@@ -655,0 +655,0 @@ <script src="../prettify.js"></script>

@@ -257,3 +257,3 @@

<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">343x</span>
<span class="cline-any cline-yes">347x</span>
<span class="cline-any cline-neutral">&nbsp;</span>

@@ -285,3 +285,3 @@ <span class="cline-any cline-yes">170x</span>

<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">115x</span>
<span class="cline-any cline-yes">119x</span>
<span class="cline-any cline-neutral">&nbsp;</span>

@@ -379,3 +379,3 @@ <span class="cline-any cline-yes">2x</span>

<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">91x</span>
<span class="cline-any cline-yes">95x</span>
<span class="cline-any cline-yes">8x</span>

@@ -583,3 +583,3 @@ <span class="cline-any cline-neutral">&nbsp;</span>

<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T12:37:14.475Z
at 2026-05-06T16:22:17.029Z
</div>

@@ -586,0 +586,0 @@ <script src="../prettify.js"></script>

@@ -344,3 +344,3 @@

<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">874x</span>
<span class="cline-any cline-yes">892x</span>
<span class="cline-any cline-neutral">&nbsp;</span>

@@ -381,3 +381,3 @@ <span class="cline-any cline-neutral">&nbsp;</span>

<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">203x</span>
<span class="cline-any cline-yes">207x</span>
<span class="cline-any cline-neutral">&nbsp;</span>

@@ -451,12 +451,12 @@ <span class="cline-any cline-neutral">&nbsp;</span>

<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">114x</span>
<span class="cline-any cline-yes">114x</span>
<span class="cline-any cline-yes">123x</span>
<span class="cline-any cline-yes">123x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">100x</span>
<span class="cline-any cline-yes">100x</span>
<span class="cline-any cline-yes">104x</span>
<span class="cline-any cline-yes">104x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">93x</span>
<span class="cline-any cline-yes">97x</span>
<span class="cline-any cline-neutral">&nbsp;</span>

@@ -475,6 +475,6 @@ <span class="cline-any cline-neutral">&nbsp;</span>

<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">203x</span>
<span class="cline-any cline-yes">203x</span>
<span class="cline-any cline-yes">203x</span>
<span class="cline-any cline-yes">203x</span>
<span class="cline-any cline-yes">212x</span>
<span class="cline-any cline-yes">212x</span>
<span class="cline-any cline-yes">212x</span>
<span class="cline-any cline-yes">212x</span>
<span class="cline-any cline-neutral">&nbsp;</span>

@@ -493,8 +493,8 @@ <span class="cline-any cline-neutral">&nbsp;</span>

<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">193x</span>
<span class="cline-any cline-yes">193x</span>
<span class="cline-any cline-yes">202x</span>
<span class="cline-any cline-yes">202x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">187x</span>
<span class="cline-any cline-yes">198x</span>
<span class="cline-any cline-neutral">&nbsp;</span>

@@ -819,3 +819,3 @@ <span class="cline-any cline-neutral">&nbsp;</span>

<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T12:37:14.475Z
at 2026-05-06T16:22:17.029Z
</div>

@@ -822,0 +822,0 @@ <script src="../prettify.js"></script>

@@ -104,3 +104,3 @@

<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T12:37:14.475Z
at 2026-05-06T16:22:17.029Z
</div>

@@ -107,0 +107,0 @@ <script src="../prettify.js"></script>

import type { OpRootNode } from '@contractkit/core';
/** Options shared by every SDK code-generation entry point. */
export interface SdkCodegenOptions {
/** Template for type import paths when `modelOutPaths` is not provided. Supports `{module}` and `{base}`. */
typeImportPathTemplate?: string;
/** Absolute path of the file currently being generated. Used to compute relative imports. */
outPath?: string;

@@ -19,2 +22,8 @@ /** Map from model name → absolute output file path (for cross-module type imports) */

includeInternal?: boolean;
/**
* Override the generated client class name. When omitted, falls back to
* `deriveClientClassName(root.file)` (the legacy per-file name). The aggregator
* uses this to emit `<Area><Subarea>Client` for area+subarea leaf files.
*/
clientClassName?: string;
}

@@ -27,14 +36,99 @@ /**

export declare function hasPublicOperations(root: OpRootNode, includeInternal?: boolean): boolean;
/**
* Generate a complete `*.client.ts` file for one operation root: imports, the client class
* declaration, and one method per public operation. Used for top-level (no-area) files and
* for subarea-leaf files. Area-level files are NOT routed through this — their methods get
* inlined into the SDK aggregator via {@link generateClientMethods} + {@link generateSdkAggregator}.
*/
export declare function generateSdk(root: OpRootNode, options?: SdkCodegenOptions): string;
/**
* Render the method-block lines for an operation file as if they were declared inside a
* client class. Returns one consolidated array of strings (each pre-indented for class body
* level, with leading blank lines between methods) plus the set of method names emitted —
* the caller uses the names to detect cross-file collisions when multiple files contribute
* to the same area-level client.
*
* Skips operations marked `internal` unless `options.includeInternal` is set.
*/
export declare function generateClientMethods(root: OpRootNode, options: SdkCodegenOptions): {
lines: string[];
methodNames: string[];
};
/** Derive a client class name from a `.ck` file path, e.g. `users.ck` → `UsersClient`. Used for legacy flat (no-area) files. */
export declare function deriveClientClassName(file: string): string;
/** Camel-cased property name for a flat client on the SDK aggregator, e.g. `users.ck` → `users`. */
export declare function deriveClientPropertyName(file: string): string;
/**
* Pull `area` / `subarea` from a file's `root.meta` (set via `options { keys: { ... } }`).
* Both are optional. `area` drives top-level SDK grouping; `subarea` drives nesting under
* an area's client class.
*/
export declare function getAreaSubarea(root: OpRootNode): {
area?: string;
subarea?: string;
};
/** Class name for the area-level client, e.g. `area=identity` → `IdentityClient`. */
export declare function deriveAreaClientClassName(area: string): string;
/** Property name on the SDK aggregator for an area, e.g. `area=identity` → `identity`. */
export declare function deriveAreaPropertyName(area: string): string;
/** Class name for a leaf subarea client, e.g. `(identity, invitations)` → `IdentityInvitationsClient`. */
export declare function deriveSubareaClientClassName(area: string, subarea: string): string;
/** Property name on the area client for a subarea, e.g. `subarea=invitations` → `invitations`. */
export declare function deriveSubareaPropertyName(subarea: string): string;
/** Generate the shared SdkOptions interface file. */
export declare function generateSdkOptions(): string;
/**
* Reference to a per-file leaf client emitted to its own `*.client.ts`. Used by the
* aggregator to import the class and wire it as either a top-level `sdk.<prop>` or a
* nested `sdk.<area>.<subarea>` property.
*/
export interface SdkClientInfo {
/** Client class name (e.g. `UsersClient`, `IdentityInvitationsClient`). */
className: string;
/** Property name to expose this client under (e.g. `users`, `invitations`). */
propertyName: string;
/** Module specifier for the leaf file, relative to `sdk.ts` and `.js`-suffixed. */
importPath: string;
}
/** Generate the sdk.ts aggregator that wraps all clients into a single Sdk class. */
export declare function generateSdkAggregator(clients: SdkClientInfo[], sdkOptionsImportPath?: string, sdkClassName?: string): string;
/**
* One area-level (no-subarea) `.ck` file whose methods are inlined directly into the
* generated `<Area>Client` class instead of getting a standalone `*.client.ts`.
*/
export interface SdkAreaInlineFile {
/** Parsed AST. */
root: OpRootNode;
/** Codegen options for this file (must have `outPath` pointing at the SDK aggregator file so type-import paths resolve correctly). */
codegenOptions: SdkCodegenOptions;
}
/** A grouping of files that share the same `keys.area`. */
export interface SdkAreaInfo {
area: string;
/** Files for which all methods are inlined into the area client (no subarea). */
inlineFiles: SdkAreaInlineFile[];
/** Per-file leaf clients exposed as named properties on the area client. */
subareaClients: {
propertyName: string;
client: SdkClientInfo;
}[];
}
export interface SdkAggregatorInput {
/** Files with no `keys.area` — kept as flat `Sdk.<filename>` properties (legacy behavior). */
topLevelClients: SdkClientInfo[];
/** One entry per `keys.area`. */
areas: SdkAreaInfo[];
/** Path to `sdk-options.ts` to import `SdkOptions`/`createSdkFetch`/etc. from. */
sdkOptionsImportPath?: string;
/** Name of the top-level aggregator class. Defaults to `Sdk`. */
sdkClassName?: string;
}
/**
* Generate the SDK aggregator (`sdk.ts`) — the entry-point file consumers import.
*
* Emits one `<Area>Client` class per area (combining inlined area-level methods with
* subarea property wiring), one `class Sdk` exposing area properties + flat top-level
* properties, plus all imports needed by the inlined methods.
*
* @throws if two area-level files in the same area produce the same method name.
*/
export declare function generateSdkAggregator(input: SdkAggregatorInput): string;
//# sourceMappingURL=codegen-sdk.d.ts.map

@@ -1,1 +0,1 @@

{"version":3,"file":"codegen-sdk.d.ts","sourceRoot":"","sources":["../src/codegen-sdk.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAkF,MAAM,mBAAmB,CAAC;AA2DpI,MAAM,WAAW,iBAAiB;IAC9B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,8GAA8G;IAC9G,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qFAAqF;IACrF,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,oFAAoF;IACpF,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC/B;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,UAAU,EAAE,eAAe,UAAQ,GAAG,OAAO,CAOtF;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,GAAE,iBAAsB,GAAG,MAAM,CAmHrF;AAsTD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG7D;AA2QD,qDAAqD;AACrD,wBAAgB,kBAAkB,IAAI,MAAM,CA6E3C;AAED,MAAM,WAAW,aAAa;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,qFAAqF;AACrF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,aAAa,EAAE,EAAE,oBAAoB,SAAqB,EAAE,YAAY,SAAQ,GAAG,MAAM,CAyBvI"}
{"version":3,"file":"codegen-sdk.d.ts","sourceRoot":"","sources":["../src/codegen-sdk.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAkF,MAAM,mBAAmB,CAAC;AA2DpI,+DAA+D;AAC/D,MAAM,WAAW,iBAAiB;IAC9B,6GAA6G;IAC7G,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,6FAA6F;IAC7F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,8GAA8G;IAC9G,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qFAAqF;IACrF,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,oFAAoF;IACpF,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC/B;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,UAAU,EAAE,eAAe,UAAQ,GAAG,OAAO,CAOtF;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,GAAE,iBAAsB,GAAG,MAAM,CAmHrF;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACjC,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,iBAAiB,GAC3B;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAA;CAAE,CAe5C;AAsTD,gIAAgI;AAChI,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,oGAAoG;AACpG,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAEpF;AAeD,qFAAqF;AACrF,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,0FAA0F;AAC1F,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,0GAA0G;AAC1G,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAElF;AAED,kGAAkG;AAClG,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjE;AA2QD,qDAAqD;AACrD,wBAAgB,kBAAkB,IAAI,MAAM,CA6E3C;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC1B,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,YAAY,EAAE,MAAM,CAAC;IACrB,mFAAmF;IACnF,UAAU,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAC9B,kBAAkB;IAClB,IAAI,EAAE,UAAU,CAAC;IACjB,sIAAsI;IACtI,cAAc,EAAE,iBAAiB,CAAC;CACrC;AAED,2DAA2D;AAC3D,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,WAAW,EAAE,iBAAiB,EAAE,CAAC;IACjC,4EAA4E;IAC5E,cAAc,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,aAAa,CAAA;KAAE,EAAE,CAAC;CACrE;AAED,MAAM,WAAW,kBAAkB;IAC/B,8FAA8F;IAC9F,eAAe,EAAE,aAAa,EAAE,CAAC;IACjC,iCAAiC;IACjC,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,kFAAkF;IAClF,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,CA6IvE"}

@@ -40,5 +40,5 @@ import type { ContractKitPlugin } from '@contractkit/core';

sdk?: string;
/** Path template for SDK type files. Supports {filename}, {dir}, {area}. */
/** Path template for SDK type files. Supports {filename}, {dir}, {area}, {subarea}. */
types?: string;
/** Path template for client class files. Supports {filename}, {dir}, {area}. */
/** Path template for client class files. Supports {filename}, {dir}, {area}, {subarea}. */
clients?: string;

@@ -77,3 +77,8 @@ };

export default plugin;
/**
* Build a `@contractkit/plugin-typescript` instance with explicit configuration, for
* programmatic use (tests, custom build scripts). Prefer the default export when loading
* via `contractkit.config.json`.
*/
export declare function createTypescriptPlugin(config: TypescriptPluginConfig, rootDir: string): ContractKitPlugin;
//# sourceMappingURL=index.d.ts.map

@@ -1,1 +0,1 @@

{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAwB3D,MAAM,WAAW,YAAY;IACzB,wFAAwF;IACxF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE;QACL,+GAA+G;QAC/G,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB;;;WAGG;QACH,KAAK,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,2EAA2E;IAC3E,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,SAAS;IACtB,qFAAqF;IACrF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE;QACL,qFAAqF;QACrF,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,4EAA4E;QAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,gFAAgF;QAChF,OAAO,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,SAAS;IACtB,4FAA4F;IAC5F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0GAA0G;IAC1G,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IACxB,uGAAuG;IACvG,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yGAAyG;IACzG,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACnC,+DAA+D;IAC/D,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,0EAA0E;IAC1E,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,8DAA8D;IAC9D,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,oGAAoG;IACpG,KAAK,CAAC,EAAE,WAAW,CAAC;CACvB;AAsRD,QAAA,MAAM,MAAM,EAAE,iBAmBb,CAAC;AAEF,eAAe,MAAM,CAAC;AAItB,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM,GAAG,iBAAiB,CAmBzG"}
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AA6B3D,MAAM,WAAW,YAAY;IACzB,wFAAwF;IACxF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE;QACL,+GAA+G;QAC/G,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB;;;WAGG;QACH,KAAK,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,2EAA2E;IAC3E,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,SAAS;IACtB,qFAAqF;IACrF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE;QACL,qFAAqF;QACrF,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,uFAAuF;QACvF,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,2FAA2F;QAC3F,OAAO,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,SAAS;IACtB,4FAA4F;IAC5F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0GAA0G;IAC1G,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IACxB,uGAAuG;IACvG,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yGAAyG;IACzG,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACnC,+DAA+D;IAC/D,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,0EAA0E;IAC1E,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,8DAA8D;IAC9D,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,oGAAoG;IACpG,KAAK,CAAC,EAAE,WAAW,CAAC;CACvB;AAiXD,QAAA,MAAM,MAAM,EAAE,iBAmBb,CAAC;AAEF,eAAe,MAAM,CAAC;AAItB;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM,GAAG,iBAAiB,CAmBzG"}
{
"name": "@contractkit/plugin-typescript",
"version": "0.17.5",
"version": "0.18.0",
"description": "ContractKit built-in plugin: TypeScript codegen (SDK clients, Koa routers, Zod schemas, plain types)",

@@ -32,4 +32,4 @@ "author": {

"devDependencies": {
"@repo/config-eslint": "0.3.1",
"@repo/config-typescript": "0.1.0"
"@repo/config-typescript": "0.1.0",
"@repo/config-eslint": "0.3.1"
},

@@ -36,0 +36,0 @@ "scripts": {

@@ -107,2 +107,3 @@ # @contractkit/contractkit-plugin-typescript

| `{area}` | Value of the `area` key from the `options` block |
| `{subarea}` | Value of the `subarea` key from the `options` block |
| `{name}` | The `name` option from the SDK sub-config |

@@ -128,4 +129,12 @@

Each operation file generates one client class. The aggregator SDK class (in `output.sdk`) instantiates all clients and exposes them as properties.
Operation files cluster on the SDK based on `keys.area` and `keys.subarea` (set in each file's `options { keys: { ... } }` block):
| File metadata | Generated layout |
| --- | --- |
| `area: identity, subarea: invitations` | leaf `IdentityInvitationsClient` emitted as `<output.clients>` (path can use `{subarea}`); aggregator wires it as `sdk.identity.invitations` |
| `area: identity` (no subarea) | methods inlined directly on `IdentityClient` (no standalone `*.client.ts`); exposed as `sdk.identity.<method>` |
| neither | flat `<Filename>Client` exposed as `sdk.<filename>` (legacy behavior) |
Multiple files mapping to the same `(area, subarea)` are merged into one leaf class. Multiple area-level files merge into a single `<Area>Client`; duplicate method names across them throw at codegen — disambiguate with `sdk:` or move one into a subarea.
Method names follow this priority:

@@ -132,0 +141,0 @@

@@ -60,4 +60,7 @@ import type { OpRootNode, OpRouteNode, OpOperationNode, OpRequestBodyNode, ContractTypeNode, ParamSource } from '@contractkit/core';

/** Options shared by every SDK code-generation entry point. */
export interface SdkCodegenOptions {
/** Template for type import paths when `modelOutPaths` is not provided. Supports `{module}` and `{base}`. */
typeImportPathTemplate?: string;
/** Absolute path of the file currently being generated. Used to compute relative imports. */
outPath?: string;

@@ -78,2 +81,8 @@ /** Map from model name → absolute output file path (for cross-module type imports) */

includeInternal?: boolean;
/**
* Override the generated client class name. When omitted, falls back to
* `deriveClientClassName(root.file)` (the legacy per-file name). The aggregator
* uses this to emit `<Area><Subarea>Client` for area+subarea leaf files.
*/
clientClassName?: string;
}

@@ -95,2 +104,8 @@

/**
* Generate a complete `*.client.ts` file for one operation root: imports, the client class
* declaration, and one method per public operation. Used for top-level (no-area) files and
* for subarea-leaf files. Area-level files are NOT routed through this — their methods get
* inlined into the SDK aggregator via {@link generateClientMethods} + {@link generateSdkAggregator}.
*/
export function generateSdk(root: OpRootNode, options: SdkCodegenOptions = {}): string {

@@ -101,3 +116,3 @@ const lines: string[] = [];

const types = collectTypes(root, options.modelsWithInput, options.modelsWithOutput, includeInternal);
const clientClassName = deriveClientClassName(root.file);
const clientClassName = options.clientClassName ?? deriveClientClassName(root.file);

@@ -214,2 +229,31 @@ // Type-only imports

/**
* Render the method-block lines for an operation file as if they were declared inside a
* client class. Returns one consolidated array of strings (each pre-indented for class body
* level, with leading blank lines between methods) plus the set of method names emitted —
* the caller uses the names to detect cross-file collisions when multiple files contribute
* to the same area-level client.
*
* Skips operations marked `internal` unless `options.includeInternal` is set.
*/
export function generateClientMethods(
root: OpRootNode,
options: SdkCodegenOptions,
): { lines: string[]; methodNames: string[] } {
const lines: string[] = [];
const methodNames: string[] = [];
const includeInternal = options.includeInternal ?? false;
for (const route of root.routes) {
for (const op of route.operations) {
const mods = resolveModifiers(route, op);
if (!includeInternal && mods.includes('internal')) continue;
lines.push('');
if (mods.includes('deprecated')) lines.push(' /** @deprecated */');
lines.push(...generateMethod(route, op, root.file, options));
methodNames.push(deriveMethodName(op, route));
}
}
return { lines, methodNames };
}
// ─── Method generation ────────────────────────────────────────────────────

@@ -523,2 +567,3 @@

/** Derive a client class name from a `.ck` file path, e.g. `users.ck` → `UsersClient`. Used for legacy flat (no-area) files. */
export function deriveClientClassName(file: string): string {

@@ -528,2 +573,3 @@ return `${deriveBaseName(file)}Client`;

/** Camel-cased property name for a flat client on the SDK aggregator, e.g. `users.ck` → `users`. */
export function deriveClientPropertyName(file: string): string {

@@ -534,2 +580,44 @@ const base = deriveBaseName(file);

/**
* Pull `area` / `subarea` from a file's `root.meta` (set via `options { keys: { ... } }`).
* Both are optional. `area` drives top-level SDK grouping; `subarea` drives nesting under
* an area's client class.
*/
export function getAreaSubarea(root: OpRootNode): { area?: string; subarea?: string } {
return { area: root.meta?.area, subarea: root.meta?.subarea };
}
function pascal(value: string): string {
return value
.split(/[-_\s]+/)
.filter(Boolean)
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
.join('');
}
function camel(value: string): string {
const p = pascal(value);
return p.charAt(0).toLowerCase() + p.slice(1);
}
/** Class name for the area-level client, e.g. `area=identity` → `IdentityClient`. */
export function deriveAreaClientClassName(area: string): string {
return `${pascal(area)}Client`;
}
/** Property name on the SDK aggregator for an area, e.g. `area=identity` → `identity`. */
export function deriveAreaPropertyName(area: string): string {
return camel(area);
}
/** Class name for a leaf subarea client, e.g. `(identity, invitations)` → `IdentityInvitationsClient`. */
export function deriveSubareaClientClassName(area: string, subarea: string): string {
return `${pascal(area)}${pascal(subarea)}Client`;
}
/** Property name on the area client for a subarea, e.g. `subarea=invitations` → `invitations`. */
export function deriveSubareaPropertyName(subarea: string): string {
return camel(subarea);
}
// ─── Type collection ──────────────────────────────────────────────────────

@@ -880,21 +968,181 @@

/**
* Reference to a per-file leaf client emitted to its own `*.client.ts`. Used by the
* aggregator to import the class and wire it as either a top-level `sdk.<prop>` or a
* nested `sdk.<area>.<subarea>` property.
*/
export interface SdkClientInfo {
/** Client class name (e.g. `UsersClient`, `IdentityInvitationsClient`). */
className: string;
/** Property name to expose this client under (e.g. `users`, `invitations`). */
propertyName: string;
/** Module specifier for the leaf file, relative to `sdk.ts` and `.js`-suffixed. */
importPath: string;
}
/** Generate the sdk.ts aggregator that wraps all clients into a single Sdk class. */
export function generateSdkAggregator(clients: SdkClientInfo[], sdkOptionsImportPath = './sdk-options.js', sdkClassName = 'Sdk'): string {
/**
* One area-level (no-subarea) `.ck` file whose methods are inlined directly into the
* generated `<Area>Client` class instead of getting a standalone `*.client.ts`.
*/
export interface SdkAreaInlineFile {
/** Parsed AST. */
root: OpRootNode;
/** Codegen options for this file (must have `outPath` pointing at the SDK aggregator file so type-import paths resolve correctly). */
codegenOptions: SdkCodegenOptions;
}
/** A grouping of files that share the same `keys.area`. */
export interface SdkAreaInfo {
area: string;
/** Files for which all methods are inlined into the area client (no subarea). */
inlineFiles: SdkAreaInlineFile[];
/** Per-file leaf clients exposed as named properties on the area client. */
subareaClients: { propertyName: string; client: SdkClientInfo }[];
}
export interface SdkAggregatorInput {
/** Files with no `keys.area` — kept as flat `Sdk.<filename>` properties (legacy behavior). */
topLevelClients: SdkClientInfo[];
/** One entry per `keys.area`. */
areas: SdkAreaInfo[];
/** Path to `sdk-options.ts` to import `SdkOptions`/`createSdkFetch`/etc. from. */
sdkOptionsImportPath?: string;
/** Name of the top-level aggregator class. Defaults to `Sdk`. */
sdkClassName?: string;
}
/**
* Generate the SDK aggregator (`sdk.ts`) — the entry-point file consumers import.
*
* Emits one `<Area>Client` class per area (combining inlined area-level methods with
* subarea property wiring), one `class Sdk` exposing area properties + flat top-level
* properties, plus all imports needed by the inlined methods.
*
* @throws if two area-level files in the same area produce the same method name.
*/
export function generateSdkAggregator(input: SdkAggregatorInput): string {
const sdkOptionsImportPath = input.sdkOptionsImportPath ?? './sdk-options.js';
const sdkClassName = input.sdkClassName ?? 'Sdk';
// ── Pre-render inline method blocks per area (also collects type/runtime needs) ──
const inlinedByArea = new Map<string, { lines: string[]; methodNames: Set<string> }>();
const typesByImportPath = new Map<string, Set<string>>(); // path → set of type names
const unresolvedTypes = new Set<string>();
let needsJson = false;
let needsBigIntReplacer = false;
let needsBigIntReviver = false;
let needsQueryString = false;
for (const area of input.areas) {
const collected: string[] = [];
const seenMethods = new Set<string>();
for (const inline of area.inlineFiles) {
const includeInternal = inline.codegenOptions.includeInternal ?? false;
// Collect method names + source lines
const { lines: methodLines, methodNames } = generateClientMethods(inline.root, inline.codegenOptions);
for (const name of methodNames) {
if (seenMethods.has(name)) {
throw new Error(
`[sdk] duplicate method '${name}' in area '${area.area}': two area-level files contribute the same method. Disambiguate via 'sdk:' or move one into a subarea.`,
);
}
seenMethods.add(name);
}
collected.push(...methodLines);
// Side info for imports
if (sdkNeedsJson(inline.root, includeInternal)) needsJson = true;
if (sdkNeedsBigIntReplacer(inline.root, includeInternal)) needsBigIntReplacer = true;
if (sdkNeedsBigIntReviver(inline.root, includeInternal)) needsBigIntReviver = true;
if (sdkNeedsQueryString(inline.root, includeInternal)) needsQueryString = true;
// Type imports: rebuild what generateTypeImports would produce, but key by import path so multiple files merge
const typesForFile = collectTypes(inline.root, inline.codegenOptions.modelsWithInput, inline.codegenOptions.modelsWithOutput, includeInternal);
const { modelOutPaths, outPath } = inline.codegenOptions;
if (modelOutPaths && outPath) {
const fromDir = dirname(outPath);
for (const t of typesForFile) {
const typeOutPath = modelOutPaths.get(t);
if (typeOutPath) {
let rel = relative(fromDir, typeOutPath).replace(/\.ts$/, '.js');
if (!rel.startsWith('.')) rel = './' + rel;
const set = typesByImportPath.get(rel) ?? new Set();
set.add(t);
typesByImportPath.set(rel, set);
} else {
unresolvedTypes.add(t);
}
}
}
}
inlinedByArea.set(area.area, { lines: collected, methodNames: seenMethods });
}
// ── Imports ──
const lines: string[] = [];
const jsonImport = needsJson ? ', JsonValue' : '';
lines.push(`import type { SdkFetch${jsonImport} } from '${sdkOptionsImportPath}';`);
const valueImports: string[] = [];
if (needsBigIntReplacer) valueImports.push('bigIntReplacer');
if (needsBigIntReviver) valueImports.push('parseJson');
if (needsQueryString) valueImports.push('buildQueryString');
if (valueImports.length > 0) {
lines.push(`import { ${valueImports.join(', ')} } from '${sdkOptionsImportPath}';`);
}
lines.push(`import type { SdkOptions } from '${sdkOptionsImportPath}';`);
lines.push(`import { createSdkFetch } from '${sdkOptionsImportPath}';`);
for (const c of clients) {
// Type imports inlined from area-level files
const typeImportPaths = [...typesByImportPath.keys()].sort();
for (const path of typeImportPaths) {
const names = [...typesByImportPath.get(path)!].sort();
lines.push(`import type { ${names.join(', ')} } from '${path}';`);
}
for (const t of [...unresolvedTypes].sort()) {
lines.push(`import type { ${t} } from './${pascalToDotCase(t)}.js';`);
}
// Leaf client imports (top-level + subarea)
const importedClients = new Set<string>();
const pushClientImport = (c: SdkClientInfo): void => {
const key = `${c.className}|${c.importPath}`;
if (importedClients.has(key)) return;
importedClients.add(key);
lines.push(`import { ${c.className} } from '${c.importPath}';`);
};
for (const c of input.topLevelClients) pushClientImport(c);
for (const area of input.areas) {
for (const sc of area.subareaClients) pushClientImport(sc.client);
}
lines.push('');
// ── <Area>Client classes ──
for (const area of input.areas) {
const areaClassName = deriveAreaClientClassName(area.area);
const inlined = inlinedByArea.get(area.area)!;
lines.push(`class ${areaClassName} {`);
// Subarea property declarations
for (const sc of area.subareaClients) {
lines.push(` readonly ${sc.propertyName}: ${sc.client.className};`);
}
if (area.subareaClients.length > 0) lines.push('');
// Constructor
if (inlined.lines.length > 0 || area.subareaClients.length > 0) {
const fetchModifier = inlined.lines.length > 0 ? 'private ' : '';
lines.push(` constructor(${fetchModifier}fetch: SdkFetch) {`);
for (const sc of area.subareaClients) {
lines.push(` this.${sc.propertyName} = new ${sc.client.className}(fetch);`);
}
lines.push(' }');
}
// Inlined methods (already class-body indented)
for (const ln of inlined.lines) lines.push(ln);
lines.push('}');
lines.push('');
}
// ── Sdk aggregator ──
lines.push(`export class ${sdkClassName} {`);
for (const c of clients) {
for (const area of input.areas) {
lines.push(` readonly ${deriveAreaPropertyName(area.area)}: ${deriveAreaClientClassName(area.area)};`);
}
for (const c of input.topLevelClients) {
lines.push(` readonly ${c.propertyName}: ${c.className};`);

@@ -905,3 +1153,6 @@ }

lines.push(' const sdkFetch = options.fetch ?? createSdkFetch(options);');
for (const c of clients) {
for (const area of input.areas) {
lines.push(` this.${deriveAreaPropertyName(area.area)} = new ${deriveAreaClientClassName(area.area)}(sdkFetch);`);
}
for (const c of input.topLevelClients) {
lines.push(` this.${c.propertyName} = new ${c.className}(sdkFetch);`);

@@ -908,0 +1159,0 @@ }

@@ -11,3 +11,8 @@ import { resolve, join, relative, dirname, basename } from 'node:path';

deriveClientPropertyName,
deriveSubareaClientClassName,
deriveSubareaPropertyName,
getAreaSubarea,
hasPublicOperations,
type SdkClientInfo,
type SdkAreaInfo,
} from './codegen-sdk.js';

@@ -68,5 +73,5 @@ import { generatePlainTypes } from './codegen-plain-types.js';

sdk?: string;
/** Path template for SDK type files. Supports {filename}, {dir}, {area}. */
/** Path template for SDK type files. Supports {filename}, {dir}, {area}, {subarea}. */
types?: string;
/** Path template for client class files. Supports {filename}, {dir}, {area}. */
/** Path template for client class files. Supports {filename}, {dir}, {area}, {subarea}. */
clients?: string;

@@ -238,2 +243,13 @@ };

// ── SDK clients ──
// Group opRoots by (area, subarea):
// - area + subarea → leaf client emitted as <Area><Subarea>Client in its own file
// - area only → no standalone file; methods inlined into <Area>Client in sdk.ts
// - neither → flat top-level client (legacy behavior)
interface AreaBucket {
leaves: { ast: typeof inputs.opRoots[number]; outPath: string; subarea: string }[];
inlineRoots: typeof inputs.opRoots[number][];
}
const areaBuckets = new Map<string, AreaBucket>();
const topLevelEntries: { ast: typeof inputs.opRoots[number]; outPath: string }[] = [];
if (config.output?.clients) {

@@ -243,12 +259,26 @@ for (const ast of inputs.opRoots) {

if (!sdkOutPath || !hasPublicOperations(ast, config.includeInternal)) continue;
sdkClientInfos.push({
outPath: sdkOutPath,
className: deriveClientClassName(ast.file),
propertyName: deriveClientPropertyName(ast.file),
});
const { area, subarea } = getAreaSubarea(ast);
if (area && subarea) {
const bucket = areaBuckets.get(area) ?? { leaves: [], inlineRoots: [] };
bucket.leaves.push({ ast, outPath: sdkOutPath, subarea });
areaBuckets.set(area, bucket);
} else if (area) {
const bucket = areaBuckets.get(area) ?? { leaves: [], inlineRoots: [] };
bucket.inlineRoots.push(ast);
areaBuckets.set(area, bucket);
} else {
topLevelEntries.push({ ast, outPath: sdkOutPath });
}
}
// Emit per-file clients for leaves (subarea) and top-level (no area). Area-only files are inlined later.
for (const { ast, outPath, subarea } of [...areaBuckets.entries()].flatMap(([area, b]) => b.leaves.map(l => ({ ...l, area })))) {
const className = deriveSubareaClientClassName((ast.meta?.area as string) ?? '', subarea);
sdkClientInfos.push({ outPath, className, propertyName: deriveSubareaPropertyName(subarea) });
emitFile(
sdkOutPath,
outPath,
generateSdk(ast, {
typeImportPathTemplate: undefined,
outPath: sdkOutPath,
outPath,
modelOutPaths: sdkModelOutPaths,

@@ -259,5 +289,22 @@ sdkOptionsPath,

includeInternal: config.includeInternal,
clientClassName: className,
}),
);
}
for (const { ast, outPath } of topLevelEntries) {
const className = deriveClientClassName(ast.file);
sdkClientInfos.push({ outPath, className, propertyName: deriveClientPropertyName(ast.file) });
emitFile(
outPath,
generateSdk(ast, {
typeImportPathTemplate: undefined,
outPath,
modelOutPaths: sdkModelOutPaths,
sdkOptionsPath,
modelsWithInput,
modelsWithOutput,
includeInternal: config.includeInternal,
}),
);
}
}

@@ -269,10 +316,7 @@

// ── sdk.ts aggregator ──
if (sdkClientInfos.length > 0) {
const hasAnything = sdkClientInfos.length > 0 || areaBuckets.size > 0;
if (hasAnything) {
const sdkEntryDir = dirname(sdkEntryPath);
const clients = sdkClientInfos.map(c => {
let rel = relative(sdkEntryDir, c.outPath).replace(/\.ts$/, '.js');
if (!rel.startsWith('.')) rel = './' + rel;
return { className: c.className, propertyName: c.propertyName, importPath: rel };
});
const sdkOptionsRel = relative(sdkEntryDir, sdkOptionsPath).replace(/\.ts$/, '.js');
const sdkOptionsImportPath = sdkOptionsRel.startsWith('.') ? sdkOptionsRel : './' + sdkOptionsRel;
const sdkClassName = sdkName

@@ -284,3 +328,55 @@ ? sdkName

: 'Sdk';
emitFile(sdkEntryPath, generateSdkAggregator(clients, sdkOptionsRel.startsWith('.') ? sdkOptionsRel : './' + sdkOptionsRel, sdkClassName));
const toClientImport = (info: { outPath: string; className: string; propertyName: string }): SdkClientInfo => {
let rel = relative(sdkEntryDir, info.outPath).replace(/\.ts$/, '.js');
if (!rel.startsWith('.')) rel = './' + rel;
return { className: info.className, propertyName: info.propertyName, importPath: rel };
};
const topLevelClients: SdkClientInfo[] = topLevelEntries.map(e => ({
className: deriveClientClassName(e.ast.file),
propertyName: deriveClientPropertyName(e.ast.file),
importPath: (() => {
const rel = relative(sdkEntryDir, e.outPath).replace(/\.ts$/, '.js');
return rel.startsWith('.') ? rel : './' + rel;
})(),
}));
const areas: SdkAreaInfo[] = [...areaBuckets.entries()]
.sort(([a], [b]) => a.localeCompare(b))
.map(([area, bucket]) => ({
area,
inlineFiles: bucket.inlineRoots.map(root => ({
root,
codegenOptions: {
typeImportPathTemplate: undefined,
outPath: sdkEntryPath,
modelOutPaths: sdkModelOutPaths,
sdkOptionsPath,
modelsWithInput,
modelsWithOutput,
includeInternal: config.includeInternal,
},
})),
subareaClients: bucket.leaves
.sort((a, b) => a.subarea.localeCompare(b.subarea))
.map(l => ({
propertyName: deriveSubareaPropertyName(l.subarea),
client: toClientImport({
outPath: l.outPath,
className: deriveSubareaClientClassName(area, l.subarea),
propertyName: deriveSubareaPropertyName(l.subarea),
}),
})),
}));
emitFile(
sdkEntryPath,
generateSdkAggregator({
topLevelClients,
areas,
sdkOptionsImportPath,
sdkClassName,
}),
);
}

@@ -294,3 +390,3 @@

const rootExports: string[] = [`export * from './${basename(sdkOptionsPath).replace(/\.ts$/, '.js')}';`];
if (sdkClientInfos.length > 0) {
if (hasAnything) {
rootExports.push(`export * from './${basename(sdkEntryPath).replace(/\.ts$/, '.js')}';`);

@@ -414,2 +510,7 @@ }

/**
* Build a `@contractkit/plugin-typescript` instance with explicit configuration, for
* programmatic use (tests, custom build scripts). Prefer the default export when loading
* via `contractkit.config.json`.
*/
export function createTypescriptPlugin(config: TypescriptPluginConfig, rootDir: string): ContractKitPlugin {

@@ -416,0 +517,0 @@ return {

@@ -193,1 +193,99 @@ import { describe, it, expect } from 'vitest';

});
// ─── SDK pipeline: area / subarea grouping ─────────────────────────────────
describe('createTypescriptPlugin (sdk) — area / subarea grouping', () => {
function findEmitted(emitted: Map<string, string>, suffix: string): string | undefined {
for (const [path, content] of emitted) {
if (path.endsWith(suffix)) return content;
}
return undefined;
}
it('emits a leaf <Area><Subarea>Client and nests it under <Area>Client in sdk.ts', async () => {
const plugin = createTypescriptPlugin({ sdk: { output: { sdk: 'sdk.ts', clients: '{area}/{subarea}.client.ts' } } }, '/project');
const ctx = makeCtx('/project');
const root = opRoot(
[opRoute('/identity/invitations', [opOperation('post', { sdk: 'createInvitation', responses: [opResponse(201)] })])],
'/project/contracts/identity-invitations.ck',
{ area: 'identity', subarea: 'invitations' },
);
await plugin.generateTargets!(inputs([root]), ctx);
// Leaf client emitted at the {area}/{subarea}.client.ts path
const leaf = findEmitted(ctx.emitted, 'identity/invitations.client.ts');
expect(leaf).toBeDefined();
expect(leaf!).toContain('export class IdentityInvitationsClient');
expect(leaf!).toContain('async createInvitation');
// Aggregator declares IdentityClient + Sdk wiring
const sdk = findEmitted(ctx.emitted, '/sdk.ts');
expect(sdk).toBeDefined();
expect(sdk!).toContain('class IdentityClient {');
expect(sdk!).toContain('readonly invitations: IdentityInvitationsClient');
expect(sdk!).toContain('this.invitations = new IdentityInvitationsClient(fetch)');
expect(sdk!).toContain('export class Sdk {');
expect(sdk!).toContain('readonly identity: IdentityClient');
expect(sdk!).toContain('this.identity = new IdentityClient(sdkFetch)');
});
it('does NOT emit a standalone client file for an area-level (no-subarea) input — methods inline into sdk.ts', async () => {
const plugin = createTypescriptPlugin({ sdk: { output: { sdk: 'sdk.ts', clients: '{area}/{filename}.client.ts' } } }, '/project');
const ctx = makeCtx('/project');
const root = opRoot(
[opRoute('/me', [opOperation('get', { sdk: 'getCurrentUser', responses: [opResponse(200)] })])],
'/project/contracts/identity-me.ck',
{ area: 'identity' },
);
await plugin.generateTargets!(inputs([root]), ctx);
// No per-file leaf for the area-level input
for (const path of ctx.emitted.keys()) {
expect(path).not.toMatch(/identity[/-]me\.client\.ts$/);
}
// Methods land directly on IdentityClient inside sdk.ts
const sdk = findEmitted(ctx.emitted, '/sdk.ts');
expect(sdk).toBeDefined();
expect(sdk!).toContain('class IdentityClient {');
expect(sdk!).toContain('async getCurrentUser');
});
it('mixes area-level inline methods with subarea property wiring on the same area client', async () => {
const plugin = createTypescriptPlugin({ sdk: { output: { sdk: 'sdk.ts', clients: '{area}/{filename}.client.ts' } } }, '/project');
const ctx = makeCtx('/project');
const me = opRoot(
[opRoute('/me', [opOperation('get', { sdk: 'getCurrentUser', responses: [opResponse(200)] })])],
'/project/contracts/identity-me.ck',
{ area: 'identity' },
);
const invitations = opRoot(
[opRoute('/identity/invitations', [opOperation('post', { sdk: 'createInvitation', responses: [opResponse(201)] })])],
'/project/contracts/identity-invitations.ck',
{ area: 'identity', subarea: 'invitations' },
);
await plugin.generateTargets!(inputs([me, invitations]), ctx);
const sdk = findEmitted(ctx.emitted, '/sdk.ts')!;
expect(sdk).toContain('class IdentityClient {');
expect(sdk).toContain('readonly invitations: IdentityInvitationsClient');
expect(sdk).toContain('async getCurrentUser');
});
it('preserves legacy flat wiring for files with no area', async () => {
const plugin = createTypescriptPlugin({ sdk: { output: { sdk: 'sdk.ts', clients: '{filename}.client.ts' } } }, '/project');
const ctx = makeCtx('/project');
const root = opRoot(
[opRoute('/webhooks', [opOperation('post', { sdk: 'sendWebhook', responses: [opResponse(202)] })])],
'/project/contracts/webhooks.ck',
);
await plugin.generateTargets!(inputs([root]), ctx);
const sdk = findEmitted(ctx.emitted, '/sdk.ts')!;
// No <Area>Client wrapper; flat property on Sdk
expect(sdk).toContain('export class Sdk {');
expect(sdk).toContain('readonly webhooks: WebhooksClient');
expect(sdk).toContain('this.webhooks = new WebhooksClient(sdkFetch)');
expect(sdk).not.toMatch(/class \w+Client \{/m);
});
});

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display