@contractkit/plugin-typescript 0.19.1 → 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
  });
@@ -11,6 +11,7 @@ function makeCtx(rootDir = '/project', options: Record<string, unknown> = {}): P
11
11
  rootDir,
12
12
  options,
13
13
  cacheEnabled: true,
14
+ cacheDir: `${rootDir}/.contractkit/cache`,
14
15
  emitFile: (outPath: string, content: string) => {
15
16
  emitted.set(outPath, content);
16
17
  },
@@ -203,7 +204,7 @@ describe('createTypescriptPlugin (sdk) — area / subarea grouping', () => {
203
204
  return undefined;
204
205
  }
205
206
 
206
- 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 () => {
207
208
  const plugin = createTypescriptPlugin({ sdk: { output: { sdk: 'sdk.ts', clients: '{area}/{subarea}.client.ts' } } }, '/project');
208
209
  const ctx = makeCtx('/project');
209
210
  const root = opRoot(
@@ -219,18 +220,25 @@ describe('createTypescriptPlugin (sdk) — area / subarea grouping', () => {
219
220
  expect(leaf!).toContain('export class IdentityInvitationsClient');
220
221
  expect(leaf!).toContain('async createInvitation');
221
222
 
222
- // 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
223
231
  const sdk = findEmitted(ctx.emitted, '/sdk.ts');
224
232
  expect(sdk).toBeDefined();
225
- expect(sdk!).toContain('class IdentityClient {');
226
- expect(sdk!).toContain('readonly invitations: IdentityInvitationsClient');
227
- expect(sdk!).toContain('this.invitations = new IdentityInvitationsClient(fetch)');
233
+ expect(sdk!).toContain("import { IdentityClient } from './identity/identity.client.js'");
228
234
  expect(sdk!).toContain('export class Sdk {');
229
235
  expect(sdk!).toContain('readonly identity: IdentityClient');
230
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 {');
231
239
  });
232
240
 
233
- 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 () => {
234
242
  const plugin = createTypescriptPlugin({ sdk: { output: { sdk: 'sdk.ts', clients: '{area}/{filename}.client.ts' } } }, '/project');
235
243
  const ctx = makeCtx('/project');
236
244
  const root = opRoot(
@@ -240,19 +248,24 @@ describe('createTypescriptPlugin (sdk) — area / subarea grouping', () => {
240
248
  );
241
249
  await plugin.generateTargets!(inputs([root]), ctx);
242
250
 
243
- // No per-file leaf for the area-level input
251
+ // The contributing file does NOT get its own *.client.ts (per the previous design).
244
252
  for (const path of ctx.emitted.keys()) {
245
253
  expect(path).not.toMatch(/identity[/-]me\.client\.ts$/);
246
254
  }
247
255
 
248
- // 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.
249
263
  const sdk = findEmitted(ctx.emitted, '/sdk.ts');
250
- expect(sdk).toBeDefined();
251
- expect(sdk!).toContain('class IdentityClient {');
252
- expect(sdk!).toContain('async getCurrentUser');
264
+ expect(sdk!).toContain("import { IdentityClient } from './identity/identity.client.js'");
265
+ expect(sdk!).not.toContain('class IdentityClient {');
253
266
  });
254
267
 
255
- 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 () => {
256
269
  const plugin = createTypescriptPlugin({ sdk: { output: { sdk: 'sdk.ts', clients: '{area}/{filename}.client.ts' } } }, '/project');
257
270
  const ctx = makeCtx('/project');
258
271
  const me = opRoot(
@@ -267,10 +280,10 @@ describe('createTypescriptPlugin (sdk) — area / subarea grouping', () => {
267
280
  );
268
281
  await plugin.generateTargets!(inputs([me, invitations]), ctx);
269
282
 
270
- const sdk = findEmitted(ctx.emitted, '/sdk.ts')!;
271
- expect(sdk).toContain('class IdentityClient {');
272
- expect(sdk).toContain('readonly invitations: IdentityInvitationsClient');
273
- 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');
274
287
  });
275
288
 
276
289
  it('preserves legacy flat wiring for files with no area', async () => {