@contractkit/plugin-typescript 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@ import {
3
3
  generateSdk,
4
4
  generateSdkOptions,
5
5
  generateSdkAggregator,
6
+ generateAreaClient,
6
7
  deriveClientClassName,
7
8
  deriveClientPropertyName,
8
9
  deriveAreaClientClassName,
@@ -1536,60 +1537,83 @@ describe('area + subarea name derivation', () => {
1536
1537
  // ─── generateSdkAggregator — area + subarea wiring ────────────────────────
1537
1538
 
1538
1539
  describe('generateSdkAggregator — area + subarea', () => {
1539
- it('emits one <Area>Client per area with subarea property wiring', () => {
1540
+ it('imports each area client and exposes it on Sdk', () => {
1540
1541
  const out = generateSdkAggregator({
1541
1542
  topLevelClients: [],
1542
1543
  areas: [
1543
1544
  {
1544
1545
  area: 'identity',
1545
- inlineFiles: [],
1546
- subareaClients: [
1547
- {
1548
- propertyName: 'invitations',
1549
- client: { className: 'IdentityInvitationsClient', propertyName: 'invitations', importPath: './identity/invitations.client.js' },
1550
- },
1551
- {
1552
- propertyName: 'users',
1553
- client: { className: 'IdentityUsersClient', propertyName: 'users', importPath: './identity/users.client.js' },
1554
- },
1555
- ],
1546
+ client: { className: 'IdentityClient', propertyName: 'identity', importPath: './identity/identity.client.js' },
1547
+ },
1548
+ ],
1549
+ });
1550
+ expect(out).toContain("import { IdentityClient } from './identity/identity.client.js'");
1551
+ expect(out).toContain('export class Sdk {');
1552
+ expect(out).toContain('readonly identity: IdentityClient');
1553
+ expect(out).toContain('this.identity = new IdentityClient(sdkFetch)');
1554
+ // The aggregator no longer declares the IdentityClient itself.
1555
+ expect(out).not.toContain('class IdentityClient {');
1556
+ });
1557
+
1558
+ it('mixes area clients with top-level (no-area) clients', () => {
1559
+ const out = generateSdkAggregator({
1560
+ topLevelClients: [{ className: 'WebhooksClient', propertyName: 'webhooks', importPath: './webhooks.client.js' }],
1561
+ areas: [
1562
+ {
1563
+ area: 'payments',
1564
+ client: { className: 'PaymentsClient', propertyName: 'payments', importPath: './payments/payments.client.js' },
1556
1565
  },
1557
1566
  ],
1558
1567
  });
1559
- expect(out).toContain("import { IdentityInvitationsClient } from './identity/invitations.client.js'");
1560
- expect(out).toContain("import { IdentityUsersClient } from './identity/users.client.js'");
1561
- expect(out).toContain('class IdentityClient {');
1568
+ expect(out).toContain('readonly payments: PaymentsClient');
1569
+ expect(out).toContain('readonly webhooks: WebhooksClient');
1570
+ expect(out).toContain('this.payments = new PaymentsClient(sdkFetch)');
1571
+ expect(out).toContain('this.webhooks = new WebhooksClient(sdkFetch)');
1572
+ });
1573
+ });
1574
+
1575
+ describe('generateAreaClient — <Area>Client emission', () => {
1576
+ it('emits one <Area>Client class with subarea property wiring', () => {
1577
+ const out = generateAreaClient({
1578
+ area: 'identity',
1579
+ outPath: '/out/identity/identity.client.ts',
1580
+ inlineFiles: [],
1581
+ subareaClients: [
1582
+ {
1583
+ propertyName: 'invitations',
1584
+ client: { className: 'IdentityInvitationsClient', propertyName: 'invitations', importPath: './invitations.client.js' },
1585
+ },
1586
+ {
1587
+ propertyName: 'users',
1588
+ client: { className: 'IdentityUsersClient', propertyName: 'users', importPath: './users.client.js' },
1589
+ },
1590
+ ],
1591
+ sdkOptionsPath: '/out/sdk-options.ts',
1592
+ });
1593
+ expect(out).toContain("import { IdentityInvitationsClient } from './invitations.client.js'");
1594
+ expect(out).toContain("import { IdentityUsersClient } from './users.client.js'");
1595
+ expect(out).toContain('export class IdentityClient {');
1562
1596
  expect(out).toContain('readonly invitations: IdentityInvitationsClient');
1563
1597
  expect(out).toContain('readonly users: IdentityUsersClient');
1564
1598
  expect(out).toContain('this.invitations = new IdentityInvitationsClient(fetch)');
1565
1599
  expect(out).toContain('this.users = new IdentityUsersClient(fetch)');
1566
- // Sdk wires the area property
1567
- expect(out).toContain('export class Sdk {');
1568
- expect(out).toContain('readonly identity: IdentityClient');
1569
- expect(out).toContain('this.identity = new IdentityClient(sdkFetch)');
1570
1600
  });
1571
1601
 
1572
- it('inlines methods from area-level (no-subarea) files into <Area>Client', () => {
1602
+ it('inlines methods from area-level (no-subarea) files into the area client', () => {
1573
1603
  const root = opRoot(
1574
1604
  [opRoute('/me', [opOperation('get', { sdk: 'getCurrentUser', responses: [opResponse(200, 'User', 'application/json')] })])],
1575
1605
  'identity.ck',
1576
1606
  { area: 'identity' },
1577
1607
  );
1578
- const out = generateSdkAggregator({
1579
- topLevelClients: [],
1580
- areas: [
1581
- {
1582
- area: 'identity',
1583
- inlineFiles: [{ root, codegenOptions: { outPath: '/out/sdk.ts', sdkOptionsPath: '/out/sdk-options.ts' } }],
1584
- subareaClients: [],
1585
- },
1586
- ],
1608
+ const out = generateAreaClient({
1609
+ area: 'identity',
1610
+ outPath: '/out/identity/identity.client.ts',
1611
+ inlineFiles: [{ root, codegenOptions: { outPath: '/out/identity/identity.client.ts', sdkOptionsPath: '/out/sdk-options.ts' } }],
1612
+ subareaClients: [],
1613
+ sdkOptionsPath: '/out/sdk-options.ts',
1587
1614
  });
1588
- expect(out).toContain('class IdentityClient {');
1615
+ expect(out).toContain('export class IdentityClient {');
1589
1616
  expect(out).toContain('async getCurrentUser(');
1590
- // No subarea property declarations on this client
1591
- expect(out).not.toMatch(/readonly \w+: \w+;[\s\S]*async getCurrentUser/);
1592
- // The fetch arg is captured as `private fetch` since methods reference `this.fetch`
1593
1617
  expect(out).toContain('constructor(private fetch: SdkFetch)');
1594
1618
  });
1595
1619
 
@@ -1599,20 +1623,17 @@ describe('generateSdkAggregator — area + subarea', () => {
1599
1623
  'identity.ck',
1600
1624
  { area: 'identity' },
1601
1625
  );
1602
- const out = generateSdkAggregator({
1603
- topLevelClients: [],
1604
- areas: [
1626
+ const out = generateAreaClient({
1627
+ area: 'identity',
1628
+ outPath: '/out/identity/identity.client.ts',
1629
+ inlineFiles: [{ root: inlineRoot, codegenOptions: { outPath: '/out/identity/identity.client.ts', sdkOptionsPath: '/out/sdk-options.ts' } }],
1630
+ subareaClients: [
1605
1631
  {
1606
- area: 'identity',
1607
- inlineFiles: [{ root: inlineRoot, codegenOptions: { outPath: '/out/sdk.ts', sdkOptionsPath: '/out/sdk-options.ts' } }],
1608
- subareaClients: [
1609
- {
1610
- propertyName: 'invitations',
1611
- client: { className: 'IdentityInvitationsClient', propertyName: 'invitations', importPath: './identity/invitations.client.js' },
1612
- },
1613
- ],
1632
+ propertyName: 'invitations',
1633
+ client: { className: 'IdentityInvitationsClient', propertyName: 'invitations', importPath: './invitations.client.js' },
1614
1634
  },
1615
1635
  ],
1636
+ sdkOptionsPath: '/out/sdk-options.ts',
1616
1637
  });
1617
1638
  expect(out).toContain('readonly invitations: IdentityInvitationsClient');
1618
1639
  expect(out).toContain('async getCurrentUser(');
@@ -1631,42 +1652,16 @@ describe('generateSdkAggregator — area + subarea', () => {
1631
1652
  { area: 'identity' },
1632
1653
  );
1633
1654
  expect(() =>
1634
- generateSdkAggregator({
1635
- topLevelClients: [],
1636
- areas: [
1637
- {
1638
- area: 'identity',
1639
- inlineFiles: [
1640
- { root: a, codegenOptions: { outPath: '/out/sdk.ts', sdkOptionsPath: '/out/sdk-options.ts' } },
1641
- { root: b, codegenOptions: { outPath: '/out/sdk.ts', sdkOptionsPath: '/out/sdk-options.ts' } },
1642
- ],
1643
- subareaClients: [],
1644
- },
1655
+ generateAreaClient({
1656
+ area: 'identity',
1657
+ outPath: '/out/identity/identity.client.ts',
1658
+ inlineFiles: [
1659
+ { root: a, codegenOptions: { outPath: '/out/identity/identity.client.ts', sdkOptionsPath: '/out/sdk-options.ts' } },
1660
+ { root: b, codegenOptions: { outPath: '/out/identity/identity.client.ts', sdkOptionsPath: '/out/sdk-options.ts' } },
1645
1661
  ],
1662
+ subareaClients: [],
1663
+ sdkOptionsPath: '/out/sdk-options.ts',
1646
1664
  }),
1647
1665
  ).toThrow(/duplicate method 'getCurrentUser' in area 'identity'/);
1648
1666
  });
1649
-
1650
- it('mixes area clients with top-level (no-area) clients', () => {
1651
- const out = generateSdkAggregator({
1652
- topLevelClients: [{ className: 'WebhooksClient', propertyName: 'webhooks', importPath: './webhooks.client.js' }],
1653
- areas: [
1654
- {
1655
- area: 'payments',
1656
- inlineFiles: [],
1657
- subareaClients: [
1658
- {
1659
- propertyName: 'transfers',
1660
- client: { className: 'PaymentsTransfersClient', propertyName: 'transfers', importPath: './payments/transfers.client.js' },
1661
- },
1662
- ],
1663
- },
1664
- ],
1665
- });
1666
- // Sdk has both an area property AND a top-level property
1667
- expect(out).toContain('readonly payments: PaymentsClient');
1668
- expect(out).toContain('readonly webhooks: WebhooksClient');
1669
- expect(out).toContain('this.payments = new PaymentsClient(sdkFetch)');
1670
- expect(out).toContain('this.webhooks = new WebhooksClient(sdkFetch)');
1671
- });
1672
1667
  });
@@ -204,7 +204,7 @@ describe('createTypescriptPlugin (sdk) — area / subarea grouping', () => {
204
204
  return undefined;
205
205
  }
206
206
 
207
- it('emits a leaf <Area><Subarea>Client and nests it under <Area>Client in sdk.ts', async () => {
207
+ it('emits a leaf <Area><Subarea>Client and a separate <Area>Client wrapper file', async () => {
208
208
  const plugin = createTypescriptPlugin({ sdk: { output: { sdk: 'sdk.ts', clients: '{area}/{subarea}.client.ts' } } }, '/project');
209
209
  const ctx = makeCtx('/project');
210
210
  const root = opRoot(
@@ -220,18 +220,25 @@ describe('createTypescriptPlugin (sdk) — area / subarea grouping', () => {
220
220
  expect(leaf!).toContain('export class IdentityInvitationsClient');
221
221
  expect(leaf!).toContain('async createInvitation');
222
222
 
223
- // Aggregator declares IdentityClient + Sdk wiring
223
+ // Separate <area>.client.ts holds the IdentityClient wrapper
224
+ const areaClient = findEmitted(ctx.emitted, 'identity/identity.client.ts');
225
+ expect(areaClient).toBeDefined();
226
+ expect(areaClient!).toContain('export class IdentityClient {');
227
+ expect(areaClient!).toContain('readonly invitations: IdentityInvitationsClient');
228
+ expect(areaClient!).toContain('this.invitations = new IdentityInvitationsClient(fetch)');
229
+
230
+ // sdk.ts just imports + wires the area client onto Sdk
224
231
  const sdk = findEmitted(ctx.emitted, '/sdk.ts');
225
232
  expect(sdk).toBeDefined();
226
- expect(sdk!).toContain('class IdentityClient {');
227
- expect(sdk!).toContain('readonly invitations: IdentityInvitationsClient');
228
- expect(sdk!).toContain('this.invitations = new IdentityInvitationsClient(fetch)');
233
+ expect(sdk!).toContain("import { IdentityClient } from './identity/identity.client.js'");
229
234
  expect(sdk!).toContain('export class Sdk {');
230
235
  expect(sdk!).toContain('readonly identity: IdentityClient');
231
236
  expect(sdk!).toContain('this.identity = new IdentityClient(sdkFetch)');
237
+ // The class declaration itself is no longer in sdk.ts.
238
+ expect(sdk!).not.toContain('class IdentityClient {');
232
239
  });
233
240
 
234
- it('does NOT emit a standalone client file for an area-level (no-subarea) input methods inline into sdk.ts', async () => {
241
+ it('emits a synthesized <area>.client.ts for area-level (no-subarea) inputs instead of inlining into sdk.ts', async () => {
235
242
  const plugin = createTypescriptPlugin({ sdk: { output: { sdk: 'sdk.ts', clients: '{area}/{filename}.client.ts' } } }, '/project');
236
243
  const ctx = makeCtx('/project');
237
244
  const root = opRoot(
@@ -241,19 +248,24 @@ describe('createTypescriptPlugin (sdk) — area / subarea grouping', () => {
241
248
  );
242
249
  await plugin.generateTargets!(inputs([root]), ctx);
243
250
 
244
- // No per-file leaf for the area-level input
251
+ // The contributing file does NOT get its own *.client.ts (per the previous design).
245
252
  for (const path of ctx.emitted.keys()) {
246
253
  expect(path).not.toMatch(/identity[/-]me\.client\.ts$/);
247
254
  }
248
255
 
249
- // Methods land directly on IdentityClient inside sdk.ts
256
+ // The area client lives in <area>/<area>.client.ts and holds the merged methods.
257
+ const areaClient = findEmitted(ctx.emitted, 'identity/identity.client.ts');
258
+ expect(areaClient).toBeDefined();
259
+ expect(areaClient!).toContain('export class IdentityClient {');
260
+ expect(areaClient!).toContain('async getCurrentUser');
261
+
262
+ // sdk.ts just imports it.
250
263
  const sdk = findEmitted(ctx.emitted, '/sdk.ts');
251
- expect(sdk).toBeDefined();
252
- expect(sdk!).toContain('class IdentityClient {');
253
- expect(sdk!).toContain('async getCurrentUser');
264
+ expect(sdk!).toContain("import { IdentityClient } from './identity/identity.client.js'");
265
+ expect(sdk!).not.toContain('class IdentityClient {');
254
266
  });
255
267
 
256
- it('mixes area-level inline methods with subarea property wiring on the same area client', async () => {
268
+ it('mixes area-level inline methods with subarea property wiring on the same <area>.client.ts', async () => {
257
269
  const plugin = createTypescriptPlugin({ sdk: { output: { sdk: 'sdk.ts', clients: '{area}/{filename}.client.ts' } } }, '/project');
258
270
  const ctx = makeCtx('/project');
259
271
  const me = opRoot(
@@ -268,10 +280,10 @@ describe('createTypescriptPlugin (sdk) — area / subarea grouping', () => {
268
280
  );
269
281
  await plugin.generateTargets!(inputs([me, invitations]), ctx);
270
282
 
271
- const sdk = findEmitted(ctx.emitted, '/sdk.ts')!;
272
- expect(sdk).toContain('class IdentityClient {');
273
- expect(sdk).toContain('readonly invitations: IdentityInvitationsClient');
274
- expect(sdk).toContain('async getCurrentUser');
283
+ const areaClient = findEmitted(ctx.emitted, 'identity/identity.client.ts')!;
284
+ expect(areaClient).toContain('export class IdentityClient {');
285
+ expect(areaClient).toContain('readonly invitations: IdentityInvitationsClient');
286
+ expect(areaClient).toContain('async getCurrentUser');
275
287
  });
276
288
 
277
289
  it('preserves legacy flat wiring for files with no area', async () => {