@contractkit/plugin-typescript
Advanced tools
| > @contractkit/plugin-typescript@0.18.0 build:ci /home/runner/work/ContractKit/ContractKit/packages/plugin-typescript | ||
| > @contractkit/plugin-typescript@0.19.0 build:ci /home/runner/work/ContractKit/ContractKit/packages/plugin-typescript | ||
| > eslint --max-warnings=0 && pnpm run build | ||
| > @contractkit/plugin-typescript@0.18.0 build /home/runner/work/ContractKit/ContractKit/packages/plugin-typescript | ||
| > @contractkit/plugin-typescript@0.19.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 @@ | ||
| [34mESM[39m Build start | ||
| [32mESM[39m [1mdist/index.js [22m[32m135.58 KB[39m | ||
| [32mESM[39m [1mdist/index.js.map [22m[32m298.86 KB[39m | ||
| [32mESM[39m ⚡️ Build success in 308ms | ||
| [32mESM[39m [1mdist/index.js [22m[32m145.87 KB[39m | ||
| [32mESM[39m [1mdist/index.js.map [22m[32m317.00 KB[39m | ||
| [32mESM[39m ⚡️ Build success in 449ms | ||
| [34mDTS[39m Build start | ||
| [32mDTS[39m ⚡️ Build success in 5090ms | ||
| [32mDTS[39m [1mdist/index.d.ts [22m[32m3.67 KB[39m | ||
| [32mDTS[39m ⚡️ Build success in 6617ms | ||
| [32mDTS[39m [1mdist/index.d.ts [22m[32m1.82 KB[39m |
| > @contractkit/plugin-typescript@0.18.0 test:ci /home/runner/work/ContractKit/ContractKit/packages/plugin-typescript | ||
| > @contractkit/plugin-typescript@0.19.0 test:ci /home/runner/work/ContractKit/ContractKit/packages/plugin-typescript | ||
| > vitest run --coverage | ||
@@ -9,13 +9,13 @@ | ||
| [32m✓[39m tests/codegen-operation.test.ts [2m([22m[2m88 tests[22m[2m)[22m[32m 53[2mms[22m[39m | ||
| [32m✓[39m tests/codegen-contract.test.ts [2m([22m[2m124 tests[22m[2m)[22m[32m 120[2mms[22m[39m | ||
| [32m✓[39m tests/codegen-sdk.test.ts [2m([22m[2m131 tests[22m[2m)[22m[32m 76[2mms[22m[39m | ||
| [32m✓[39m tests/codegen-plain-types.test.ts [2m([22m[2m61 tests[22m[2m)[22m[32m 37[2mms[22m[39m | ||
| [32m✓[39m tests/pipeline.test.ts [2m([22m[2m25 tests[22m[2m)[22m[32m 142[2mms[22m[39m | ||
| [32m✓[39m tests/codegen-server.test.ts [2m([22m[2m19 tests[22m[2m)[22m[32m 24[2mms[22m[39m | ||
| [32m✓[39m tests/codegen-contract.test.ts [2m([22m[2m124 tests[22m[2m)[22m[32m 300[2mms[22m[39m | ||
| [32m✓[39m tests/codegen-operation.test.ts [2m([22m[2m88 tests[22m[2m)[22m[32m 264[2mms[22m[39m | ||
| [32m✓[39m tests/codegen-sdk.test.ts [2m([22m[2m131 tests[22m[2m)[22m[32m 201[2mms[22m[39m | ||
| [32m✓[39m tests/codegen-plain-types.test.ts [2m([22m[2m61 tests[22m[2m)[22m[32m 34[2mms[22m[39m | ||
| [32m✓[39m tests/pipeline.test.ts [2m([22m[2m25 tests[22m[2m)[22m[32m 198[2mms[22m[39m | ||
| [32m✓[39m tests/codegen-server.test.ts [2m([22m[2m19 tests[22m[2m)[22m[32m 28[2mms[22m[39m | ||
| [2m Test Files [22m [1m[32m6 passed[39m[22m[90m (6)[39m | ||
| [2m Tests [22m [1m[32m448 passed[39m[22m[90m (448)[39m | ||
| [2m Start at [22m 16:22:10 | ||
| [2m Duration [22m 6.11s[2m (transform 5.19s, setup 0ms, import 12.92s, tests 452ms, environment 1ms)[22m | ||
| [2m Start at [22m 11:09:07 | ||
| [2m Duration [22m 7.56s[2m (transform 4.43s, setup 0ms, import 15.37s, tests 1.02s, environment 1ms)[22m | ||
@@ -26,4 +26,4 @@ [34m % [39m[2mCoverage report from [22m[33mv8[39m | ||
| -------------------|---------|----------|---------|---------|------------------- | ||
| All files | 80.98 | 76.16 | 84.16 | 83.21 | | ||
| src | 80.77 | 75.84 | 83.76 | 82.97 | | ||
| All files | 79.96 | 75.35 | 83.41 | 82.44 | | ||
| src | 79.74 | 75.03 | 82.97 | 82.19 | | ||
| ...n-contract.ts | 87.83 | 82.12 | 90.08 | 88.96 | ...1069,1074-1075 | ||
@@ -33,3 +33,3 @@ ...-operation.ts | 78.18 | 74.26 | 78.08 | 79.69 | ...68-679,684-685 | ||
| 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 | ||
| index.ts | 58.17 | 47.22 | 63.04 | 62.78 | ...48-751,757-776 | ||
| path-utils.ts | 36.52 | 27.53 | 75 | 38.14 | ...32-136,147-187 | ||
@@ -36,0 +36,0 @@ ts-render.ts | 82.19 | 85.39 | 72.22 | 87.09 | 56,90,126,157-165 |
+11
-0
| # @contractkit/contractkit-plugin-typescript | ||
| ## 0.19.0 | ||
| ### Minor Changes | ||
| - 10ca07b: Add per-output incremental caching to the Bruno, Python, and TypeScript plugins. Editing a single contract or operation no longer regenerates every output file — only the units whose transitive inputs actually changed are re-rendered, with the rest reused from a per-plugin manifest. `@contractkit/core` exposes the shared utility (`runIncrementalCodegen`, `parseIncrementalManifest`, `hashFingerprint`, `collectTransitiveModelRefs`, manifest types) for plugin authors. `PluginContext` gains a `cacheEnabled` flag so plugins can honor `--force` / `cache: false`. | ||
| ### Patch Changes | ||
| - Updated dependencies [10ca07b] | ||
| - @contractkit/core@0.15.0 | ||
| ## 0.18.0 | ||
@@ -4,0 +15,0 @@ |
+20
-20
@@ -26,5 +26,5 @@ | ||
| <div class='fl pad1y space-right2'> | ||
| <span class="strong">80.98% </span> | ||
| <span class="strong">79.96% </span> | ||
| <span class="quiet">Statements</span> | ||
| <span class='fraction'>1921/2372</span> | ||
| <span class='fraction'>1992/2491</span> | ||
| </div> | ||
@@ -34,5 +34,5 @@ | ||
| <div class='fl pad1y space-right2'> | ||
| <span class="strong">76.16% </span> | ||
| <span class="strong">75.35% </span> | ||
| <span class="quiet">Branches</span> | ||
| <span class='fraction'>1198/1573</span> | ||
| <span class='fraction'>1220/1619</span> | ||
| </div> | ||
@@ -42,5 +42,5 @@ | ||
| <div class='fl pad1y space-right2'> | ||
| <span class="strong">84.16% </span> | ||
| <span class="strong">83.41% </span> | ||
| <span class="quiet">Functions</span> | ||
| <span class='fraction'>319/379</span> | ||
| <span class='fraction'>332/398</span> | ||
| </div> | ||
@@ -50,5 +50,5 @@ | ||
| <div class='fl pad1y space-right2'> | ||
| <span class="strong">83.21% </span> | ||
| <span class="strong">82.44% </span> | ||
| <span class="quiet">Lines</span> | ||
| <span class='fraction'>1681/2020</span> | ||
| <span class='fraction'>1742/2113</span> | ||
| </div> | ||
@@ -68,3 +68,3 @@ | ||
| </div> | ||
| <div class='status-line high'></div> | ||
| <div class='status-line medium'></div> | ||
| <div class="pad1"> | ||
@@ -87,14 +87,14 @@ <table class="coverage-summary"> | ||
| <tbody><tr> | ||
| <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 class="file medium" data-value="src"><a href="src/index.html">src</a></td> | ||
| <td data-value="79.74" class="pic medium"> | ||
| <div class="chart"><div class="cover-fill" style="width: 79%"></div><div class="cover-empty" style="width: 21%"></div></div> | ||
| </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="79.74" class="pct medium">79.74%</td> | ||
| <td data-value="2439" class="abs medium">1945/2439</td> | ||
| <td data-value="75.03" class="pct medium">75.03%</td> | ||
| <td data-value="1586" class="abs medium">1190/1586</td> | ||
| <td data-value="82.97" class="pct high">82.97%</td> | ||
| <td data-value="1974" class="abs high">1638/1974</td> | ||
| <td data-value="370" class="abs high">307/370</td> | ||
| <td data-value="82.19" class="pct high">82.19%</td> | ||
| <td data-value="2067" class="abs high">1699/2067</td> | ||
| </tr> | ||
@@ -125,3 +125,3 @@ | ||
| <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> | ||
| at 2026-05-06T16:22:17.029Z | ||
| at 2026-05-07T11:09:15.235Z | ||
| </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-06T16:22:17.029Z | ||
| at 2026-05-07T11:09:15.235Z | ||
| </div> | ||
@@ -988,0 +988,0 @@ <script src="../prettify.js"></script> |
+20
-20
@@ -26,5 +26,5 @@ | ||
| <div class='fl pad1y space-right2'> | ||
| <span class="strong">80.77% </span> | ||
| <span class="strong">79.74% </span> | ||
| <span class="quiet">Statements</span> | ||
| <span class='fraction'>1874/2320</span> | ||
| <span class='fraction'>1945/2439</span> | ||
| </div> | ||
@@ -34,5 +34,5 @@ | ||
| <div class='fl pad1y space-right2'> | ||
| <span class="strong">75.84% </span> | ||
| <span class="strong">75.03% </span> | ||
| <span class="quiet">Branches</span> | ||
| <span class='fraction'>1168/1540</span> | ||
| <span class='fraction'>1190/1586</span> | ||
| </div> | ||
@@ -42,5 +42,5 @@ | ||
| <div class='fl pad1y space-right2'> | ||
| <span class="strong">83.76% </span> | ||
| <span class="strong">82.97% </span> | ||
| <span class="quiet">Functions</span> | ||
| <span class='fraction'>294/351</span> | ||
| <span class='fraction'>307/370</span> | ||
| </div> | ||
@@ -50,5 +50,5 @@ | ||
| <div class='fl pad1y space-right2'> | ||
| <span class="strong">82.97% </span> | ||
| <span class="strong">82.19% </span> | ||
| <span class="quiet">Lines</span> | ||
| <span class='fraction'>1638/1974</span> | ||
| <span class='fraction'>1699/2067</span> | ||
| </div> | ||
@@ -68,3 +68,3 @@ | ||
| </div> | ||
| <div class='status-line high'></div> | ||
| <div class='status-line medium'></div> | ||
| <div class="pad1"> | ||
@@ -148,13 +148,13 @@ <table class="coverage-summary"> | ||
| <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 data-value="58.17" class="pic medium"> | ||
| <div class="chart"><div class="cover-fill" style="width: 58%"></div><div class="cover-empty" style="width: 42%"></div></div> | ||
| </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> | ||
| <td data-value="58.17" class="pct medium">58.17%</td> | ||
| <td data-value="318" class="abs medium">185/318</td> | ||
| <td data-value="47.22" class="pct low">47.22%</td> | ||
| <td data-value="144" class="abs low">68/144</td> | ||
| <td data-value="63.04" class="pct medium">63.04%</td> | ||
| <td data-value="46" class="abs medium">29/46</td> | ||
| <td data-value="62.78" class="pct medium">62.78%</td> | ||
| <td data-value="266" class="abs medium">167/266</td> | ||
| </tr> | ||
@@ -200,3 +200,3 @@ | ||
| <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> | ||
| at 2026-05-06T16:22:17.029Z | ||
| at 2026-05-07T11:09:15.235Z | ||
| </div> | ||
@@ -203,0 +203,0 @@ <script src="../prettify.js"></script> |
@@ -637,3 +637,3 @@ | ||
| <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> | ||
| at 2026-05-06T16:22:17.029Z | ||
| at 2026-05-07T11:09:15.235Z | ||
| </div> | ||
@@ -640,0 +640,0 @@ <script src="../prettify.js"></script> |
@@ -580,3 +580,3 @@ | ||
| <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> | ||
| at 2026-05-06T16:22:17.029Z | ||
| at 2026-05-07T11:09:15.235Z | ||
| </div> | ||
@@ -583,0 +583,0 @@ <script src="../prettify.js"></script> |
@@ -814,3 +814,3 @@ | ||
| <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> | ||
| at 2026-05-06T16:22:17.029Z | ||
| at 2026-05-07T11:09:15.235Z | ||
| </div> | ||
@@ -817,0 +817,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-06T16:22:17.029Z | ||
| at 2026-05-07T11:09:15.235Z | ||
| </div> | ||
@@ -107,0 +107,0 @@ <script src="../prettify.js"></script> |
+8
-41
@@ -5,79 +5,46 @@ import type { ContractKitPlugin } from '@contractkit/core'; | ||
| baseDir?: string; | ||
| /** | ||
| * When true, `output.types` emits Zod schema files (via `generateContract`). | ||
| * When false/omitted, `output.types` emits plain TypeScript interfaces. | ||
| */ | ||
| /** When true, `output.types` emits Zod schema files (via `generateContract`). When false/omitted, emits plain TypeScript. */ | ||
| zod?: boolean; | ||
| output?: { | ||
| /** Path template for Koa router files. Supports {filename}, {dir}, {area}. Default: `{filename}.router.ts`. */ | ||
| /** Path template for Koa router files. Supports {filename}, {dir}, {area}. */ | ||
| routes?: string; | ||
| /** | ||
| * Path template for type/schema files. Supports {filename}, {dir}, {area}. | ||
| * Generates Zod schemas when `zod: true`, otherwise plain TypeScript interfaces. | ||
| */ | ||
| /** Path template for type/schema files. Supports {filename}, {dir}, {area}. */ | ||
| types?: string; | ||
| }; | ||
| /** Import path template for service implementations. Supports {module}. */ | ||
| /** Import path template for service implementations. */ | ||
| servicePathTemplate?: string; | ||
| /** | ||
| * Whether to emit handlers for operations marked `internal`. Defaults to `true` — | ||
| * the server still needs routes for internal endpoints. Set to `false` to omit them. | ||
| */ | ||
| /** Whether to emit handlers for `internal` operations. Default true. */ | ||
| includeInternal?: boolean; | ||
| } | ||
| export interface SdkConfig { | ||
| /** Directory (relative to rootDir) where SDK files are written. Default: rootDir. */ | ||
| baseDir?: string; | ||
| /** Name used for the aggregator SDK class (e.g. "homegrown" → `HomegrownSdk`). */ | ||
| name?: string; | ||
| /** | ||
| * When true, `output.types` emits Zod schema files (via `generateContract`). | ||
| * When false/omitted, `output.types` emits plain TypeScript interfaces. | ||
| */ | ||
| zod?: boolean; | ||
| output?: { | ||
| /** Path template for the SDK aggregator file. Supports {name}. Default: `sdk.ts`. */ | ||
| sdk?: string; | ||
| /** Path template for SDK type files. Supports {filename}, {dir}, {area}, {subarea}. */ | ||
| types?: string; | ||
| /** Path template for client class files. Supports {filename}, {dir}, {area}, {subarea}. */ | ||
| clients?: string; | ||
| }; | ||
| /** | ||
| * Whether to emit SDK methods for operations marked `internal`. Defaults to `false` — | ||
| * internal ops are omitted from the SDK so consumers don't pick them up. Set to `true` | ||
| * for an internal-use SDK that should expose them. | ||
| */ | ||
| includeInternal?: boolean; | ||
| } | ||
| export interface ZodConfig { | ||
| /** Directory (relative to rootDir) where Zod schema files are written. Default: rootDir. */ | ||
| baseDir?: string; | ||
| /** Output path template. Supports {filename}, {dir}. Default: `{filename}.schema.ts` alongside source. */ | ||
| output?: string; | ||
| } | ||
| export interface TypesConfig { | ||
| /** Directory (relative to rootDir) where plain TypeScript type files are written. Default: rootDir. */ | ||
| baseDir?: string; | ||
| /** Output path template. Supports {filename}, {dir}. Default: `{filename}.types.ts` alongside source. */ | ||
| output?: string; | ||
| } | ||
| export interface TypescriptPluginConfig { | ||
| /** Generate Koa router files from `operation` declarations. */ | ||
| server?: ServerConfig; | ||
| /** Generate TypeScript SDK client files from `operation` declarations. */ | ||
| sdk?: SdkConfig; | ||
| /** Generate Zod schema files from `contract` declarations. */ | ||
| zod?: ZodConfig; | ||
| /** Generate plain TypeScript interface/type files from `contract` declarations (no Zod runtime). */ | ||
| types?: TypesConfig; | ||
| } | ||
| /** Bumped when the codegen output shape changes in a way that should bust every per-file fingerprint. */ | ||
| export declare const TYPESCRIPT_CODEGEN_VERSION = "1"; | ||
| declare const plugin: ContractKitPlugin; | ||
| 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`. | ||
| */ | ||
| /** Build a `@contractkit/plugin-typescript` instance with explicit configuration, for programmatic use. */ | ||
| 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;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"} | ||
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACR,iBAAiB,EAQpB,MAAM,mBAAmB,CAAC;AAqC3B,MAAM,WAAW,YAAY;IACzB,wFAAwF;IACxF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6HAA6H;IAC7H,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE;QACL,8EAA8E;QAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,+EAA+E;QAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,wDAAwD;IACxD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,SAAS;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE;QACL,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,SAAS;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACnC,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,KAAK,CAAC,EAAE,WAAW,CAAC;CACvB;AAID,yGAAyG;AACzG,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAM9C,QAAA,MAAM,MAAM,EAAE,iBAMb,CAAC;AAEF,eAAe,MAAM,CAAC;AAEtB,2GAA2G;AAC3G,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM,GAAG,iBAAiB,CAOzG"} |
+4
-4
| { | ||
| "name": "@contractkit/plugin-typescript", | ||
| "version": "0.18.0", | ||
| "version": "0.19.0", | ||
| "description": "ContractKit built-in plugin: TypeScript codegen (SDK clients, Koa routers, Zod schemas, plain types)", | ||
@@ -29,7 +29,7 @@ "author": { | ||
| "dependencies": { | ||
| "@contractkit/core": "0.14.0" | ||
| "@contractkit/core": "0.15.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@repo/config-typescript": "0.1.0", | ||
| "@repo/config-eslint": "0.3.1" | ||
| "@repo/config-eslint": "0.3.1", | ||
| "@repo/config-typescript": "0.1.0" | ||
| }, | ||
@@ -36,0 +36,0 @@ "scripts": { |
+481
-222
| import { resolve, join, relative, dirname, basename } from 'node:path'; | ||
| import { existsSync, readFileSync, rmSync, readdirSync, rmdirSync } from 'node:fs'; | ||
| import { generateContract } from './codegen-contract.js'; | ||
| import { generateOp } from './codegen-operation.js'; | ||
| import type { ContractKitPlugin } from '@contractkit/core'; | ||
| import type { | ||
| ContractKitPlugin, | ||
| PluginContext, | ||
| ContractRootNode, | ||
| OpRootNode, | ||
| ModelNode, | ||
| IncrementalManifest, | ||
| IncrementalUnit, | ||
| IncrementalOutputFile, | ||
| } from '@contractkit/core'; | ||
| import { | ||
| runIncrementalCodegen, | ||
| parseIncrementalManifest, | ||
| emptyIncrementalManifest, | ||
| hashFingerprint, | ||
| collectTransitiveModelRefs, | ||
| collectTypeRefs, | ||
| } from '@contractkit/core'; | ||
| import { | ||
| generateSdk, | ||
@@ -36,22 +54,13 @@ generateSdkOptions, | ||
| baseDir?: string; | ||
| /** | ||
| * When true, `output.types` emits Zod schema files (via `generateContract`). | ||
| * When false/omitted, `output.types` emits plain TypeScript interfaces. | ||
| */ | ||
| /** When true, `output.types` emits Zod schema files (via `generateContract`). When false/omitted, emits plain TypeScript. */ | ||
| zod?: boolean; | ||
| output?: { | ||
| /** Path template for Koa router files. Supports {filename}, {dir}, {area}. Default: `{filename}.router.ts`. */ | ||
| /** Path template for Koa router files. Supports {filename}, {dir}, {area}. */ | ||
| routes?: string; | ||
| /** | ||
| * Path template for type/schema files. Supports {filename}, {dir}, {area}. | ||
| * Generates Zod schemas when `zod: true`, otherwise plain TypeScript interfaces. | ||
| */ | ||
| /** Path template for type/schema files. Supports {filename}, {dir}, {area}. */ | ||
| types?: string; | ||
| }; | ||
| /** Import path template for service implementations. Supports {module}. */ | ||
| /** Import path template for service implementations. */ | ||
| servicePathTemplate?: string; | ||
| /** | ||
| * Whether to emit handlers for operations marked `internal`. Defaults to `true` — | ||
| * the server still needs routes for internal endpoints. Set to `false` to omit them. | ||
| */ | ||
| /** Whether to emit handlers for `internal` operations. Default true. */ | ||
| includeInternal?: boolean; | ||
@@ -61,24 +70,10 @@ } | ||
| export interface SdkConfig { | ||
| /** Directory (relative to rootDir) where SDK files are written. Default: rootDir. */ | ||
| baseDir?: string; | ||
| /** Name used for the aggregator SDK class (e.g. "homegrown" → `HomegrownSdk`). */ | ||
| name?: string; | ||
| /** | ||
| * When true, `output.types` emits Zod schema files (via `generateContract`). | ||
| * When false/omitted, `output.types` emits plain TypeScript interfaces. | ||
| */ | ||
| zod?: boolean; | ||
| output?: { | ||
| /** Path template for the SDK aggregator file. Supports {name}. Default: `sdk.ts`. */ | ||
| sdk?: string; | ||
| /** Path template for SDK type files. Supports {filename}, {dir}, {area}, {subarea}. */ | ||
| types?: string; | ||
| /** Path template for client class files. Supports {filename}, {dir}, {area}, {subarea}. */ | ||
| clients?: string; | ||
| }; | ||
| /** | ||
| * Whether to emit SDK methods for operations marked `internal`. Defaults to `false` — | ||
| * internal ops are omitted from the SDK so consumers don't pick them up. Set to `true` | ||
| * for an internal-use SDK that should expose them. | ||
| */ | ||
| includeInternal?: boolean; | ||
@@ -88,5 +83,3 @@ } | ||
| export interface ZodConfig { | ||
| /** Directory (relative to rootDir) where Zod schema files are written. Default: rootDir. */ | ||
| baseDir?: string; | ||
| /** Output path template. Supports {filename}, {dir}. Default: `{filename}.schema.ts` alongside source. */ | ||
| output?: string; | ||
@@ -96,5 +89,3 @@ } | ||
| export interface TypesConfig { | ||
| /** Directory (relative to rootDir) where plain TypeScript type files are written. Default: rootDir. */ | ||
| baseDir?: string; | ||
| /** Output path template. Supports {filename}, {dir}. Default: `{filename}.types.ts` alongside source. */ | ||
| output?: string; | ||
@@ -104,19 +95,171 @@ } | ||
| export interface TypescriptPluginConfig { | ||
| /** Generate Koa router files from `operation` declarations. */ | ||
| server?: ServerConfig; | ||
| /** Generate TypeScript SDK client files from `operation` declarations. */ | ||
| sdk?: SdkConfig; | ||
| /** Generate Zod schema files from `contract` declarations. */ | ||
| zod?: ZodConfig; | ||
| /** Generate plain TypeScript interface/type files from `contract` declarations (no Zod runtime). */ | ||
| types?: TypesConfig; | ||
| } | ||
| // ─── Server generation ───────────────────────────────────────────────────── | ||
| // ─── Caching constants ───────────────────────────────────────────────────── | ||
| function runServerGeneration( | ||
| /** Bumped when the codegen output shape changes in a way that should bust every per-file fingerprint. */ | ||
| export const TYPESCRIPT_CODEGEN_VERSION = '1'; | ||
| const MANIFEST_FILENAME = '.contractkit-typescript-manifest.json'; | ||
| // ─── Plugin entry points ────────────────────────────────────────────────── | ||
| const plugin: ContractKitPlugin = { | ||
| name: 'typescript', | ||
| async generateTargets(inputs, ctx) { | ||
| const config = ctx.options as TypescriptPluginConfig; | ||
| await runTypescriptCodegen(inputs, ctx, config, ctx.rootDir); | ||
| }, | ||
| }; | ||
| export default plugin; | ||
| /** Build a `@contractkit/plugin-typescript` instance with explicit configuration, for programmatic use. */ | ||
| export function createTypescriptPlugin(config: TypescriptPluginConfig, rootDir: string): ContractKitPlugin { | ||
| return { | ||
| name: 'typescript', | ||
| async generateTargets(inputs, ctx) { | ||
| await runTypescriptCodegen(inputs, ctx, config, rootDir); | ||
| }, | ||
| }; | ||
| } | ||
| /** | ||
| * Shared orchestration. Each sub-generator (server / sdk / zod / types) contributes a | ||
| * set of cacheable units (per-file fingerprints) plus a set of always-regenerated global | ||
| * files (aggregators, barrels, sdk-options). Units share a single manifest so the cache | ||
| * survives cross-cutting reads — the manifest lives at `<rootDir>/.contractkit-typescript-manifest.json`. | ||
| * | ||
| * Honors `ctx.cacheEnabled` — `--force` bypasses the manifest entirely. | ||
| */ | ||
| async function runTypescriptCodegen( | ||
| inputs: Parameters<NonNullable<ContractKitPlugin['generateTargets']>>[0], | ||
| ctx: PluginContext, | ||
| config: TypescriptPluginConfig, | ||
| rootDir: string, | ||
| ): Promise<void> { | ||
| const manifestPath = resolve(rootDir, MANIFEST_FILENAME); | ||
| const prevManifest: IncrementalManifest = ctx.cacheEnabled ? readManifest(manifestPath) : emptyIncrementalManifest(TYPESCRIPT_CODEGEN_VERSION); | ||
| const units: IncrementalUnit[] = []; | ||
| const globalFiles: IncrementalOutputFile[] = []; | ||
| if (config.server) collectServerOutput(config.server, rootDir, inputs, units); | ||
| if (config.sdk) collectSdkOutput(config.sdk, rootDir, inputs, units, globalFiles); | ||
| if (config.zod) collectZodOutput(config.zod, rootDir, inputs, units); | ||
| if (config.types) collectTypesOutput(config.types, rootDir, inputs, units); | ||
| const result = runIncrementalCodegen({ | ||
| codegenVersion: TYPESCRIPT_CODEGEN_VERSION, | ||
| manifestFilename: manifestPath, | ||
| prevManifest, | ||
| globalFiles, | ||
| units, | ||
| // Paths are absolute, so existsSync works directly. | ||
| fileExists: existsSync, | ||
| }); | ||
| deleteStalePaths(result.deletedPaths); | ||
| for (const { relativePath, content } of result.filesToWrite) { | ||
| ctx.emitFile(relativePath, content); | ||
| } | ||
| } | ||
| // ─── Cross-file dependency analysis ──────────────────────────────────────── | ||
| /** Build a quick lookup from model name → its definition. */ | ||
| function buildModelMap(contractRoots: readonly ContractRootNode[]): Map<string, ModelNode> { | ||
| const map = new Map<string, ModelNode>(); | ||
| for (const root of contractRoots) { | ||
| for (const model of root.models) map.set(model.name, model); | ||
| } | ||
| return map; | ||
| } | ||
| /** Collect every model referenced by this contract root (own models' fields + bases). Used to slice cross-file fingerprint inputs to just what this file actually depends on. */ | ||
| function collectContractRootRefs(root: ContractRootNode, modelMap: Map<string, ModelNode>): Set<string> { | ||
| const seeds: Parameters<typeof collectTypeRefs>[0][] = []; | ||
| for (const m of root.models) { | ||
| if (m.type) seeds.push(m.type); | ||
| for (const f of m.fields) seeds.push(f.type); | ||
| if (m.bases) { | ||
| for (const b of m.bases) seeds.push({ kind: 'ref', name: b } as Parameters<typeof collectTypeRefs>[0]); | ||
| } | ||
| } | ||
| return collectTransitiveModelRefs(seeds, modelMap); | ||
| } | ||
| /** Collect every model referenced by an op root's routes/operations (transitive). */ | ||
| function collectOpRootRefs(root: OpRootNode, modelMap: Map<string, ModelNode>): Set<string> { | ||
| const seeds: Parameters<typeof collectTypeRefs>[0][] = []; | ||
| for (const route of root.routes) { | ||
| if (route.params) seeds.push(...paramSourceTypes(route.params)); | ||
| for (const op of route.operations) { | ||
| if (op.query) seeds.push(...paramSourceTypes(op.query)); | ||
| if (op.headers) seeds.push(...paramSourceTypes(op.headers)); | ||
| if (op.request) { | ||
| for (const body of op.request.bodies) seeds.push(body.bodyType); | ||
| } | ||
| for (const resp of op.responses) { | ||
| if (resp.bodyType) seeds.push(resp.bodyType); | ||
| if (resp.headers) { | ||
| for (const h of resp.headers) seeds.push(h.type); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return collectTransitiveModelRefs(seeds, modelMap); | ||
| } | ||
| function paramSourceTypes(src: NonNullable<OpRootNode['routes'][number]['params']>): Parameters<typeof collectTypeRefs>[0][] { | ||
| const out: Parameters<typeof collectTypeRefs>[0][] = []; | ||
| if (src.kind === 'params') { | ||
| for (const n of src.nodes) out.push(n.type); | ||
| } else if (src.kind === 'ref') { | ||
| out.push({ kind: 'ref', name: src.name } as Parameters<typeof collectTypeRefs>[0]); | ||
| } else if (src.kind === 'type') { | ||
| out.push(src.node); | ||
| } | ||
| return out; | ||
| } | ||
| /** Build a sorted, JSON-stable record of (modelName -> outPath) for refs this unit depends on. */ | ||
| function sliceOutPathMap(refs: Set<string>, modelOutPaths: Map<string, string>, modelsWithInput: Set<string>, modelsWithOutput: Set<string>): Record<string, string> { | ||
| const slice: Record<string, string> = {}; | ||
| for (const ref of [...refs].sort()) { | ||
| const p = modelOutPaths.get(ref); | ||
| if (p) slice[ref] = p; | ||
| if (modelsWithInput.has(ref)) { | ||
| const ip = modelOutPaths.get(`${ref}Input`); | ||
| if (ip) slice[`${ref}Input`] = ip; | ||
| } | ||
| if (modelsWithOutput.has(ref)) { | ||
| const op = modelOutPaths.get(`${ref}Output`); | ||
| if (op) slice[`${ref}Output`] = op; | ||
| } | ||
| } | ||
| return slice; | ||
| } | ||
| /** Slice modelsWithInput/Output to only the names relevant to this unit. */ | ||
| function sliceModelSet(refs: Set<string>, ownNames: Set<string>, set: Set<string>): string[] { | ||
| const result: string[] = []; | ||
| for (const name of set) { | ||
| if (refs.has(name) || ownNames.has(name)) result.push(name); | ||
| } | ||
| return result.sort(); | ||
| } | ||
| // ─── Server sub-generator ────────────────────────────────────────────────── | ||
| function collectServerOutput( | ||
| config: ServerConfig, | ||
| rootDir: string, | ||
| inputs: Parameters<NonNullable<ContractKitPlugin['generateTargets']>>[0], | ||
| emitFile: (outPath: string, content: string) => void, | ||
| units: IncrementalUnit[], | ||
| ): void { | ||
@@ -126,16 +269,13 @@ const serverBase = resolve(rootDir, config.baseDir ?? '.'); | ||
| const modelsWithOutput = inputs.modelsWithOutput as Set<string>; | ||
| const modelMap = buildModelMap(inputs.contractRoots); | ||
| const allFiles = [...inputs.contractRoots.map(r => r.file), ...inputs.opRoots.map(r => r.file)]; | ||
| const commonRoot = commonDir(allFiles, rootDir); | ||
| const subConfigKey = stableSubConfig(config); | ||
| // ── Types / Zod output ── | ||
| // When output.types is configured we generate type files ourselves and build a | ||
| // local modelOutPaths map so the router generator can resolve import paths. | ||
| let serverModelOutPaths = new Map<string, string>(); | ||
| // Pre-pass: register all model → outPath. Cross-file refs need to resolve correctly, | ||
| // which means we need the COMPLETE map (not a slice) — even though each unit's fingerprint | ||
| // only includes its own slice. | ||
| const serverModelOutPaths = new Map<string, string>(); | ||
| const typeEntries: { ast: ContractRootNode; typeOutPath: string }[] = []; | ||
| if (config.output?.types) { | ||
| serverModelOutPaths = new Map(); | ||
| // Pass 1: register all model → outPath entries before generating content, | ||
| // so cross-file type refs resolve correctly. | ||
| const typeEntries: { ast: (typeof inputs.contractRoots)[number]; typeOutPath: string }[] = []; | ||
| for (const ast of inputs.contractRoots) { | ||
@@ -146,41 +286,83 @@ const typeOutPath = computeContractOutPath(ast.file, serverBase, config.output.types, '.ts', commonRoot, ast.meta); | ||
| serverModelOutPaths.set(model.name, typeOutPath); | ||
| if (modelsWithInput.has(model.name)) { | ||
| serverModelOutPaths.set(`${model.name}Input`, typeOutPath); | ||
| } | ||
| if (modelsWithOutput.has(model.name)) { | ||
| serverModelOutPaths.set(`${model.name}Output`, typeOutPath); | ||
| } | ||
| if (modelsWithInput.has(model.name)) serverModelOutPaths.set(`${model.name}Input`, typeOutPath); | ||
| if (modelsWithOutput.has(model.name)) serverModelOutPaths.set(`${model.name}Output`, typeOutPath); | ||
| } | ||
| } | ||
| } | ||
| // Pass 2: emit type files. | ||
| for (const { ast, typeOutPath } of typeEntries) { | ||
| const ctx = { modelOutPaths: serverModelOutPaths, currentOutPath: typeOutPath, modelsWithInput, modelsWithOutput }; | ||
| const content = config.zod ? generateContract(ast, ctx) : generatePlainTypes(ast, ctx); | ||
| emitFile(typeOutPath, content); | ||
| } | ||
| // ── Per-contract-root types unit ── | ||
| for (const { ast, typeOutPath } of typeEntries) { | ||
| const refs = collectContractRootRefs(ast, modelMap); | ||
| const ownNames = new Set(ast.models.map(m => m.name)); | ||
| const fingerprint = hashFingerprint({ | ||
| kind: 'server-types', | ||
| v: TYPESCRIPT_CODEGEN_VERSION, | ||
| outPath: typeOutPath, | ||
| root: ast, | ||
| outPathSlice: sliceOutPathMap(refs, serverModelOutPaths, modelsWithInput, modelsWithOutput), | ||
| modelsWithInput: sliceModelSet(refs, ownNames, modelsWithInput), | ||
| modelsWithOutput: sliceModelSet(refs, ownNames, modelsWithOutput), | ||
| sub: subConfigKey, | ||
| }); | ||
| units.push({ | ||
| key: `server-types::${typeOutPath}`, | ||
| fingerprint, | ||
| render: () => { | ||
| const renderCtx = { | ||
| modelOutPaths: serverModelOutPaths, | ||
| currentOutPath: typeOutPath, | ||
| modelsWithInput, | ||
| modelsWithOutput, | ||
| }; | ||
| const content = config.zod ? generateContract(ast, renderCtx) : generatePlainTypes(ast, renderCtx); | ||
| return [{ relativePath: typeOutPath, content }]; | ||
| }, | ||
| }); | ||
| } | ||
| // ── Routes output ── | ||
| // ── Per-op-root router unit ── | ||
| for (const ast of inputs.opRoots) { | ||
| const outPath = computeOpOutPath(ast.file, serverBase, config.output?.routes, '.router.ts', commonRoot, ast.meta); | ||
| const content = generateOp(ast, { | ||
| servicePathTemplate: config.servicePathTemplate, | ||
| const refs = collectOpRootRefs(ast, modelMap); | ||
| const fingerprint = hashFingerprint({ | ||
| kind: 'server-router', | ||
| v: TYPESCRIPT_CODEGEN_VERSION, | ||
| outPath, | ||
| modelOutPaths: serverModelOutPaths, | ||
| modelsWithInput, | ||
| modelsWithOutput, | ||
| includeInternal: config.includeInternal, | ||
| root: ast, | ||
| // The router imports types from each contract root's type file; the slice covers exactly that. | ||
| outPathSlice: sliceOutPathMap(refs, serverModelOutPaths, modelsWithInput, modelsWithOutput), | ||
| modelsWithInput: sliceModelSet(refs, new Set(), modelsWithInput), | ||
| modelsWithOutput: sliceModelSet(refs, new Set(), modelsWithOutput), | ||
| servicePathTemplate: config.servicePathTemplate ?? null, | ||
| includeInternal: config.includeInternal ?? true, | ||
| sub: subConfigKey, | ||
| }); | ||
| emitFile(outPath, content); | ||
| units.push({ | ||
| key: `server-router::${outPath}`, | ||
| fingerprint, | ||
| render: () => [ | ||
| { | ||
| relativePath: outPath, | ||
| content: generateOp(ast, { | ||
| servicePathTemplate: config.servicePathTemplate, | ||
| outPath, | ||
| modelOutPaths: serverModelOutPaths, | ||
| modelsWithInput, | ||
| modelsWithOutput, | ||
| includeInternal: config.includeInternal, | ||
| }), | ||
| }, | ||
| ], | ||
| }); | ||
| } | ||
| } | ||
| // ─── SDK generation ──────────────────────────────────────────────────────── | ||
| // ─── SDK sub-generator ───────────────────────────────────────────────────── | ||
| function runSdkGeneration( | ||
| function collectSdkOutput( | ||
| config: SdkConfig, | ||
| rootDir: string, | ||
| inputs: Parameters<NonNullable<ContractKitPlugin['generateTargets']>>[0], | ||
| emitFile: (outPath: string, content: string) => void, | ||
| units: IncrementalUnit[], | ||
| globalFiles: IncrementalOutputFile[], | ||
| ): void { | ||
@@ -194,18 +376,18 @@ const sdkBase = config.baseDir ? resolve(rootDir, config.baseDir) : rootDir; | ||
| const sdkOptionsPath = join(dirname(sdkEntryPath), 'sdk-options.ts'); | ||
| const subConfigKey = stableSubConfig(config); | ||
| const modelsWithInput = inputs.modelsWithInput as Set<string>; | ||
| const modelsWithOutput = inputs.modelsWithOutput as Set<string>; | ||
| const modelMap = buildModelMap(inputs.contractRoots); | ||
| const allFiles = [...inputs.contractRoots.map(r => r.file), ...inputs.opRoots.map(r => r.file)]; | ||
| const ckCommonRoot = commonDir(allFiles, rootDir); | ||
| let sdkModelOutPaths = new Map<string, string>(); | ||
| const sdkModelOutPaths = new Map<string, string>(); | ||
| const sdkTypePaths: string[] = []; | ||
| const sdkClientInfos: { outPath: string; className: string; propertyName: string }[] = []; | ||
| // ── SDK types ── | ||
| // ── Pre-pass: SDK type files ── | ||
| const sdkContractEntries: { ast: ContractRootNode; typeOutPath: string }[] = []; | ||
| if (config.output?.types) { | ||
| sdkModelOutPaths = new Map<string, string>(); | ||
| const publicTypes = computePubliclyReachableTypes(inputs.opRoots, inputs.contractRoots, modelsWithInput, modelsWithOutput); | ||
| const sdkContractEntries: { ast: (typeof inputs.contractRoots)[number]; typeOutPath: string }[] = []; | ||
| for (const ast of inputs.contractRoots) { | ||
@@ -223,38 +405,54 @@ const typeOutPath = computeSdkTypeOutPath(ast.file, sdkBase, config.output.types, ckCommonRoot, ast.meta); | ||
| } | ||
| } | ||
| for (const { ast, typeOutPath } of sdkContractEntries) { | ||
| let content: string; | ||
| if (config.zod) { | ||
| content = generateContract(ast, { | ||
| modelOutPaths: sdkModelOutPaths, | ||
| currentOutPath: typeOutPath, | ||
| modelsWithInput, | ||
| modelsWithOutput, | ||
| }); | ||
| } else { | ||
| let rel = relative(dirname(typeOutPath), sdkOptionsPath).replace(/\.ts$/, '.js'); | ||
| if (!rel.startsWith('.')) rel = './' + rel; | ||
| content = generatePlainTypes(ast, { | ||
| modelOutPaths: sdkModelOutPaths, | ||
| currentOutPath: typeOutPath, | ||
| modelsWithInput, | ||
| modelsWithOutput, | ||
| jsonValueImportPath: rel, | ||
| }); | ||
| } | ||
| emitFile(typeOutPath, content); | ||
| } | ||
| // ── SDK type units ── | ||
| for (const { ast, typeOutPath } of sdkContractEntries) { | ||
| const refs = collectContractRootRefs(ast, modelMap); | ||
| const ownNames = new Set(ast.models.map(m => m.name)); | ||
| const fingerprint = hashFingerprint({ | ||
| kind: 'sdk-types', | ||
| v: TYPESCRIPT_CODEGEN_VERSION, | ||
| outPath: typeOutPath, | ||
| root: ast, | ||
| outPathSlice: sliceOutPathMap(refs, sdkModelOutPaths, modelsWithInput, modelsWithOutput), | ||
| modelsWithInput: sliceModelSet(refs, ownNames, modelsWithInput), | ||
| modelsWithOutput: sliceModelSet(refs, ownNames, modelsWithOutput), | ||
| sdkOptionsPath, | ||
| sub: subConfigKey, | ||
| }); | ||
| units.push({ | ||
| key: `sdk-types::${typeOutPath}`, | ||
| fingerprint, | ||
| render: () => { | ||
| let content: string; | ||
| if (config.zod) { | ||
| content = generateContract(ast, { | ||
| modelOutPaths: sdkModelOutPaths, | ||
| currentOutPath: typeOutPath, | ||
| modelsWithInput, | ||
| modelsWithOutput, | ||
| }); | ||
| } else { | ||
| let rel = relative(dirname(typeOutPath), sdkOptionsPath).replace(/\.ts$/, '.js'); | ||
| if (!rel.startsWith('.')) rel = './' + rel; | ||
| content = generatePlainTypes(ast, { | ||
| modelOutPaths: sdkModelOutPaths, | ||
| currentOutPath: typeOutPath, | ||
| modelsWithInput, | ||
| modelsWithOutput, | ||
| jsonValueImportPath: rel, | ||
| }); | ||
| } | ||
| return [{ relativePath: typeOutPath, content }]; | ||
| }, | ||
| }); | ||
| } | ||
| // ── 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) | ||
| // ── Bucket op roots by area/subarea ── | ||
| interface AreaBucket { | ||
| leaves: { ast: typeof inputs.opRoots[number]; outPath: string; subarea: string }[]; | ||
| inlineRoots: typeof inputs.opRoots[number][]; | ||
| leaves: { ast: OpRootNode; outPath: string; subarea: string }[]; | ||
| inlineRoots: OpRootNode[]; | ||
| } | ||
| const areaBuckets = new Map<string, AreaBucket>(); | ||
| const topLevelEntries: { ast: typeof inputs.opRoots[number]; outPath: string }[] = []; | ||
| const topLevelEntries: { ast: OpRootNode; outPath: string }[] = []; | ||
@@ -265,3 +463,2 @@ if (config.output?.clients) { | ||
| if (!sdkOutPath || !hasPublicOperations(ast, config.includeInternal)) continue; | ||
| const { area, subarea } = getAreaSubarea(ast); | ||
@@ -281,42 +478,87 @@ if (area && subarea) { | ||
| // 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( | ||
| outPath, | ||
| generateSdk(ast, { | ||
| typeImportPathTemplate: undefined, | ||
| outPath, | ||
| modelOutPaths: sdkModelOutPaths, | ||
| // ── Per-leaf-client (area+subarea) units ── | ||
| for (const [area, bucket] of areaBuckets.entries()) { | ||
| for (const leaf of bucket.leaves) { | ||
| const className = deriveSubareaClientClassName(area, leaf.subarea); | ||
| sdkClientInfos.push({ outPath: leaf.outPath, className, propertyName: deriveSubareaPropertyName(leaf.subarea) }); | ||
| const refs = collectOpRootRefs(leaf.ast, modelMap); | ||
| const fingerprint = hashFingerprint({ | ||
| kind: 'sdk-leaf-client', | ||
| v: TYPESCRIPT_CODEGEN_VERSION, | ||
| outPath: leaf.outPath, | ||
| root: leaf.ast, | ||
| outPathSlice: sliceOutPathMap(refs, sdkModelOutPaths, modelsWithInput, modelsWithOutput), | ||
| modelsWithInput: sliceModelSet(refs, new Set(), modelsWithInput), | ||
| modelsWithOutput: sliceModelSet(refs, new Set(), modelsWithOutput), | ||
| sdkOptionsPath, | ||
| modelsWithInput, | ||
| modelsWithOutput, | ||
| includeInternal: config.includeInternal, | ||
| clientClassName: className, | ||
| }), | ||
| ); | ||
| className, | ||
| includeInternal: config.includeInternal ?? false, | ||
| sub: subConfigKey, | ||
| }); | ||
| units.push({ | ||
| key: `sdk-leaf-client::${leaf.outPath}`, | ||
| fingerprint, | ||
| render: () => [ | ||
| { | ||
| relativePath: leaf.outPath, | ||
| content: generateSdk(leaf.ast, { | ||
| typeImportPathTemplate: undefined, | ||
| outPath: leaf.outPath, | ||
| modelOutPaths: sdkModelOutPaths, | ||
| sdkOptionsPath, | ||
| modelsWithInput, | ||
| modelsWithOutput, | ||
| includeInternal: config.includeInternal, | ||
| clientClassName: className, | ||
| }), | ||
| }, | ||
| ], | ||
| }); | ||
| } | ||
| } | ||
| // ── Top-level (no area) client units ── | ||
| for (const { ast, outPath } of topLevelEntries) { | ||
| const className = deriveClientClassName(ast.file); | ||
| sdkClientInfos.push({ outPath, className, propertyName: deriveClientPropertyName(ast.file) }); | ||
| emitFile( | ||
| const refs = collectOpRootRefs(ast, modelMap); | ||
| const fingerprint = hashFingerprint({ | ||
| kind: 'sdk-top-client', | ||
| v: TYPESCRIPT_CODEGEN_VERSION, | ||
| outPath, | ||
| generateSdk(ast, { | ||
| typeImportPathTemplate: undefined, | ||
| outPath, | ||
| modelOutPaths: sdkModelOutPaths, | ||
| sdkOptionsPath, | ||
| modelsWithInput, | ||
| modelsWithOutput, | ||
| includeInternal: config.includeInternal, | ||
| }), | ||
| ); | ||
| root: ast, | ||
| outPathSlice: sliceOutPathMap(refs, sdkModelOutPaths, modelsWithInput, modelsWithOutput), | ||
| modelsWithInput: sliceModelSet(refs, new Set(), modelsWithInput), | ||
| modelsWithOutput: sliceModelSet(refs, new Set(), modelsWithOutput), | ||
| sdkOptionsPath, | ||
| includeInternal: config.includeInternal ?? false, | ||
| sub: subConfigKey, | ||
| }); | ||
| units.push({ | ||
| key: `sdk-top-client::${outPath}`, | ||
| fingerprint, | ||
| render: () => [ | ||
| { | ||
| relativePath: outPath, | ||
| content: generateSdk(ast, { | ||
| typeImportPathTemplate: undefined, | ||
| outPath, | ||
| modelOutPaths: sdkModelOutPaths, | ||
| sdkOptionsPath, | ||
| modelsWithInput, | ||
| modelsWithOutput, | ||
| includeInternal: config.includeInternal, | ||
| }), | ||
| }, | ||
| ], | ||
| }); | ||
| } | ||
| } | ||
| // ── sdk-options.ts ── | ||
| emitFile(sdkOptionsPath, generateSdkOptions()); | ||
| // ── Global files: sdk-options, aggregator, barrels, root index ── | ||
| // The aggregator inlines area-only op roots, so its content depends on each inline root's | ||
| // full AST. Caching it gains little — the codegen is fast and any inline-root change rebuilds | ||
| // the file anyway. Same for barrels (one line per imported file). Always regenerate. | ||
| globalFiles.push({ relativePath: sdkOptionsPath, content: generateSdkOptions() }); | ||
| // ── sdk.ts aggregator ── | ||
| const hasAnything = sdkClientInfos.length > 0 || areaBuckets.size > 0; | ||
@@ -377,22 +619,14 @@ if (hasAnything) { | ||
| emitFile( | ||
| sdkEntryPath, | ||
| generateSdkAggregator({ | ||
| topLevelClients, | ||
| areas, | ||
| sdkOptionsImportPath, | ||
| sdkClassName, | ||
| }), | ||
| ); | ||
| globalFiles.push({ | ||
| relativePath: sdkEntryPath, | ||
| content: generateSdkAggregator({ topLevelClients, areas, sdkOptionsImportPath, sdkClassName }), | ||
| }); | ||
| } | ||
| // ── Barrel files ── | ||
| const sdkSrcDir = dirname(sdkEntryPath); | ||
| const sdkTypeBarrels = generateBarrelFiles(sdkTypePaths); | ||
| for (const barrel of sdkTypeBarrels) emitFile(barrel.outPath, barrel.content); | ||
| for (const barrel of sdkTypeBarrels) globalFiles.push({ relativePath: barrel.outPath, content: barrel.content }); | ||
| const rootExports: string[] = [`export * from './${basename(sdkOptionsPath).replace(/\.ts$/, '.js')}';`]; | ||
| if (hasAnything) { | ||
| rootExports.push(`export * from './${basename(sdkEntryPath).replace(/\.ts$/, '.js')}';`); | ||
| } | ||
| if (hasAnything) rootExports.push(`export * from './${basename(sdkEntryPath).replace(/\.ts$/, '.js')}';`); | ||
| for (const c of sdkClientInfos) { | ||
@@ -408,12 +642,15 @@ let rel = relative(sdkSrcDir, c.outPath).replace(/\.ts$/, '.js'); | ||
| } | ||
| emitFile(join(sdkSrcDir, 'index.ts'), `// Auto-generated barrel file\n${rootExports.sort().join('\n')}\n`); | ||
| globalFiles.push({ | ||
| relativePath: join(sdkSrcDir, 'index.ts'), | ||
| content: `// Auto-generated barrel file\n${rootExports.sort().join('\n')}\n`, | ||
| }); | ||
| } | ||
| // ─── Zod generation ──────────────────────────────────────────────────────── | ||
| // ─── Zod sub-generator ───────────────────────────────────────────────────── | ||
| function runZodGeneration( | ||
| function collectZodOutput( | ||
| config: ZodConfig, | ||
| rootDir: string, | ||
| inputs: Parameters<NonNullable<ContractKitPlugin['generateTargets']>>[0], | ||
| emitFile: (outPath: string, content: string) => void, | ||
| units: IncrementalUnit[], | ||
| ): void { | ||
@@ -425,6 +662,7 @@ const zodBase = resolve(rootDir, config.baseDir ?? '.'); | ||
| const modelsWithOutput = inputs.modelsWithOutput as Set<string>; | ||
| const modelMap = buildModelMap(inputs.contractRoots); | ||
| const subConfigKey = stableSubConfig(config); | ||
| // Pre-pass: register all model → outPath before generating, so cross-file imports resolve. | ||
| const modelOutPaths = new Map<string, string>(); | ||
| const entries: { ast: (typeof inputs.contractRoots)[number]; outPath: string }[] = []; | ||
| const entries: { ast: ContractRootNode; outPath: string }[] = []; | ||
| for (const ast of inputs.contractRoots) { | ||
@@ -441,19 +679,34 @@ const outPath = computeContractOutPath(ast.file, zodBase, config.output, '.schema.ts', commonRoot, ast.meta); | ||
| for (const { ast, outPath } of entries) { | ||
| const content = generateContract(ast, { | ||
| modelOutPaths, | ||
| currentOutPath: outPath, | ||
| modelsWithInput, | ||
| modelsWithOutput, | ||
| const refs = collectContractRootRefs(ast, modelMap); | ||
| const ownNames = new Set(ast.models.map(m => m.name)); | ||
| const fingerprint = hashFingerprint({ | ||
| kind: 'zod', | ||
| v: TYPESCRIPT_CODEGEN_VERSION, | ||
| outPath, | ||
| root: ast, | ||
| outPathSlice: sliceOutPathMap(refs, modelOutPaths, modelsWithInput, modelsWithOutput), | ||
| modelsWithInput: sliceModelSet(refs, ownNames, modelsWithInput), | ||
| modelsWithOutput: sliceModelSet(refs, ownNames, modelsWithOutput), | ||
| sub: subConfigKey, | ||
| }); | ||
| emitFile(outPath, content); | ||
| units.push({ | ||
| key: `zod::${outPath}`, | ||
| fingerprint, | ||
| render: () => [ | ||
| { | ||
| relativePath: outPath, | ||
| content: generateContract(ast, { modelOutPaths, currentOutPath: outPath, modelsWithInput, modelsWithOutput }), | ||
| }, | ||
| ], | ||
| }); | ||
| } | ||
| } | ||
| // ─── Types generation ────────────────────────────────────────────────────── | ||
| // ─── Plain types sub-generator ───────────────────────────────────────────── | ||
| function runTypesGeneration( | ||
| function collectTypesOutput( | ||
| config: TypesConfig, | ||
| rootDir: string, | ||
| inputs: Parameters<NonNullable<ContractKitPlugin['generateTargets']>>[0], | ||
| emitFile: (outPath: string, content: string) => void, | ||
| units: IncrementalUnit[], | ||
| ): void { | ||
@@ -465,6 +718,7 @@ const typesBase = resolve(rootDir, config.baseDir ?? '.'); | ||
| const modelsWithOutput = inputs.modelsWithOutput as Set<string>; | ||
| const modelMap = buildModelMap(inputs.contractRoots); | ||
| const subConfigKey = stableSubConfig(config); | ||
| // Pre-pass: register all model → outPath before generating. | ||
| const modelOutPaths = new Map<string, string>(); | ||
| const entries: { ast: (typeof inputs.contractRoots)[number]; outPath: string }[] = []; | ||
| const entries: { ast: ContractRootNode; outPath: string }[] = []; | ||
| for (const ast of inputs.contractRoots) { | ||
@@ -481,63 +735,68 @@ const outPath = computeContractOutPath(ast.file, typesBase, config.output, '.types.ts', commonRoot, ast.meta); | ||
| for (const { ast, outPath } of entries) { | ||
| const content = generatePlainTypes(ast, { | ||
| modelOutPaths, | ||
| currentOutPath: outPath, | ||
| modelsWithInput, | ||
| modelsWithOutput, | ||
| const refs = collectContractRootRefs(ast, modelMap); | ||
| const ownNames = new Set(ast.models.map(m => m.name)); | ||
| const fingerprint = hashFingerprint({ | ||
| kind: 'plain-types', | ||
| v: TYPESCRIPT_CODEGEN_VERSION, | ||
| outPath, | ||
| root: ast, | ||
| outPathSlice: sliceOutPathMap(refs, modelOutPaths, modelsWithInput, modelsWithOutput), | ||
| modelsWithInput: sliceModelSet(refs, ownNames, modelsWithInput), | ||
| modelsWithOutput: sliceModelSet(refs, ownNames, modelsWithOutput), | ||
| sub: subConfigKey, | ||
| }); | ||
| emitFile(outPath, content); | ||
| units.push({ | ||
| key: `plain-types::${outPath}`, | ||
| fingerprint, | ||
| render: () => [ | ||
| { | ||
| relativePath: outPath, | ||
| content: generatePlainTypes(ast, { modelOutPaths, currentOutPath: outPath, modelsWithInput, modelsWithOutput }), | ||
| }, | ||
| ], | ||
| }); | ||
| } | ||
| } | ||
| // ─── Combined plugin ──────────────────────────────────────────────────────── | ||
| // ─── Manifest IO + cleanup ───────────────────────────────────────────────── | ||
| const plugin: ContractKitPlugin = { | ||
| name: 'typescript', | ||
| cacheKey: 'typescript', | ||
| async generateTargets(inputs, ctx) { | ||
| const config = ctx.options as TypescriptPluginConfig; | ||
| function readManifest(manifestPath: string): IncrementalManifest { | ||
| if (!existsSync(manifestPath)) return emptyIncrementalManifest(TYPESCRIPT_CODEGEN_VERSION); | ||
| try { | ||
| return parseIncrementalManifest(readFileSync(manifestPath, 'utf-8')); | ||
| } catch { | ||
| return emptyIncrementalManifest(TYPESCRIPT_CODEGEN_VERSION); | ||
| } | ||
| } | ||
| if (config.server) { | ||
| runServerGeneration(config.server, ctx.rootDir, inputs, ctx.emitFile.bind(ctx)); | ||
| function deleteStalePaths(absPaths: string[]): void { | ||
| if (absPaths.length === 0) return; | ||
| const removedDirs = new Set<string>(); | ||
| for (const abs of absPaths) { | ||
| if (existsSync(abs)) { | ||
| rmSync(abs, { force: true }); | ||
| removedDirs.add(dirname(abs)); | ||
| } | ||
| if (config.sdk) { | ||
| runSdkGeneration(config.sdk, ctx.rootDir, inputs, ctx.emitFile.bind(ctx)); | ||
| } | ||
| // Walk up affected dirs and remove if empty. Bounded — stops at filesystem root or first non-empty dir. | ||
| for (const dir of removedDirs) { | ||
| let current = dir; | ||
| while (current.length > 1) { | ||
| try { | ||
| if (readdirSync(current).length === 0) { | ||
| rmdirSync(current); | ||
| current = dirname(current); | ||
| } else { | ||
| break; | ||
| } | ||
| } catch { | ||
| break; | ||
| } | ||
| } | ||
| if (config.zod) { | ||
| runZodGeneration(config.zod, ctx.rootDir, inputs, ctx.emitFile.bind(ctx)); | ||
| } | ||
| if (config.types) { | ||
| runTypesGeneration(config.types, ctx.rootDir, inputs, ctx.emitFile.bind(ctx)); | ||
| } | ||
| }, | ||
| }; | ||
| } | ||
| } | ||
| export default plugin; | ||
| // ─── Factory: for programmatic use with explicit config ──────────────────── | ||
| /** | ||
| * 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 { | ||
| return { | ||
| name: 'typescript', | ||
| cacheKey: `typescript:${JSON.stringify(config)}`, | ||
| async generateTargets(inputs, ctx) { | ||
| if (config.server) { | ||
| runServerGeneration(config.server, rootDir, inputs, ctx.emitFile.bind(ctx)); | ||
| } | ||
| if (config.sdk) { | ||
| runSdkGeneration(config.sdk, rootDir, inputs, ctx.emitFile.bind(ctx)); | ||
| } | ||
| if (config.zod) { | ||
| runZodGeneration(config.zod, rootDir, inputs, ctx.emitFile.bind(ctx)); | ||
| } | ||
| if (config.types) { | ||
| runTypesGeneration(config.types, rootDir, inputs, ctx.emitFile.bind(ctx)); | ||
| } | ||
| }, | ||
| }; | ||
| /** Stringify a sub-config so it can participate in fingerprints. JSON.stringify gives stable output for typical config shapes. */ | ||
| function stableSubConfig(config: unknown): string { | ||
| return JSON.stringify(config ?? null); | ||
| } |
@@ -13,2 +13,3 @@ import { describe, it, expect } from 'vitest'; | ||
| options, | ||
| cacheEnabled: true, | ||
| emitFile: (outPath: string, content: string) => { | ||
@@ -67,3 +68,4 @@ emitted.set(outPath, content); | ||
| ); | ||
| expect(ctx.emitted.size).toBe(2); | ||
| const routeFiles = [...ctx.emitted.keys()].filter(p => p.endsWith('.router.ts')); | ||
| expect(routeFiles.length).toBe(2); | ||
| }); | ||
@@ -70,0 +72,0 @@ |
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
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
2471014
4.53%15914
3.94%2
100%+ Added
- Removed
Updated