@contractkit/plugin-typescript 0.28.2 → 0.29.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.
@@ -658,6 +658,77 @@ describe('generateOperation', () => {
658
658
  const output = generateOp(root, { modelsWithOutput: new Set(['AuthToken']) });
659
659
  expect(output).toContain('result: AuthTokenOutput[]');
660
660
  });
661
+
662
+ // Scalar response bodies used to emit the .ck scalar name verbatim (`result: binary`),
663
+ // which only happened to compile for `string`.
664
+ describe('scalar response bodies map to the server-side TypeScript type', () => {
665
+ const cases: Array<[string, string]> = [
666
+ ['binary', 'Buffer'],
667
+ ['int', 'number'],
668
+ ['number', 'number'],
669
+ ['bigint', 'bigint'],
670
+ ['boolean', 'boolean'],
671
+ ['string', 'string'],
672
+ ['uuid', 'string'],
673
+ ['email', 'string'],
674
+ ['url', 'string'],
675
+ ['datetime', 'DateTime'],
676
+ ['date', 'DateTime'],
677
+ ['time', 'DateTime'],
678
+ ['duration', 'Duration'],
679
+ ['interval', 'string'],
680
+ ['json', '_JsonValue'],
681
+ ['object', 'Record<string, unknown>'],
682
+ ['unknown', 'unknown'],
683
+ ['null', 'null'],
684
+ ];
685
+
686
+ for (const [scalar, tsType] of cases) {
687
+ it(`renders ${scalar} as ${tsType}`, () => {
688
+ const root = opRoot([
689
+ opRoute('/x', [
690
+ opOperation('get', {
691
+ responses: [opResponse(200, scalarType(scalar as never), 'application/octet-stream')],
692
+ }),
693
+ ]),
694
+ ]);
695
+ expect(generateOp(root)).toContain(`const result: ${tsType} = await service.list();`);
696
+ });
697
+ }
698
+
699
+ it('renders an array of binary as Buffer[]', () => {
700
+ const root = opRoot([
701
+ opRoute('/x', [opOperation('get', { responses: [opResponse(200, arrayType(scalarType('binary')), 'application/json')] })]),
702
+ ]);
703
+ expect(generateOp(root)).toContain('const result: Buffer[] = await service.list();');
704
+ });
705
+ });
706
+
707
+ describe('luxon imports cover every scalar that references a luxon class', () => {
708
+ it('imports Duration for a duration response body', () => {
709
+ const root = opRoot([opRoute('/x', [opOperation('get', { responses: [opResponse(200, scalarType('duration'), 'application/json')] })])]);
710
+ expect(generateOp(root)).toContain("import { Duration } from 'luxon';");
711
+ });
712
+
713
+ it('imports Interval and emits the _ZodInterval helper for an interval body', () => {
714
+ const root = opRoot([opRoute('/x', [opOperation('get', { responses: [opResponse(200, scalarType('interval'), 'application/json')] })])]);
715
+ const output = generateOp(root);
716
+ expect(output).toContain("import { Interval } from 'luxon';");
717
+ expect(output).toContain('const _ZodInterval =');
718
+ });
719
+
720
+ it('imports DateTime and Duration together when both are used', () => {
721
+ const root = opRoot([
722
+ opRoute('/x', [
723
+ opOperation('post', {
724
+ request: opRequest(scalarType('datetime')),
725
+ responses: [opResponse(200, scalarType('duration'), 'application/json')],
726
+ }),
727
+ ]),
728
+ ]);
729
+ expect(generateOp(root)).toContain("import { DateTime, Duration } from 'luxon';");
730
+ });
731
+ });
661
732
  });
662
733
 
663
734
  // ─── Service inference ────────────────────────────────────────
@@ -98,6 +98,54 @@ describe('generatePlainTypes', () => {
98
98
  expect(output).toContain('o: Record<string, unknown>;');
99
99
  expect(output).toContain('bin: Blob;');
100
100
  });
101
+
102
+ // `binary` is the one scalar with no runtime-independent TypeScript type: a fetch client
103
+ // sees a Blob, a Koa handler sees the Buffer that _ZodBinary validates.
104
+ describe('binary follows the render target', () => {
105
+ const ctx = (target: 'client' | 'server') => ({
106
+ modelOutPaths: new Map<string, string>(),
107
+ currentOutPath: 'out.ts',
108
+ target,
109
+ });
110
+
111
+ it('renders Buffer for the server target', () => {
112
+ const root = contractRoot([model('M', [field('bin', scalarType('binary'))])]);
113
+ expect(generatePlainTypes(root, ctx('server'))).toContain('bin: Buffer;');
114
+ });
115
+
116
+ it('renders Blob for the client target', () => {
117
+ const root = contractRoot([model('M', [field('bin', scalarType('binary'))])]);
118
+ expect(generatePlainTypes(root, ctx('client'))).toContain('bin: Blob;');
119
+ });
120
+
121
+ it('defaults to the client target when unset', () => {
122
+ const root = contractRoot([model('M', [field('bin', scalarType('binary'))])]);
123
+ expect(generatePlainTypes(root)).toContain('bin: Blob;');
124
+ });
125
+
126
+ it('applies to nested and compound positions', () => {
127
+ const root = contractRoot([
128
+ model('M', [
129
+ field('list', arrayType(scalarType('binary'))),
130
+ field('nested', inlineObjectType([field('bin', scalarType('binary'))])),
131
+ field('map', recordType(scalarType('string'), scalarType('binary'))),
132
+ ]),
133
+ ]);
134
+ const output = generatePlainTypes(root, ctx('server'));
135
+ expect(output).toContain('list: Buffer[];');
136
+ expect(output).toContain('nested: { bin: Buffer };');
137
+ expect(output).toContain('map: Record<string, Buffer>;');
138
+ });
139
+
140
+ it('applies to type aliases and their Input variants', () => {
141
+ const root = contractRoot([
142
+ model('Blob1', [field('bin', scalarType('binary')), field('secret', scalarType('binary'), { visibility: 'readonly' })]),
143
+ ]);
144
+ const output = generatePlainTypes(root, { ...ctx('server'), modelsWithInput: new Set(['Blob1']) });
145
+ expect(output).toContain('bin: Buffer;');
146
+ expect(output).not.toContain('Blob;');
147
+ });
148
+ });
101
149
  });
102
150
 
103
151
  // ─── Compound types ───────────────────────────────────────────
@@ -120,6 +120,43 @@ describe('createTypescriptPlugin (server)', () => {
120
120
  expect(typeContent).not.toContain('z.');
121
121
  expect(typeContent).toContain('export interface User');
122
122
  });
123
+
124
+ it('renders binary as Buffer in server plain types', async () => {
125
+ const plugin = createTypescriptPlugin({ server: { output: { types: 'src/types/{filename}.ts' } } }, '/project');
126
+ const ctx = makeCtx('/project');
127
+ const contractRoots = [contractRoot([model('Upload', [field('data', scalarType('binary'))])], '/project/contracts/uploads.ck')];
128
+ await plugin.generateTargets!(inputs([], contractRoots as any), ctx);
129
+ const typeContent = [...ctx.emitted.values()][0]!;
130
+ expect(typeContent).toContain('data: Buffer;');
131
+ expect(typeContent).not.toContain('Blob');
132
+ });
133
+
134
+ it('renders binary as Blob in SDK plain types', async () => {
135
+ const plugin = createTypescriptPlugin({ sdk: { output: { types: 'src/types/{filename}.ts' } } }, '/project');
136
+ const ctx = makeCtx('/project');
137
+ const contractRoots = [contractRoot([model('Upload', [field('data', scalarType('binary'))])], '/project/contracts/uploads.ck')];
138
+ await plugin.generateTargets!(inputs([], contractRoots as any), ctx);
139
+ const typeContent = [...ctx.emitted.values()].find(c => c.includes('interface Upload'))!;
140
+ expect(typeContent).toContain('data: Blob;');
141
+ });
142
+
143
+ it('honors types.target on the standalone types sub-generator', async () => {
144
+ const contractRoots = [contractRoot([model('Upload', [field('data', scalarType('binary'))])], '/project/contracts/uploads.ck')];
145
+
146
+ const serverCtx = makeCtx('/project');
147
+ await createTypescriptPlugin({ types: { output: 'src/{filename}.types.ts', target: 'server' } }, '/project').generateTargets!(
148
+ inputs([], contractRoots as any),
149
+ serverCtx,
150
+ );
151
+ expect([...serverCtx.emitted.values()][0]!).toContain('data: Buffer;');
152
+
153
+ const defaultCtx = makeCtx('/project');
154
+ await createTypescriptPlugin({ types: { output: 'src/{filename}.types.ts' } }, '/project').generateTargets!(
155
+ inputs([], contractRoots as any),
156
+ defaultCtx,
157
+ );
158
+ expect([...defaultCtx.emitted.values()][0]!).toContain('data: Blob;');
159
+ });
123
160
  });
124
161
 
125
162
  describe('generated content', () => {