fork of hey-api/openapi-ts because I need some additional things
1import fs from 'node:fs';
2import path from 'node:path';
3import { fileURLToPath } from 'node:url';
4
5import { customClientPlugin } from '@hey-api/custom-client/plugin';
6import { createClient, type UserConfig } from '@hey-api/openapi-ts';
7
8import { getFilePaths, getSpecsPath } from '../../utils';
9import { myClientPlugin } from './custom/client/plugin';
10
11const __filename = fileURLToPath(import.meta.url);
12const __dirname = path.dirname(__filename);
13
14const clients = [
15 '@hey-api/client-angular',
16 '@hey-api/client-axios',
17 '@hey-api/client-fetch',
18 '@hey-api/client-ky',
19 '@hey-api/client-next',
20 '@hey-api/client-nuxt',
21 '@hey-api/client-ofetch',
22] satisfies UserConfig['plugins'];
23
24for (const client of clients) {
25 const namespace = 'clients';
26
27 const outputDir = path.join(__dirname, 'generated', '3.1.x', namespace, client);
28
29 describe(client, () => {
30 const createConfig = (
31 userConfig: Omit<UserConfig, 'input'> & Pick<Partial<UserConfig>, 'input'>,
32 ) => {
33 const output = userConfig.output instanceof Array ? userConfig.output[0] : userConfig.output;
34 return {
35 ...userConfig,
36 input: path.join(getSpecsPath(), '3.1.x', 'full.yaml'),
37 logs: {
38 level: 'silent',
39 },
40 output:
41 typeof output === 'string'
42 ? path.join(outputDir, output)
43 : {
44 ...output,
45 path: path.join(outputDir, output?.path ?? ''),
46 },
47 } as const satisfies UserConfig;
48 };
49
50 const scenarios = [
51 {
52 config: createConfig({
53 output: 'default',
54 plugins: [client],
55 }),
56 description: 'default output',
57 },
58 {
59 config: createConfig({
60 output: 'sdk-client-optional',
61 plugins: [
62 client,
63 {
64 client: true,
65 name: '@hey-api/sdk',
66 },
67 ],
68 }),
69 description: 'SDK with optional client option',
70 },
71 {
72 config: createConfig({
73 output: 'sdk-client-required',
74 plugins: [
75 client,
76 {
77 client: false,
78 name: '@hey-api/sdk',
79 },
80 ],
81 }),
82 description: 'SDK with required client option',
83 },
84 {
85 config: createConfig({
86 output: 'base-url-false',
87 plugins: [
88 {
89 baseUrl: false,
90 name: client,
91 },
92 '@hey-api/typescript',
93 ],
94 }),
95 description: 'client without base URL',
96 },
97 {
98 config: createConfig({
99 output: 'base-url-number',
100 plugins: [
101 {
102 baseUrl: 0,
103 name: client,
104 },
105 '@hey-api/typescript',
106 ],
107 }),
108 description: 'client with numeric base URL',
109 },
110 {
111 config: createConfig({
112 output: 'base-url-string',
113 plugins: [
114 {
115 baseUrl: 'https://foo.com',
116 name: client,
117 },
118 '@hey-api/typescript',
119 ],
120 }),
121 description: 'client with custom string base URL',
122 },
123 {
124 config: createConfig({
125 output: 'base-url-strict',
126 plugins: [
127 {
128 name: client,
129 strictBaseUrl: true,
130 },
131 '@hey-api/typescript',
132 ],
133 }),
134 description: 'client with strict base URL',
135 },
136 {
137 config: createConfig({
138 output: {
139 path: 'tsconfig-nodenext-sdk',
140 tsConfigPath: path.join(__dirname, 'tsconfig', 'tsconfig.nodenext.json'),
141 },
142 plugins: [client, '@hey-api/sdk'],
143 }),
144 description: 'SDK with NodeNext tsconfig',
145 },
146 {
147 config: createConfig({
148 output: {
149 path: 'tsconfig-node16-sdk',
150 tsConfigPath: path.join(__dirname, 'tsconfig', 'tsconfig.node16.json'),
151 },
152 plugins: [client, '@hey-api/sdk'],
153 }),
154 description: 'SDK with Node16 tsconfig',
155 },
156 {
157 config: createConfig({
158 output: {
159 importFileExtension: '.ts',
160 path: 'import-file-extension-ts',
161 },
162 plugins: [client, '@hey-api/sdk'],
163 }),
164 description: 'SDK with .ts import file extensions',
165 },
166 {
167 config: createConfig({
168 output: {
169 clean: false,
170 path: 'clean-false',
171 },
172 plugins: [client, '@hey-api/sdk'],
173 }),
174 description: 'avoid appending extension multiple times | twice',
175 },
176 ];
177
178 it.each(scenarios)('$description', async ({ config, description }) => {
179 await createClient(config);
180 if (description.endsWith('twice')) {
181 await createClient(config);
182 }
183
184 const outputPath = typeof config.output === 'string' ? config.output : config.output.path;
185 const filePaths = getFilePaths(outputPath);
186
187 await Promise.all(
188 filePaths.map(async (filePath) => {
189 const fileContent = fs.readFileSync(filePath, 'utf-8');
190
191 // flaky test reordering client imports, skip
192 if (
193 client === '@hey-api/client-nuxt' &&
194 typeof config.output === 'string' &&
195 config.output.includes('bundle')
196 ) {
197 expect(1).toBe(1);
198 return;
199 }
200
201 await expect(fileContent).toMatchFileSnapshot(
202 path.join(
203 __dirname,
204 '__snapshots__',
205 '3.1.x',
206 namespace,
207 client,
208 filePath.slice(outputDir.length + 1),
209 ),
210 );
211 }),
212 );
213 });
214 });
215}
216
217describe('custom-client', () => {
218 const namespace = 'clients';
219
220 const outputDir = path.join(__dirname, 'generated', '3.1.x', namespace, 'client-custom');
221
222 const createConfig = (
223 userConfig: Omit<UserConfig, 'input'> & Pick<Partial<UserConfig>, 'input'>,
224 ) =>
225 ({
226 ...userConfig,
227 input: path.join(getSpecsPath(), '3.1.x', 'full.yaml'),
228 logs: {
229 level: 'silent',
230 },
231 output: path.join(outputDir, typeof userConfig.output === 'string' ? userConfig.output : ''),
232 }) as const satisfies UserConfig;
233
234 const scenarios = [
235 {
236 config: createConfig({
237 output: 'default',
238 plugins: [customClientPlugin()],
239 }),
240 description: 'default output',
241 },
242 // TODO: enable custom client bundle, it's currently producing CJS output which fails typecheck
243 // {
244 // config: createConfig({
245 // output: 'bundle',
246 // plugins: [
247 // customClientPlugin({
248 // bundle: true,
249 // }),
250 // ],
251 // }),
252 // description: 'default output with bundled client',
253 // },
254 {
255 config: createConfig({
256 output: 'sdk-client-optional',
257 plugins: [
258 customClientPlugin(),
259 {
260 client: true,
261 name: '@hey-api/sdk',
262 },
263 ],
264 }),
265 description: 'SDK with optional client option',
266 },
267 {
268 config: createConfig({
269 output: 'sdk-client-required',
270 plugins: [
271 customClientPlugin(),
272 {
273 client: false,
274 name: '@hey-api/sdk',
275 },
276 ],
277 }),
278 description: 'SDK with required client option',
279 },
280 {
281 config: createConfig({
282 output: 'base-url-false',
283 plugins: [
284 customClientPlugin({
285 baseUrl: false,
286 }),
287 '@hey-api/typescript',
288 ],
289 }),
290 description: 'client without base URL',
291 },
292 {
293 config: createConfig({
294 output: 'base-url-number',
295 plugins: [
296 customClientPlugin({
297 baseUrl: 0,
298 }),
299 '@hey-api/typescript',
300 ],
301 }),
302 description: 'client with numeric base URL',
303 },
304 {
305 config: createConfig({
306 output: 'base-url-string',
307 plugins: [
308 customClientPlugin({
309 baseUrl: 'https://foo.com',
310 }),
311 '@hey-api/typescript',
312 ],
313 }),
314 description: 'client with custom string base URL',
315 },
316 {
317 config: createConfig({
318 output: 'base-url-strict',
319 plugins: [
320 customClientPlugin({
321 strictBaseUrl: true,
322 }),
323 '@hey-api/typescript',
324 ],
325 }),
326 description: 'client with strict base URL',
327 },
328 ];
329
330 it.each(scenarios)('$description', async ({ config }) => {
331 await createClient(config);
332
333 const filePaths = getFilePaths(config.output);
334
335 await Promise.all(
336 filePaths.map(async (filePath) => {
337 const fileContent = fs.readFileSync(filePath, 'utf-8');
338
339 await expect(fileContent).toMatchFileSnapshot(
340 path.join(
341 __dirname,
342 '__snapshots__',
343 '3.1.x',
344 namespace,
345 'client-custom',
346 filePath.slice(outputDir.length + 1),
347 ),
348 );
349 }),
350 );
351 });
352});
353
354describe('my-client', () => {
355 const namespace = 'clients';
356
357 const outputDir = path.join(__dirname, 'generated', '3.1.x', namespace, 'my-client');
358
359 const createConfig = (
360 userConfig: Omit<UserConfig, 'input'> & Pick<Partial<UserConfig>, 'input'>,
361 ) =>
362 ({
363 ...userConfig,
364 input: path.join(getSpecsPath(), '3.1.x', 'full.yaml'),
365 logs: {
366 level: 'silent',
367 },
368 output: path.join(outputDir, typeof userConfig.output === 'string' ? userConfig.output : ''),
369 }) as const satisfies UserConfig;
370
371 const scenarios = [
372 {
373 config: createConfig({
374 output: 'default',
375 plugins: [myClientPlugin()],
376 }),
377 description: 'default output',
378 },
379 {
380 config: createConfig({
381 output: 'bundle',
382 plugins: [
383 myClientPlugin({
384 bundle: true,
385 }),
386 ],
387 }),
388 description: 'default output with bundled client',
389 },
390 {
391 config: createConfig({
392 output: 'sdk-client-optional',
393 plugins: [
394 myClientPlugin(),
395 {
396 client: true,
397 name: '@hey-api/sdk',
398 },
399 ],
400 }),
401 description: 'SDK with optional client option',
402 },
403 {
404 config: createConfig({
405 output: 'sdk-client-required',
406 plugins: [
407 myClientPlugin(),
408 {
409 client: false,
410 name: '@hey-api/sdk',
411 },
412 ],
413 }),
414 description: 'SDK with required client option',
415 },
416 {
417 config: createConfig({
418 output: 'base-url-false',
419 plugins: [
420 myClientPlugin({
421 baseUrl: false,
422 }),
423 '@hey-api/typescript',
424 ],
425 }),
426 description: 'client without base URL',
427 },
428 {
429 config: createConfig({
430 output: 'base-url-number',
431 plugins: [
432 myClientPlugin({
433 baseUrl: 0,
434 }),
435 '@hey-api/typescript',
436 ],
437 }),
438 description: 'client with numeric base URL',
439 },
440 {
441 config: createConfig({
442 output: 'base-url-string',
443 plugins: [
444 myClientPlugin({
445 baseUrl: 'https://foo.com',
446 }),
447 '@hey-api/typescript',
448 ],
449 }),
450 description: 'client with custom string base URL',
451 },
452 {
453 config: createConfig({
454 output: 'base-url-strict',
455 plugins: [
456 myClientPlugin({
457 strictBaseUrl: true,
458 }),
459 '@hey-api/typescript',
460 ],
461 }),
462 description: 'client with strict base URL',
463 },
464 ];
465
466 it.each(scenarios)('$description', async ({ config }) => {
467 await createClient(config);
468
469 const filePaths = getFilePaths(config.output);
470
471 await Promise.all(
472 filePaths.map(async (filePath) => {
473 const fileContent = fs.readFileSync(filePath, 'utf-8');
474
475 await expect(fileContent).toMatchFileSnapshot(
476 path.join(
477 __dirname,
478 '__snapshots__',
479 '3.1.x',
480 namespace,
481 'my-client',
482 filePath.slice(outputDir.length + 1),
483 ),
484 );
485 }),
486 );
487 });
488});