@contractkit/plugin-typescript 0.20.0 → 0.22.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.
@@ -807,9 +807,9 @@ describe('generateOp — route modifiers JSDoc', () => {
807
807
  expect(out).toContain('anonymous access, no security required');
808
808
  });
809
809
 
810
- it('emits no annotation for security with roles', () => {
810
+ it('emits no annotation for security with requireMfa', () => {
811
811
  const op = opOperation('get', {
812
- security: { roles: ['admin'], loc: { file: 'test.op', line: 1 } },
812
+ security: { requireMfa: true, loc: { file: 'test.op', line: 1 } },
813
813
  });
814
814
  const root = opRoot([opRoute('/users', [op])]);
815
815
  const out = generateOp(root);
@@ -867,7 +867,7 @@ describe('generateOp — route modifiers JSDoc', () => {
867
867
 
868
868
  it('does not import requireSignature when no signature is set', () => {
869
869
  const op = opOperation('get', {
870
- security: { roles: ['admin'], loc: { file: 'test.op', line: 1 } },
870
+ security: { requireMfa: true, loc: { file: 'test.op', line: 1 } },
871
871
  });
872
872
  const root = opRoot([opRoute('/users', [op])]);
873
873
  const out = generateOp(root);
@@ -876,7 +876,7 @@ describe('generateOp — route modifiers JSDoc', () => {
876
876
  });
877
877
  });
878
878
 
879
- // ─── Security (roles) middleware ────────────────────────────────
879
+ // ─── Security (requireMfa) middleware ───────────────────────────
880
880
 
881
881
  describe('security middleware', () => {
882
882
  it('injects requireSecurity() with no args for unannotated routes', () => {
@@ -884,27 +884,26 @@ describe('generateOp — route modifiers JSDoc', () => {
884
884
  const root = opRoot([opRoute('/users', [op])]);
885
885
  const out = generateOp(root);
886
886
  expect(out).toContain(`import { ServerKitRouter, bodyParserMiddleware, requireSecurity }`);
887
- expect(out).toContain(`requireSecurity({ })`);
887
+ expect(out).toContain(`requireSecurity()`);
888
888
  });
889
889
 
890
- it('injects requireSecurity with roles when roles are set', () => {
890
+ it('injects requireSecurity with requireMfa: true when set', () => {
891
891
  const op = opOperation('get', {
892
- security: { roles: ['admin'], loc: { file: 'test.op', line: 1 } },
892
+ security: { requireMfa: true, loc: { file: 'test.op', line: 1 } },
893
893
  });
894
894
  const root = opRoot([opRoute('/users', [op])]);
895
895
  const out = generateOp(root);
896
- expect(out).toContain(`requireSecurity({ roles: ['admin'] })`);
896
+ expect(out).toContain(`requireSecurity({ requireMfa: true })`);
897
897
  });
898
898
 
899
- it('passes multiple roles as an array', () => {
899
+ it('injects requireSecurity with requireMfa: false when set', () => {
900
900
  const op = opOperation('get', {
901
- security: { roles: ['admin', 'support'], loc: { file: 'test.op', line: 1 } },
901
+ security: { requireMfa: false, loc: { file: 'test.op', line: 1 } },
902
902
  });
903
- const root = opRoot([opRoute('/users', [op])]);
904
- const routeLine = generateOp(root)
903
+ const routeLine = generateOp(opRoot([opRoute('/users', [op])]))
905
904
  .split('\n')
906
905
  .find(l => l.includes('.get('));
907
- expect(routeLine).toContain(`requireSecurity({ roles: ['admin', 'support'] })`);
906
+ expect(routeLine).toContain(`requireSecurity({ requireMfa: false })`);
908
907
  });
909
908
 
910
909
  it('does not inject requireSecurity for public (security: none) routes', () => {
@@ -924,7 +923,7 @@ describe('generateOp — route modifiers JSDoc', () => {
924
923
 
925
924
  it('places requireSecurity before bodyParserMiddleware in the route line', () => {
926
925
  const op = opOperation('post', {
927
- security: { roles: ['admin'], loc: { file: 'test.op', line: 1 } },
926
+ security: { requireMfa: true, loc: { file: 'test.op', line: 1 } },
928
927
  request: opRequest('Payload'),
929
928
  });
930
929
  const root = opRoot([opRoute('/users', [op])]);
@@ -941,7 +940,7 @@ describe('generateOp — route modifiers JSDoc', () => {
941
940
  it('places requireSecurity before requireSignature when both are set', () => {
942
941
  const op = opOperation('post', {
943
942
  signature: 'MY_KEY',
944
- security: { roles: ['admin'], loc: { file: 'test.op', line: 1 } },
943
+ security: { requireMfa: true, loc: { file: 'test.op', line: 1 } },
945
944
  request: opRequest('Payload'),
946
945
  });
947
946
  const root = opRoot([opRoute('/webhooks', [op])]);
@@ -958,7 +957,7 @@ describe('generateOp — route modifiers JSDoc', () => {
958
957
  it('imports both requireSecurity and requireSignature when both are set', () => {
959
958
  const op = opOperation('post', {
960
959
  signature: 'MY_KEY',
961
- security: { roles: ['admin'], loc: { file: 'test.op', line: 1 } },
960
+ security: { requireMfa: true, loc: { file: 'test.op', line: 1 } },
962
961
  request: opRequest('Payload'),
963
962
  });
964
963
  const root = opRoot([opRoute('/webhooks', [op])]);
@@ -966,13 +965,13 @@ describe('generateOp — route modifiers JSDoc', () => {
966
965
  expect(out).toContain(`import { ServerKitRouter, bodyParserMiddleware, requireSecurity, requireSignature }`);
967
966
  });
968
967
 
969
- it('works with route-level roles security', () => {
968
+ it('works with route-level requireMfa security', () => {
970
969
  const op = opOperation('get');
971
970
  const route = opRoute('/users', [op]);
972
- route.security = { roles: ['admin'], loc: { file: 'test.op', line: 1 } };
971
+ route.security = { requireMfa: true, loc: { file: 'test.op', line: 1 } };
973
972
  const root = opRoot([route]);
974
973
  const out = generateOp(root);
975
- expect(out).toContain(`requireSecurity({ roles: ['admin'] })`);
974
+ expect(out).toContain(`requireSecurity({ requireMfa: true })`);
976
975
  });
977
976
  });
978
977
  });
@@ -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 () => {