diff --git a/.changeset/dry-jeans-ring.md b/.changeset/dry-jeans-ring.md new file mode 100644 index 0000000000..13638cde51 --- /dev/null +++ b/.changeset/dry-jeans-ring.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +feat: support multiple configurations diff --git a/docs/openapi-ts/configuration.md b/docs/openapi-ts/configuration.md index c0d117a062..02fece76eb 100644 --- a/docs/openapi-ts/configuration.md +++ b/docs/openapi-ts/configuration.md @@ -38,31 +38,6 @@ export default { Alternatively, you can use `openapi-ts.config.js` and configure the export statement depending on your project setup. - - - ## Input You must provide an input so we can load your OpenAPI specification. @@ -163,6 +138,108 @@ Plugins are responsible for generating artifacts from your input. By default, He You can learn more on the [Output](/openapi-ts/output) page. +## Advanced + +More complex configuration scenarios can be handled by providing an array of inputs, outputs, or configurations. + +### Multiple jobs + +Throughout this documentation, we generally reference single job configurations. However, you can easily run multiple jobs by providing an array of configuration objects. + +::: code-group + +```js [config] +export default [ + { + input: 'foo.yaml', + output: 'src/foo', + }, + { + input: 'bar.yaml', + output: 'src/bar', + }, +]; +``` + +```md [example] +src/ +├── foo/ +│ ├── client/ +│ ├── core/ +│ ├── client.gen.ts +│ ├── index.ts +│ ├── sdk.gen.ts +│ └── types.gen.ts +└── bar/ +├── client/ +├── core/ +├── client.gen.ts +├── index.ts +├── sdk.gen.ts +└── types.gen.ts +``` + +::: + +### Job matrix + +Reusing configuration across multiple jobs is possible by defining a job matrix. You can create a job matrix by providing `input` and `output` arrays of matching length. + +::: code-group + +```js [config] +export default { + input: ['foo.yaml', 'bar.yaml'], + output: ['src/foo', 'src/bar'], +}; +``` + +```md [example] +src/ +├── foo/ +│ ├── client/ +│ ├── core/ +│ ├── client.gen.ts +│ ├── index.ts +│ ├── sdk.gen.ts +│ └── types.gen.ts +└── bar/ +├── client/ +├── core/ +├── client.gen.ts +├── index.ts +├── sdk.gen.ts +└── types.gen.ts +``` + +::: + +### Merging inputs + +You can merge inputs by defining multiple inputs and a single output. + +::: code-group + +```js [config] +export default { + input: ['foo.yaml', 'bar.yaml'], + output: 'src/client', +}; +``` + +```md [example] +src/ +└── client/ +├── client/ +├── core/ +├── client.gen.ts +├── index.ts +├── sdk.gen.ts +└── types.gen.ts +``` + +::: + ## API You can view the complete list of options in the [UserConfig](https://github.com/hey-api/openapi-ts/blob/main/packages/openapi-ts/src/types/config.d.ts) interface. diff --git a/docs/openapi-ts/configuration/input.md b/docs/openapi-ts/configuration/input.md index a6b5f199e6..6269d1c769 100644 --- a/docs/openapi-ts/configuration/input.md +++ b/docs/openapi-ts/configuration/input.md @@ -54,6 +54,8 @@ export default { ::: +You can learn more about complex use cases in the [Advanced](/openapi-ts/configuration#advanced) section. + ::: info If you use an HTTPS URL with a self-signed certificate in development, you will need to set [`NODE_TLS_REJECT_UNAUTHORIZED=0`](https://github.com/hey-api/openapi-ts/issues/276#issuecomment-2043143501) in your environment. ::: diff --git a/docs/openapi-ts/configuration/output.md b/docs/openapi-ts/configuration/output.md index d2d04d4292..a47ace0e4d 100644 --- a/docs/openapi-ts/configuration/output.md +++ b/docs/openapi-ts/configuration/output.md @@ -34,6 +34,8 @@ export default { ::: +You can learn more about complex use cases in the [Advanced](/openapi-ts/configuration#advanced) section. + ## File Name You can customize the naming and casing pattern for files using the `fileName` option. diff --git a/package.json b/package.json index 90ff0da17c..7698a1361e 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "rollup": "4.31.0", "rollup-plugin-dts": "6.1.1", "tsup": "8.4.0", - "turbo": "2.5.6", + "turbo": "2.5.8", "typescript": "5.8.3", "typescript-eslint": "8.29.1", "vitest": "3.1.1" diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 945ef7412e..1b79432361 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -60,9 +60,11 @@ export default defineNuxtModule({ config.watch = false; } + const output = + config.output instanceof Array ? config.output[0] : config.output; const folder = path.resolve( nuxt.options.rootDir, - typeof config.output === 'string' ? config.output : config.output.path, + typeof output === 'string' ? output : (output?.path ?? ''), ); nuxt.options.alias[options.alias!] = folder; diff --git a/packages/openapi-ts-tests/main/test/2.0.x.test.ts b/packages/openapi-ts-tests/main/test/2.0.x.test.ts index 4be3857bd0..e2d62e34e2 100644 --- a/packages/openapi-ts-tests/main/test/2.0.x.test.ts +++ b/packages/openapi-ts-tests/main/test/2.0.x.test.ts @@ -15,13 +15,15 @@ const version = '2.0.x'; const outputDir = path.join(__dirname, 'generated', version); describe(`OpenAPI ${version}`, () => { - const createConfig = (userConfig: UserConfig): UserConfig => { + const createConfig = (userConfig: UserConfig) => { + const input = + userConfig.input instanceof Array + ? userConfig.input[0] + : userConfig.input; const inputPath = path.join( getSpecsPath(), version, - typeof userConfig.input === 'string' - ? userConfig.input - : (userConfig.input.path as string), + typeof input === 'string' ? input : ((input?.path as string) ?? ''), ); return { plugins: ['@hey-api/typescript'], @@ -40,7 +42,7 @@ describe(`OpenAPI ${version}`, () => { outputDir, typeof userConfig.output === 'string' ? userConfig.output : '', ), - }; + } as const satisfies UserConfig; }; const scenarios = [ @@ -391,9 +393,7 @@ describe(`OpenAPI ${version}`, () => { it.each(scenarios)('$description', async ({ config }) => { await createClient(config); - const outputPath = - typeof config.output === 'string' ? config.output : config.output.path; - const filePaths = getFilePaths(outputPath); + const filePaths = getFilePaths(config.output); await Promise.all( filePaths.map(async (filePath) => { diff --git a/packages/openapi-ts-tests/main/test/3.0.x.test.ts b/packages/openapi-ts-tests/main/test/3.0.x.test.ts index 50ba49b5e8..e18036add2 100644 --- a/packages/openapi-ts-tests/main/test/3.0.x.test.ts +++ b/packages/openapi-ts-tests/main/test/3.0.x.test.ts @@ -15,13 +15,15 @@ const version = '3.0.x'; const outputDir = path.join(__dirname, 'generated', version); describe(`OpenAPI ${version}`, () => { - const createConfig = (userConfig: UserConfig): UserConfig => { + const createConfig = (userConfig: UserConfig) => { + const input = + userConfig.input instanceof Array + ? userConfig.input[0] + : userConfig.input; const inputPath = path.join( getSpecsPath(), version, - typeof userConfig.input === 'string' - ? userConfig.input - : (userConfig.input.path as string), + typeof input === 'string' ? input : ((input?.path as string) ?? ''), ); return { plugins: ['@hey-api/typescript'], @@ -40,7 +42,7 @@ describe(`OpenAPI ${version}`, () => { outputDir, typeof userConfig.output === 'string' ? userConfig.output : '', ), - }; + } as const satisfies UserConfig; }; const scenarios = [ @@ -668,9 +670,7 @@ describe(`OpenAPI ${version}`, () => { it.each(scenarios)('$description', async ({ config }) => { await createClient(config); - const outputPath = - typeof config.output === 'string' ? config.output : config.output.path; - const filePaths = getFilePaths(outputPath); + const filePaths = getFilePaths(config.output); await Promise.all( filePaths.map(async (filePath) => { diff --git a/packages/openapi-ts-tests/main/test/3.1.x.test.ts b/packages/openapi-ts-tests/main/test/3.1.x.test.ts index ab78014bd5..04f22d55f9 100644 --- a/packages/openapi-ts-tests/main/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/main/test/3.1.x.test.ts @@ -15,14 +15,20 @@ const version = '3.1.x'; const outputDir = path.join(__dirname, 'generated', version); describe(`OpenAPI ${version}`, () => { - const createConfig = (userConfig: UserConfig): UserConfig => { + const createConfig = (userConfig: UserConfig) => { + const input = + userConfig.input instanceof Array + ? userConfig.input[0] + : userConfig.input; const inputPath = path.join( getSpecsPath(), version, - typeof userConfig.input === 'string' - ? userConfig.input - : (userConfig.input.path as string), + typeof input === 'string' ? input : ((input?.path as string) ?? ''), ); + const output = + userConfig.output instanceof Array + ? userConfig.output[0] + : userConfig.output; return { plugins: ['@hey-api/typescript'], ...userConfig, @@ -38,9 +44,9 @@ describe(`OpenAPI ${version}`, () => { }, output: path.join( outputDir, - typeof userConfig.output === 'string' ? userConfig.output : '', + typeof output === 'string' ? output : (output?.path ?? ''), ), - }; + } as const satisfies UserConfig; }; const scenarios = [ @@ -969,9 +975,7 @@ describe(`OpenAPI ${version}`, () => { it.each(scenarios)('$description', async ({ config }) => { await createClient(config); - const outputPath = - typeof config.output === 'string' ? config.output : config.output.path; - const filePaths = getFilePaths(outputPath); + const filePaths = getFilePaths(config.output); await Promise.all( filePaths.map(async (filePath) => { diff --git a/packages/openapi-ts-tests/main/test/bin.test.ts b/packages/openapi-ts-tests/main/test/bin.test.ts index 47234fa51c..4d6a848411 100755 --- a/packages/openapi-ts-tests/main/test/bin.test.ts +++ b/packages/openapi-ts-tests/main/test/bin.test.ts @@ -195,8 +195,6 @@ describe('bin', () => { '--dry-run', 'true', ]); - expect(result.stdout.toString()).toBe(''); - expect(result.stderr.toString()).toContain('encountered an error'); expect(result.stderr.toString()).toContain('missing input'); }); @@ -216,8 +214,6 @@ describe('bin', () => { '--dry-run', 'true', ]); - expect(result.stdout.toString()).toBe(''); - expect(result.stderr.toString()).toContain('encountered an error'); expect(result.stderr.toString()).toContain('missing output'); }); @@ -240,7 +236,6 @@ describe('bin', () => { '--dry-run', 'true', ]); - expect(result.stdout.toString()).toBe(''); expect(result.stderr.toString()).toContain( `error: unknown option '--unknown'`, ); @@ -285,21 +280,19 @@ describe('cli', () => { '--output', path.resolve(__dirname, 'generated', 'bin'), '--debug', - '--exportCore', - 'false', '--plugins', '--useOptions', 'false', '--dry-run', 'true', ]); - expect(result.stderr.toString()).toContain('debug: true'); - expect(result.stderr.toString()).toContain('dryRun: true'); - expect(result.stderr.toString()).toContain('exportCore: false'); - expect(result.stderr.toString()).not.toContain('@hey-api/typescript'); - expect(result.stderr.toString()).not.toContain('@hey-api/sdk'); - expect(result.stderr.toString()).not.toContain('@hey-api/schemas'); - expect(result.stderr.toString()).toContain('useOptions: false'); + const stderr = result.stderr.toString(); + expect(stderr).toMatch(/level: 'debug'/); + expect(stderr).toMatch(/dryRun: true/); + expect(stderr).not.toMatch(/@hey-api\/schemas/); + expect(stderr).not.toMatch(/@hey-api\/sdk/); + expect(stderr).not.toMatch(/@hey-api\/typescript/); + expect(stderr).toMatch(/useOptions: false/); }); it('handles true booleans', () => { @@ -320,8 +313,6 @@ describe('cli', () => { '--client', '@hey-api/client-fetch', '--debug', - '--exportCore', - 'true', '--plugins', '@hey-api/schemas', '@hey-api/sdk', @@ -331,13 +322,13 @@ describe('cli', () => { '--dry-run', 'true', ]); - expect(result.stderr.toString()).toContain('debug: true'); - expect(result.stderr.toString()).toContain('dryRun: true'); - expect(result.stderr.toString()).toContain('exportCore: true'); - expect(result.stderr.toString()).toContain('@hey-api/typescript'); - expect(result.stderr.toString()).toContain('@hey-api/sdk'); - expect(result.stderr.toString()).toContain('@hey-api/schemas'); - expect(result.stderr.toString()).toContain('useOptions: true'); + const stderr = result.stderr.toString(); + expect(stderr).toMatch(/level: 'debug'/); + expect(stderr).toMatch(/dryRun: true/); + expect(stderr).toMatch(/@hey-api\/schemas/); + expect(stderr).toMatch(/@hey-api\/sdk/); + expect(stderr).toMatch(/@hey-api\/typescript/); + expect(stderr).toMatch(/useOptions: true/); }); it('handles optional booleans', () => { @@ -358,7 +349,6 @@ describe('cli', () => { '--client', '@hey-api/client-fetch', '--debug', - '--exportCore', '--plugins', '@hey-api/schemas', '@hey-api/sdk', @@ -367,12 +357,12 @@ describe('cli', () => { '--dry-run', 'true', ]); - expect(result.stderr.toString()).toContain('debug: true'); - expect(result.stderr.toString()).toContain('dryRun: true'); - expect(result.stderr.toString()).toContain('exportCore: true'); - expect(result.stderr.toString()).toContain('@hey-api/schemas'); - expect(result.stderr.toString()).toContain('@hey-api/sdk'); - expect(result.stderr.toString()).toContain('@hey-api/typescript'); - expect(result.stderr.toString()).toContain('useOptions: true'); + const stderr = result.stderr.toString(); + expect(stderr).toMatch(/level: 'debug'/); + expect(stderr).toMatch(/dryRun: true/); + expect(stderr).toMatch(/@hey-api\/schemas/); + expect(stderr).toMatch(/@hey-api\/sdk/); + expect(stderr).toMatch(/@hey-api\/typescript/); + expect(stderr).toMatch(/useOptions: true/); }); }); diff --git a/packages/openapi-ts-tests/main/test/clients.test.ts b/packages/openapi-ts-tests/main/test/clients.test.ts index 628664c75f..f0a85e9bee 100644 --- a/packages/openapi-ts-tests/main/test/clients.test.ts +++ b/packages/openapi-ts-tests/main/test/clients.test.ts @@ -36,20 +36,26 @@ for (const client of clients) { const createConfig = ( userConfig: Omit & Pick, 'input'>, - ): UserConfig => ({ - ...userConfig, - input: path.join(getSpecsPath(), '3.1.x', 'full.yaml'), - logs: { - level: 'silent', - }, - output: - typeof userConfig.output === 'string' - ? path.join(outputDir, userConfig.output) - : { - ...userConfig.output, - path: path.join(outputDir, userConfig.output.path), - }, - }); + ) => { + const output = + userConfig.output instanceof Array + ? userConfig.output[0] + : userConfig.output; + return { + ...userConfig, + input: path.join(getSpecsPath(), '3.1.x', 'full.yaml'), + logs: { + level: 'silent', + }, + output: + typeof output === 'string' + ? path.join(outputDir, output) + : { + ...output, + path: path.join(outputDir, output?.path ?? ''), + }, + } as const satisfies UserConfig; + }; const scenarios = [ { @@ -226,17 +232,18 @@ describe('custom-client', () => { const createConfig = ( userConfig: Omit & Pick, 'input'>, - ): UserConfig => ({ - ...userConfig, - input: path.join(getSpecsPath(), '3.1.x', 'full.yaml'), - logs: { - level: 'silent', - }, - output: path.join( - outputDir, - typeof userConfig.output === 'string' ? userConfig.output : '', - ), - }); + ) => + ({ + ...userConfig, + input: path.join(getSpecsPath(), '3.1.x', 'full.yaml'), + logs: { + level: 'silent', + }, + output: path.join( + outputDir, + typeof userConfig.output === 'string' ? userConfig.output : '', + ), + }) as const satisfies UserConfig; const scenarios = [ { @@ -336,9 +343,7 @@ describe('custom-client', () => { it.each(scenarios)('$description', async ({ config }) => { await createClient(config); - const outputPath = - typeof config.output === 'string' ? config.output : config.output.path; - const filePaths = getFilePaths(outputPath); + const filePaths = getFilePaths(config.output); await Promise.all( filePaths.map(async (filePath) => { @@ -372,17 +377,18 @@ describe('my-client', () => { const createConfig = ( userConfig: Omit & Pick, 'input'>, - ): UserConfig => ({ - ...userConfig, - input: path.join(getSpecsPath(), '3.1.x', 'full.yaml'), - logs: { - level: 'silent', - }, - output: path.join( - outputDir, - typeof userConfig.output === 'string' ? userConfig.output : '', - ), - }); + ) => + ({ + ...userConfig, + input: path.join(getSpecsPath(), '3.1.x', 'full.yaml'), + logs: { + level: 'silent', + }, + output: path.join( + outputDir, + typeof userConfig.output === 'string' ? userConfig.output : '', + ), + }) as const satisfies UserConfig; const scenarios = [ { @@ -482,9 +488,7 @@ describe('my-client', () => { it.each(scenarios)('$description', async ({ config }) => { await createClient(config); - const outputPath = - typeof config.output === 'string' ? config.output : config.output.path; - const filePaths = getFilePaths(outputPath); + const filePaths = getFilePaths(config.output); await Promise.all( filePaths.map(async (filePath) => { diff --git a/packages/openapi-ts-tests/main/test/index.test.ts b/packages/openapi-ts-tests/main/test/index.test.ts index 0a787595ea..57a308ecab 100644 --- a/packages/openapi-ts-tests/main/test/index.test.ts +++ b/packages/openapi-ts-tests/main/test/index.test.ts @@ -518,6 +518,9 @@ describe('index', () => { createClient({ dryRun: true, input: '../specs/v2.json', + logs: { + level: 'silent', + }, output: './generated/v2/', plugins: ['@hey-api/client-fetch'], }), @@ -529,6 +532,9 @@ describe('index', () => { createClient({ dryRun: true, input: '../specs/v3.json', + logs: { + level: 'silent', + }, output: './generated/v3/', plugins: ['@hey-api/client-fetch'], }), @@ -540,6 +546,9 @@ describe('index', () => { createClient({ dryRun: true, input: '../specs/v3-transforms.json', + logs: { + level: 'silent', + }, output: './generated/v3/', plugins: [ '@hey-api/client-fetch', @@ -561,6 +570,9 @@ describe('index', () => { dryRun: true, input: 'https://raw.githubusercontent.com/hey-api/openapi-ts/main/packages/openapi-ts-tests/specs/v2.json', + logs: { + level: 'silent', + }, output: './generated/v2-downloaded/', plugins: ['@hey-api/client-fetch'], }), @@ -573,6 +585,9 @@ describe('index', () => { dryRun: true, input: 'https://raw.githubusercontent.com/hey-api/openapi-ts/main/packages/openapi-ts-tests/specs/v3.json', + logs: { + level: 'silent', + }, output: './generated/v3-downloaded/', plugins: ['@hey-api/client-fetch'], }), diff --git a/packages/openapi-ts-tests/main/test/openapi-ts.config.ts b/packages/openapi-ts-tests/main/test/openapi-ts.config.ts index 2bade13917..471360f8c1 100644 --- a/packages/openapi-ts-tests/main/test/openapi-ts.config.ts +++ b/packages/openapi-ts-tests/main/test/openapi-ts.config.ts @@ -14,393 +14,415 @@ import { myClientPlugin } from './custom/client/plugin'; // eslint-disable-next-line arrow-body-style export default defineConfig(() => { // ... - return { - // experimentalParser: false, - input: { - // branch: 'main', - // fetch: { - // headers: { - // 'x-foo': 'bar', - // }, - // }, - // organization: 'hey-api', - // path: { - // components: {}, - // info: { - // version: '1.0.0', - // }, - // openapi: '3.1.0', - // paths: {}, - // }, - path: path.resolve( - getSpecsPath(), - '3.0.x', - // 'circular.yaml', - 'dutchie.json', - // 'invalid', - // 'openai.yaml', - // 'full.yaml', - // 'opencode.yaml', - // 'sdk-instance.yaml', - // 'string-with-format.yaml', - // 'transformers.json', - // 'type-format.yaml', - // 'validators.yaml', - // 'validators-circular-ref-2.yaml', - // 'zoom-video-sdk.json', - ), - // path: 'scalar:@scalar/access-service', - // path: 'hey-api/backend', - // path: 'hey-api/backend?branch=main&version=1.0.0', - // path: 'https://get.heyapi.dev/hey-api/backend?branch=main&version=1.0.0', - // path: 'http://localhost:4000/', - // path: 'http://localhost:8000/openapi.json', - // path: 'https://mongodb-mms-prod-build-server.s3.amazonaws.com/openapi/2caffd88277a4e27c95dcefc7e3b6a63a3b03297-v2-2023-11-15.json', - // path: 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', - // project: 'backend', - // project: 'upload-openapi-spec', - // version: '1.0.0', - // watch: { - // enabled: true, - // interval: 500, - // timeout: 30_000, - // }, - }, - logs: { - // level: 'debug', - path: './logs', - }, - // name: 'foo', - output: { - // case: 'snake_case', - clean: true, - fileName: { - // case: 'snake_case', - // name: '{{name}}.renamed', - suffix: '.meh', - }, - // format: 'prettier', - importFileExtension: '.ts', - // indexFile: false, - // lint: 'eslint', - path: path.resolve(__dirname, 'generated', 'sample'), - tsConfigPath: path.resolve( - __dirname, - 'tsconfig', - 'tsconfig.nodenext.json', - ), - }, - parser: { - filters: { - // deprecated: false, - operations: { - include: [ - // 'GET /event', - // '/^[A-Z]+ /v1//', - ], - }, - // orphans: true, - // preserveOrder: true, - // schemas: { - // include: ['Foo'], - // }, - // tags: { - // exclude: ['bar'], - // }, - }, - hooks: { - operations: { - getKind() { - // noop - }, - isMutation() { - // noop - }, - isQuery: (op) => { - if (op.method === 'post' && op.path === '/search') { - return true; - } - return; - }, - }, - symbols: { - // getFilePath: (symbol) => { - // if (symbol.name) { - // return symbol.name[0]?.toLowerCase(); - // } - // return; + return [ + { + // experimentalParser: false, + input: [ + { + // fetch: { + // headers: { + // 'x-foo': 'bar', + // }, + // }, + // path: { + // components: {}, + // info: { + // version: '1.0.0', + // }, + // openapi: '3.1.0', + // paths: {}, + // }, + path: path.resolve( + getSpecsPath(), + '3.0.x', + // 'circular.yaml', + 'dutchie.json', + // 'invalid', + // 'openai.yaml', + // 'full.yaml', + // 'opencode.yaml', + // 'sdk-instance.yaml', + // 'string-with-format.yaml', + // 'transformers.json', + // 'type-format.yaml', + // 'validators.yaml', + // 'validators-circular-ref-2.yaml', + // 'zoom-video-sdk.json', + ), + // path: 'https://get.heyapi.dev/hey-api/backend?branch=main&version=1.0.0', + // path: 'http://localhost:4000/', + // path: 'http://localhost:8000/openapi.json', + // path: 'https://mongodb-mms-prod-build-server.s3.amazonaws.com/openapi/2caffd88277a4e27c95dcefc7e3b6a63a3b03297-v2-2023-11-15.json', + // path: 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', + // watch: { + // enabled: true, + // interval: 500, + // timeout: 30_000, // }, }, - }, - pagination: { - // keywords: ['aa'], - }, - patch: { - // operations: { - // 'GET /foo': (operation: any) => { - // operation.responses['200'].description = 'foo'; - // }, - // }, - // version: () => '3.1.1', - }, - transforms: { - // enums: { - // enabled: false, - // mode: 'root', - // // name: '{{name}}', - // }, - propertiesRequiredByDefault: true, - // readWrite: { - // // enabled: false, - // requests: '{{name}}Writable', - // responses: '{{name}}', + // path.resolve(getSpecsPath(), '3.1.x', 'full.yaml'), + // { + // branch: 'main', + // organization: 'hey-api', + // path: 'hey-api/backend', + // project: 'backend', + // project: 'upload-openapi-spec', + // version: '1.0.0', // }, + // 'hey-api/backend?branch=main&version=1.0.0', + // 'scalar:@scalar/access-service', + // 'readme:@developers/v2.0#nysezql0wwo236', + // 'readme:nysezql0wwo236', + // 'https://dash.readme.com/api/v1/api-registry/nysezql0wwo236', + // 'https://somefakedomain.com/openapi.yaml', + ], + logs: { + // level: 'debug', + path: './logs', }, - validate_EXPERIMENTAL: true, - }, - plugins: [ - // customClientPlugin({ - // baseUrl: false, - // }), - // myClientPlugin(), - { - // baseUrl: false, - exportFromIndex: true, - name: '@hey-api/client-fetch', - // name: 'legacy/angular', - // runtimeConfigPath: path.resolve(__dirname, 'hey-api.ts'), - runtimeConfigPath: './src/hey-api.ts', - // strictBaseUrl: true, - // throwOnError: true, - }, - { - // case: 'snake_case', - // definitions: '你_snake_{{name}}', - // enums: { - // // case: 'PascalCase', - // // constantsIgnoreNull: true, - // // enabled: false, - // mode: 'javascript', - // }, - // errors: { - // error: '他們_error_{{name}}', - // name: '你們_errors_{{name}}', - // }, - name: '@hey-api/typescript', - // requests: '我們_data_{{name}}', - // responses: { - // name: '我_responses_{{name}}', - // response: '他_response_{{name}}', - // }, - // topType: 'any', - // tree: true, - // webhooks: { - // name: 'Webby{{name}}Hook', - // payload: '{{name}}WebhookEvent', - // }, - }, - { - asClass: true, - // auth: false, - // classNameBuilder: '{{name}}', - // classNameBuilder: '{{name}}Service', - // classStructure: 'off', - // client: false, - // include... - // instance: true, - name: '@hey-api/sdk', - // operationId: false, - // params: 'experiment', - // responseStyle: 'data', - // transformer: '@hey-api/transformers', - // transformer: true, - // validator: { - // request: 'valibot', - // response: 'zod', + // name: 'foo', + output: [ + // { + // // case: 'snake_case', + // clean: true, + // fileName: { + // // case: 'snake_case', + // // name: '{{name}}.renamed', + // suffix: '.meh', + // }, + // // format: 'prettier', + // importFileExtension: '.ts', + // // indexFile: false, + // // lint: 'eslint', + // path: path.resolve(__dirname, 'generated', 'sample'), + // tsConfigPath: path.resolve( + // __dirname, + // 'tsconfig', + // 'tsconfig.nodenext.json', + // ), // }, - '~hooks': { - symbols: { - // getFilePath: (symbol) => { - // if (symbol.name) { - // return utils.stringCase({ - // case: 'camelCase', - // value: symbol.name, - // }); - // } - // return; - // }, + path.resolve(__dirname, 'generated', 'sample'), + ], + parser: { + filters: { + // deprecated: false, + operations: { + include: [ + // 'GET /event', + // '/^[A-Z]+ /v1//', + ], }, + // orphans: true, + // preserveOrder: true, + // schemas: { + // include: ['Foo'], + // }, + // tags: { + // exclude: ['bar'], + // }, }, - }, - { - // bigInt: true, - // dates: true, - // name: '@hey-api/transformers', - }, - { - // name: 'fastify', - }, - { - // case: 'SCREAMING_SNAKE_CASE', - // comments: false, - exportFromIndex: true, - // infiniteQueryKeys: { - // name: '{{name}}IQK', - // }, - // infiniteQueryOptions: { - // name: '{{name}}IQO', - // }, - // mutationOptions: { - // name: '{{name}}MO', - // }, - name: '@tanstack/react-query', - // queryKeys: { - // name: '{{name}}QK', - // }, - // queryOptions: false, - // queryOptions: { - // name: '{{name}}QO', - // }, - useQuery: true, - '~hooks': { + hooks: { operations: { - getKind: (op) => { - if (op.method === 'post' && op.path === '/search') { - return ['query']; - } - return; + getKind() { + // noop }, isMutation() { // noop }, - isQuery: () => { - // noop + isQuery: (op) => { + if (op.method === 'post' && op.path === '/search') { + return true; + } + return; }, }, - }, - }, - { - // case: 'SCREAMING_SNAKE_CASE', - // comments: false, - // definitions: 'z{{name}}Definition', - exportFromIndex: true, - // metadata: true, - name: 'valibot', - // requests: { - // case: 'PascalCase', - // name: '{{name}}Data', - // }, - // responses: { - // // case: 'snake_case', - // name: 'z{{name}}TestResponse', - // }, - // webhooks: { - // name: 'q{{name}}CoolWebhook', - // }, - '~hooks': { symbols: { // getFilePath: (symbol) => { // if (symbol.name) { - // return utils.stringCase({ - // case: 'camelCase', - // value: symbol.name, - // }); + // return symbol.name[0]?.toLowerCase(); // } // return; // }, }, }, - }, - { - // case: 'snake_case', - // comments: false, - compatibilityVersion: 'mini', - dates: { - local: true, - // offset: true, + pagination: { + // keywords: ['aa'], }, - definitions: { - // name: 'z{{name}}Definition', - // types: { - // infer: 'D{{name}}ZodType', + patch: { + // operations: { + // 'GET /foo': (operation: any) => { + // operation.responses['200'].description = 'foo'; // }, + // }, + // version: () => '3.1.1', }, - exportFromIndex: true, - metadata: true, - name: 'zod', - // requests: { - // // case: 'SCREAMING_SNAKE_CASE', - // // name: 'z{{name}}TestData', - // types: { - // infer: 'E{{name}}DataZodType', - // }, - // }, - responses: { + transforms: { + // enums: { + // enabled: false, + // mode: 'root', + // // name: '{{name}}', + // }, + // propertiesRequiredByDefault: true, + // readWrite: { + // // enabled: false, + // requests: '{{name}}Writable', + // responses: '{{name}}', + // }, + }, + // validate_EXPERIMENTAL: true, + }, + plugins: [ + // customClientPlugin({ + // baseUrl: false, + // }), + // myClientPlugin(), + { + // baseUrl: false, + exportFromIndex: true, + name: '@hey-api/client-fetch', + // name: 'legacy/angular', + // runtimeConfigPath: path.resolve(__dirname, 'hey-api.ts'), + runtimeConfigPath: './src/hey-api.ts', + // strictBaseUrl: true, + // throwOnError: true, + }, + { // case: 'snake_case', - // name: (name) => { - // if (name === 'complexTypes') { - // return 'z'; - // } - // return 'z{{name}}Response'; + // definitions: '你_snake_{{name}}', + // enums: { + // // case: 'PascalCase', + // // constantsIgnoreNull: true, + // // enabled: false, + // mode: 'javascript', + // }, + // errors: { + // error: '他們_error_{{name}}', + // name: '你們_errors_{{name}}', // }, - // types: { - // infer: 'F{{name}}ResponseZodType', + name: '@hey-api/typescript', + // requests: '我們_data_{{name}}', + // responses: { + // name: '我_responses_{{name}}', + // response: '他_response_{{name}}', + // }, + // topType: 'any', + // tree: true, + // webhooks: { + // name: 'Webby{{name}}Hook', + // payload: '{{name}}WebhookEvent', + // }, + }, + { + asClass: true, + // auth: false, + // classNameBuilder: '{{name}}', + // classNameBuilder: '{{name}}Service', + // classStructure: 'off', + // client: false, + // include... + // instance: true, + name: '@hey-api/sdk', + // operationId: false, + // params: 'experiment', + // responseStyle: 'data', + // transformer: '@hey-api/transformers', + // transformer: true, + // validator: { + // request: 'valibot', + // response: 'zod', // }, + '~hooks': { + symbols: { + // getFilePath: (symbol) => { + // if (symbol.name) { + // return utils.stringCase({ + // case: 'camelCase', + // value: symbol.name, + // }); + // } + // return; + // }, + }, + }, + }, + { + // bigInt: true, + // dates: true, + // name: '@hey-api/transformers', + }, + { + // name: 'fastify', }, - types: { - // infer: { - // case: 'snake_case', + { + // case: 'SCREAMING_SNAKE_CASE', + // comments: false, + exportFromIndex: true, + // infiniteQueryKeys: { + // name: '{{name}}IQK', + // }, + // infiniteQueryOptions: { + // name: '{{name}}IQO', + // }, + // mutationOptions: { + // name: '{{name}}MO', + // }, + name: '@tanstack/react-query', + // queryKeys: { + // name: '{{name}}QK', + // }, + // queryOptions: false, + // queryOptions: { + // name: '{{name}}QO', // }, + useQuery: true, + '~hooks': { + operations: { + getKind: (op) => { + if (op.method === 'post' && op.path === '/search') { + return ['query']; + } + return; + }, + isMutation() { + // noop + }, + isQuery: () => { + // noop + }, + }, + }, }, - '~hooks': { - symbols: { - // getFilePath: (symbol) => { - // if (symbol.name === 'z') { - // return 'complexService'; + { + // case: 'SCREAMING_SNAKE_CASE', + // comments: false, + // definitions: 'z{{name}}Definition', + exportFromIndex: true, + // metadata: true, + name: 'valibot', + // requests: { + // case: 'PascalCase', + // name: '{{name}}Data', + // }, + // responses: { + // // case: 'snake_case', + // name: 'z{{name}}TestResponse', + // }, + // webhooks: { + // name: 'q{{name}}CoolWebhook', + // }, + '~hooks': { + symbols: { + // getFilePath: (symbol) => { + // if (symbol.name) { + // return utils.stringCase({ + // case: 'camelCase', + // value: symbol.name, + // }); + // } + // return; + // }, + }, + }, + }, + { + // case: 'snake_case', + // comments: false, + compatibilityVersion: 'mini', + dates: { + local: true, + // offset: true, + }, + definitions: { + // name: 'z{{name}}Definition', + // types: { + // infer: 'D{{name}}ZodType', + // }, + }, + exportFromIndex: true, + metadata: true, + name: 'zod', + // requests: { + // // case: 'SCREAMING_SNAKE_CASE', + // // name: 'z{{name}}TestData', + // types: { + // infer: 'E{{name}}DataZodType', + // }, + // }, + responses: { + // case: 'snake_case', + // name: (name) => { + // if (name === 'complexTypes') { + // return 'z'; // } - // return; + // return 'z{{name}}Response'; + // }, + // types: { + // infer: 'F{{name}}ResponseZodType', // }, }, + types: { + // infer: { + // case: 'snake_case', + // }, + }, + '~hooks': { + symbols: { + // getFilePath: (symbol) => { + // if (symbol.name === 'z') { + // return 'complexService'; + // } + // return; + // }, + }, + }, }, - }, - { - exportFromIndex: true, - // name: '@hey-api/schemas', - // type: 'json', - }, - { - exportFromIndex: true, - httpRequests: { - // asClass: true, - }, - httpResources: { - // asClass: true, + { + exportFromIndex: true, + // name: '@hey-api/schemas', + // type: 'json', }, - // name: '@angular/common', - }, - { - exportFromIndex: true, - // mutationOptions: '{{name}}Mutationssss', - // name: '@pinia/colada', - // queryOptions: { - // name: '{{name}}Queryyyyy', - // }, - queryKeys: { - tags: true, + { + exportFromIndex: true, + httpRequests: { + // asClass: true, + }, + httpResources: { + // asClass: true, + }, + // name: '@angular/common', }, - '~hooks': { - operations: { - getKind: (op) => { - if (op.method === 'post' && op.path === '/search') { - return ['query']; - } - return; + { + exportFromIndex: true, + // mutationOptions: '{{name}}Mutationssss', + // name: '@pinia/colada', + // queryOptions: { + // name: '{{name}}Queryyyyy', + // }, + queryKeys: { + tags: true, + }, + '~hooks': { + operations: { + getKind: (op) => { + if (op.method === 'post' && op.path === '/search') { + return ['query']; + } + return; + }, }, }, }, - }, - ], - // useOptions: false, - // watch: 3_000, - }; + ], + // useOptions: false, + // watch: 3_000, + }, + // { + // input: 'scalar:@scalar/access-service', + // logs: { + // // level: 'debug', + // path: './logs', + // }, + // output: path.resolve(__dirname, 'generated', 'sample'), + // }, + ]; }); diff --git a/packages/openapi-ts-tests/main/test/plugins.test.ts b/packages/openapi-ts-tests/main/test/plugins.test.ts index e3b47cda33..b80e6e1668 100644 --- a/packages/openapi-ts-tests/main/test/plugins.test.ts +++ b/packages/openapi-ts-tests/main/test/plugins.test.ts @@ -26,25 +26,26 @@ for (const version of versions) { userConfig: Omit & Pick, 'plugins'> & Pick, 'input'>, - ): UserConfig => ({ - ...userConfig, - input: path.join( - getSpecsPath(), - version, - typeof userConfig.input === 'string' ? userConfig.input : 'full.yaml', - ), - logs: { - level: 'silent', - }, - output: path.join( - outputDir, - typeof userConfig.plugins[0] === 'string' - ? userConfig.plugins[0] - : userConfig.plugins[0]!.name, - typeof userConfig.output === 'string' ? userConfig.output : '', - ), - plugins: userConfig.plugins ?? ['@hey-api/client-fetch'], - }); + ) => + ({ + ...userConfig, + input: path.join( + getSpecsPath(), + version, + typeof userConfig.input === 'string' ? userConfig.input : 'full.yaml', + ), + logs: { + level: 'silent', + }, + output: path.join( + outputDir, + typeof userConfig.plugins[0] === 'string' + ? userConfig.plugins[0] + : userConfig.plugins[0]!.name, + typeof userConfig.output === 'string' ? userConfig.output : '', + ), + plugins: userConfig.plugins ?? ['@hey-api/client-fetch'], + }) as const satisfies UserConfig; const scenarios = [ { @@ -566,9 +567,7 @@ for (const version of versions) { it.each(scenarios)('$description', async ({ config }) => { await createClient(config); - const outputPath = - typeof config.output === 'string' ? config.output : config.output.path; - const filePaths = getFilePaths(outputPath); + const filePaths = getFilePaths(config.output); await Promise.all( filePaths.map(async (filePath) => { @@ -641,7 +640,7 @@ for (const version of versions) { output: path.join(outputDir, myPlugin.name, 'default'), plugins: [myPlugin, '@hey-api/client-fetch'], }), - ).rejects.toThrowError(/unknown plugin/g); + ).rejects.toThrowError(/Found 1 configuration error./g); expect(myPlugin.handler).not.toHaveBeenCalled(); expect(myPlugin.handlerLegacy).not.toHaveBeenCalled(); diff --git a/packages/openapi-ts-tests/zod/v3/test/3.0.x.test.ts b/packages/openapi-ts-tests/zod/v3/test/3.0.x.test.ts index d1cbd1460b..3e255b3bd4 100644 --- a/packages/openapi-ts-tests/zod/v3/test/3.0.x.test.ts +++ b/packages/openapi-ts-tests/zod/v3/test/3.0.x.test.ts @@ -68,9 +68,7 @@ for (const zodVersion of zodVersions) { it.each(scenarios)('$description', async ({ config }) => { await createClient(config); - const outputPath = - typeof config.output === 'string' ? config.output : config.output.path; - const filePaths = getFilePaths(outputPath); + const filePaths = getFilePaths(config.output); await Promise.all( filePaths.map(async (filePath) => { diff --git a/packages/openapi-ts-tests/zod/v3/test/3.1.x.test.ts b/packages/openapi-ts-tests/zod/v3/test/3.1.x.test.ts index 4fedd7c557..c8d0d0b1c3 100644 --- a/packages/openapi-ts-tests/zod/v3/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/zod/v3/test/3.1.x.test.ts @@ -150,9 +150,7 @@ for (const zodVersion of zodVersions) { it.each(scenarios)('$description', async ({ config }) => { await createClient(config); - const outputPath = - typeof config.output === 'string' ? config.output : config.output.path; - const filePaths = getFilePaths(outputPath); + const filePaths = getFilePaths(config.output); await Promise.all( filePaths.map(async (filePath) => { diff --git a/packages/openapi-ts-tests/zod/v3/test/openapi.test.ts b/packages/openapi-ts-tests/zod/v3/test/openapi.test.ts index 47fa7470db..5605ecc637 100644 --- a/packages/openapi-ts-tests/zod/v3/test/openapi.test.ts +++ b/packages/openapi-ts-tests/zod/v3/test/openapi.test.ts @@ -54,11 +54,7 @@ for (const version of versions) { it.each(scenarios)('$description', async ({ config }) => { await createClient(config); - const outputPath = - typeof config.output === 'string' - ? config.output - : config.output.path; - const filePaths = getFilePaths(outputPath); + const filePaths = getFilePaths(config.output); await Promise.all( filePaths.map(async (filePath) => { diff --git a/packages/openapi-ts-tests/zod/v3/test/utils.ts b/packages/openapi-ts-tests/zod/v3/test/utils.ts index d18918019c..74307a9085 100644 --- a/packages/openapi-ts-tests/zod/v3/test/utils.ts +++ b/packages/openapi-ts-tests/zod/v3/test/utils.ts @@ -18,13 +18,15 @@ export const createZodConfig = outputDir: string; zodVersion: (typeof zodVersions)[number]; }) => - (userConfig: UserConfig): UserConfig => { + (userConfig: UserConfig) => { + const input = + userConfig.input instanceof Array + ? userConfig.input[0]! + : userConfig.input; const inputPath = path.join( getSpecsPath(), openApiVersion, - typeof userConfig.input === 'string' - ? userConfig.input - : (userConfig.input.path as string), + typeof input === 'string' ? input : (input.path as string), ); return { plugins: [ @@ -48,7 +50,7 @@ export const createZodConfig = outputDir, typeof userConfig.output === 'string' ? userConfig.output : '', ), - }; + } as const satisfies UserConfig; }; export const getSnapshotsPath = (): string => diff --git a/packages/openapi-ts-tests/zod/v4/test/3.0.x.test.ts b/packages/openapi-ts-tests/zod/v4/test/3.0.x.test.ts index d1cbd1460b..3e255b3bd4 100644 --- a/packages/openapi-ts-tests/zod/v4/test/3.0.x.test.ts +++ b/packages/openapi-ts-tests/zod/v4/test/3.0.x.test.ts @@ -68,9 +68,7 @@ for (const zodVersion of zodVersions) { it.each(scenarios)('$description', async ({ config }) => { await createClient(config); - const outputPath = - typeof config.output === 'string' ? config.output : config.output.path; - const filePaths = getFilePaths(outputPath); + const filePaths = getFilePaths(config.output); await Promise.all( filePaths.map(async (filePath) => { diff --git a/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts b/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts index 4fedd7c557..c8d0d0b1c3 100644 --- a/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts @@ -150,9 +150,7 @@ for (const zodVersion of zodVersions) { it.each(scenarios)('$description', async ({ config }) => { await createClient(config); - const outputPath = - typeof config.output === 'string' ? config.output : config.output.path; - const filePaths = getFilePaths(outputPath); + const filePaths = getFilePaths(config.output); await Promise.all( filePaths.map(async (filePath) => { diff --git a/packages/openapi-ts-tests/zod/v4/test/openapi.test.ts b/packages/openapi-ts-tests/zod/v4/test/openapi.test.ts index 47fa7470db..5605ecc637 100644 --- a/packages/openapi-ts-tests/zod/v4/test/openapi.test.ts +++ b/packages/openapi-ts-tests/zod/v4/test/openapi.test.ts @@ -54,11 +54,7 @@ for (const version of versions) { it.each(scenarios)('$description', async ({ config }) => { await createClient(config); - const outputPath = - typeof config.output === 'string' - ? config.output - : config.output.path; - const filePaths = getFilePaths(outputPath); + const filePaths = getFilePaths(config.output); await Promise.all( filePaths.map(async (filePath) => { diff --git a/packages/openapi-ts-tests/zod/v4/test/utils.ts b/packages/openapi-ts-tests/zod/v4/test/utils.ts index d18918019c..74307a9085 100644 --- a/packages/openapi-ts-tests/zod/v4/test/utils.ts +++ b/packages/openapi-ts-tests/zod/v4/test/utils.ts @@ -18,13 +18,15 @@ export const createZodConfig = outputDir: string; zodVersion: (typeof zodVersions)[number]; }) => - (userConfig: UserConfig): UserConfig => { + (userConfig: UserConfig) => { + const input = + userConfig.input instanceof Array + ? userConfig.input[0]! + : userConfig.input; const inputPath = path.join( getSpecsPath(), openApiVersion, - typeof userConfig.input === 'string' - ? userConfig.input - : (userConfig.input.path as string), + typeof input === 'string' ? input : (input.path as string), ); return { plugins: [ @@ -48,7 +50,7 @@ export const createZodConfig = outputDir, typeof userConfig.output === 'string' ? userConfig.output : '', ), - }; + } as const satisfies UserConfig; }; export const getSnapshotsPath = (): string => diff --git a/packages/openapi-ts/bin/index.cjs b/packages/openapi-ts/bin/index.cjs index 0621b156e7..79e93dd59e 100755 --- a/packages/openapi-ts/bin/index.cjs +++ b/packages/openapi-ts/bin/index.cjs @@ -36,7 +36,7 @@ const params = program .option('-s, --silent', 'Set log level to silent') .option( '--no-log-file', - 'Disable writing a log file. Works like --silent but without supressing console output', + 'Disable writing a log file. Works like --silent but without suppressing console output', ) .option( '-w, --watch [value]', @@ -119,6 +119,7 @@ async function start() { } userConfig.logs.file = userConfig.logFile; + delete userConfig.logFile; if (typeof params.watch === 'string') { userConfig.watch = Number.parseInt(params.watch, 10); diff --git a/packages/openapi-ts/package.json b/packages/openapi-ts/package.json index e802b2141d..bb2b887804 100644 --- a/packages/openapi-ts/package.json +++ b/packages/openapi-ts/package.json @@ -91,9 +91,9 @@ }, "dependencies": { "@hey-api/codegen-core": "workspace:^0.2.0", - "@hey-api/json-schema-ref-parser": "1.1.0", + "@hey-api/json-schema-ref-parser": "1.2.0", "ansi-colors": "4.1.3", - "c12": "2.0.1", + "c12": "3.3.0", "color-support": "1.1.3", "commander": "13.0.0", "handlebars": "4.7.8", diff --git a/packages/openapi-ts/src/__tests__/createClient.test.ts b/packages/openapi-ts/src/__tests__/createClient.test.ts index ecbffcb882..6c4acec075 100644 --- a/packages/openapi-ts/src/__tests__/createClient.test.ts +++ b/packages/openapi-ts/src/__tests__/createClient.test.ts @@ -34,6 +34,7 @@ describe('compileInputPath', () => { it('with platform string', () => { const path = compileInputPath({ path: 'https://get.heyapi.dev/foo/bar?branch=main&commit_sha=sha&tags=a,b,c&version=1.0.0', + registry: 'hey-api', }); expect(path).toEqual({ branch: 'main', @@ -41,6 +42,7 @@ describe('compileInputPath', () => { organization: 'foo', path: 'https://get.heyapi.dev/foo/bar?branch=main&commit_sha=sha&tags=a,b,c&version=1.0.0', project: 'bar', + registry: 'hey-api', tags: ['a', 'b', 'c'], version: '1.0.0', }); @@ -71,6 +73,7 @@ describe('compileInputPath', () => { process.env.HEY_API_TOKEN = 'foo'; const path = compileInputPath({ path: 'https://get.heyapi.dev/foo/bar', + registry: 'hey-api', }); delete process.env.HEY_API_TOKEN; expect(path).toEqual({ @@ -78,6 +81,7 @@ describe('compileInputPath', () => { organization: 'foo', path: 'https://get.heyapi.dev/foo/bar?api_key=foo', project: 'bar', + registry: 'hey-api', }); }); }); diff --git a/packages/openapi-ts/src/__tests__/error.test.ts b/packages/openapi-ts/src/__tests__/error.test.ts index 91f4c86fa2..57cff1d4fd 100644 --- a/packages/openapi-ts/src/__tests__/error.test.ts +++ b/packages/openapi-ts/src/__tests__/error.test.ts @@ -20,10 +20,8 @@ describe('shouldReportCrash', () => { }); it('should not prompt when isInteractive is explicitly false', async () => { - // Mock stdin/stdout to ensure we don't wait for user input - const writeSpy = vi - .spyOn(process.stdout, 'write') - .mockImplementation(() => true); + // Mock stdin and console.log to ensure we don't wait for user input + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined); const setEncodingSpy = vi .spyOn(process.stdin, 'setEncoding') .mockImplementation(() => process.stdin as any); @@ -37,20 +35,18 @@ describe('shouldReportCrash', () => { }); expect(result).toBe(false); - expect(writeSpy).not.toHaveBeenCalled(); + expect(logSpy).not.toHaveBeenCalled(); expect(setEncodingSpy).not.toHaveBeenCalled(); expect(onceSpy).not.toHaveBeenCalled(); - writeSpy.mockRestore(); + logSpy.mockRestore(); setEncodingSpy.mockRestore(); onceSpy.mockRestore(); }); it('should prompt when isInteractive is true', async () => { - // Mock stdin/stdout for interactive session - const writeSpy = vi - .spyOn(process.stdout, 'write') - .mockImplementation(() => true); + // Mock stdin and console.log for interactive session + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined); const setEncodingSpy = vi .spyOn(process.stdin, 'setEncoding') .mockImplementation(() => process.stdin as any); @@ -70,20 +66,18 @@ describe('shouldReportCrash', () => { }); expect(result).toBe(false); // User said 'n' - expect(writeSpy).toHaveBeenCalledWith( + expect(logSpy).toHaveBeenCalledWith( expect.stringContaining('📢 Open a GitHub issue with crash details?'), ); - writeSpy.mockRestore(); + logSpy.mockRestore(); setEncodingSpy.mockRestore(); onceSpy.mockRestore(); }); it('should handle user saying yes to crash report', async () => { - // Mock stdin/stdout for interactive session - const writeSpy = vi - .spyOn(process.stdout, 'write') - .mockImplementation(() => true); + // Mock stdin and console.log for interactive session + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined); const setEncodingSpy = vi .spyOn(process.stdin, 'setEncoding') .mockImplementation(() => process.stdin as any); @@ -104,7 +98,7 @@ describe('shouldReportCrash', () => { expect(result).toBe(true); // User said 'y' - writeSpy.mockRestore(); + logSpy.mockRestore(); setEncodingSpy.mockRestore(); onceSpy.mockRestore(); }); diff --git a/packages/openapi-ts/src/__tests__/index.test.ts b/packages/openapi-ts/src/__tests__/index.test.ts index 66ef27b778..d2c7b631a3 100644 --- a/packages/openapi-ts/src/__tests__/index.test.ts +++ b/packages/openapi-ts/src/__tests__/index.test.ts @@ -1,10 +1,153 @@ import { describe, expect, it } from 'vitest'; -describe('main entry index', () => { - describe('createClient', () => { - it('should be exported', async () => { - const { createClient } = await import('../index'); - expect(createClient).toBeDefined(); - }); +import { createClient } from '../index'; + +type Config = Parameters[0]; + +describe('createClient', () => { + it('1 config, 1 input, 1 output', async () => { + const config: Config = { + dryRun: true, + input: { + info: { title: 'foo', version: '1.0.0' }, + openapi: '3.0.0', + }, + logs: { + level: 'silent', + }, + output: 'output', + plugins: ['@hey-api/typescript'], + }; + + const results = await createClient(config); + expect(results).toHaveLength(1); + }); + + it('1 config, 2 inputs, 1 output', async () => { + const config: Config = { + dryRun: true, + input: [ + { + info: { title: 'foo', version: '1.0.0' }, + openapi: '3.0.0', + }, + { + info: { title: 'bar', version: '1.0.0' }, + openapi: '3.0.0', + paths: {}, + }, + ], + logs: { + level: 'silent', + }, + output: 'output', + plugins: ['@hey-api/typescript'], + }; + + const results = await createClient(config); + expect(results).toHaveLength(1); + }); + + it('1 config, 2 inputs, 2 outputs', async () => { + const config: Config = { + dryRun: true, + input: [ + { + info: { title: 'foo', version: '1.0.0' }, + openapi: '3.0.0', + }, + { + info: { title: 'bar', version: '1.0.0' }, + openapi: '3.0.0', + paths: {}, + }, + ], + logs: { + level: 'silent', + }, + output: ['output', 'output2'], + plugins: ['@hey-api/typescript'], + }; + + const results = await createClient(config); + expect(results).toHaveLength(2); + }); + + it('2 configs, 1 input, 1 output', async () => { + const config: Config = [ + { + dryRun: true, + input: { + info: { title: 'foo', version: '1.0.0' }, + openapi: '3.0.0', + }, + logs: { + level: 'silent', + }, + output: 'output', + plugins: ['@hey-api/typescript'], + }, + { + dryRun: true, + input: { + info: { title: 'bar', version: '1.0.0' }, + openapi: '3.0.0', + }, + logs: { + level: 'silent', + }, + output: 'output2', + plugins: ['@hey-api/typescript'], + }, + ]; + + const results = await createClient(config); + expect(results).toHaveLength(2); + }); + + it('2 configs, 2 inputs, 2 outputs', async () => { + const config: Config = [ + { + dryRun: true, + input: [ + { + info: { title: 'foo', version: '1.0.0' }, + openapi: '3.0.0', + }, + { + info: { title: 'bar', version: '1.0.0' }, + openapi: '3.0.0', + paths: {}, + }, + ], + logs: { + level: 'silent', + }, + output: ['output', 'output2'], + plugins: ['@hey-api/typescript'], + }, + { + dryRun: true, + input: [ + { + info: { title: 'baz', version: '1.0.0' }, + openapi: '3.0.0', + }, + { + info: { title: 'qux', version: '1.0.0' }, + openapi: '3.0.0', + paths: {}, + }, + ], + logs: { + level: 'silent', + }, + output: ['output3', 'output4'], + plugins: ['@hey-api/typescript'], + }, + ]; + + const results = await createClient(config); + expect(results).toHaveLength(4); }); }); diff --git a/packages/openapi-ts/src/__tests__/interactive.test.ts b/packages/openapi-ts/src/__tests__/interactive.test.ts index e5b2efc60c..626e357b42 100644 --- a/packages/openapi-ts/src/__tests__/interactive.test.ts +++ b/packages/openapi-ts/src/__tests__/interactive.test.ts @@ -2,12 +2,18 @@ import { afterEach, describe, expect, it } from 'vitest'; import { detectInteractiveSession, initConfigs } from '../config/init'; import { mergeConfigs } from '../config/merge'; +import { Logger } from '../utils/logger'; describe('interactive config', () => { it('should use detectInteractiveSession when not provided', async () => { const result = await initConfigs({ - input: 'test.json', - output: './test', + logger: new Logger(), + userConfigs: [ + { + input: 'test.json', + output: './test', + }, + ], }); // In test environment, TTY is typically not available, so it should be false @@ -16,9 +22,14 @@ describe('interactive config', () => { it('should respect user config when set to true', async () => { const result = await initConfigs({ - input: 'test.json', - interactive: true, - output: './test', + logger: new Logger(), + userConfigs: [ + { + input: 'test.json', + interactive: true, + output: './test', + }, + ], }); expect(result.results[0]?.config.interactive).toBe(true); @@ -26,9 +37,14 @@ describe('interactive config', () => { it('should respect user config when set to false', async () => { const result = await initConfigs({ - input: 'test.json', - interactive: false, - output: './test', + logger: new Logger(), + userConfigs: [ + { + input: 'test.json', + interactive: false, + output: './test', + }, + ], }); expect(result.results[0]?.config.interactive).toBe(false); diff --git a/packages/openapi-ts/src/config/__tests__/input.test.ts b/packages/openapi-ts/src/config/__tests__/input.test.ts index 1276be0bbe..b55b3246f1 100644 --- a/packages/openapi-ts/src/config/__tests__/input.test.ts +++ b/packages/openapi-ts/src/config/__tests__/input.test.ts @@ -11,7 +11,7 @@ describe('input config', () => { output: 'src/client', }; - const result = getInput(userConfig); + const result = getInput(userConfig)[0]!; expect(result.path).toBe('https://example.com/openapi.yaml'); }); @@ -21,7 +21,7 @@ describe('input config', () => { output: 'src/client', }; - const result = getInput(userConfig); + const result = getInput(userConfig)[0]!; expect(result.path).toBe( 'https://dash.readme.com/api/v1/api-registry/abc123', ); @@ -33,7 +33,7 @@ describe('input config', () => { output: 'src/client', }; - const result = getInput(userConfig); + const result = getInput(userConfig)[0]!; expect(result.path).toBe( 'https://dash.readme.com/api/v1/api-registry/uuid123', ); @@ -45,7 +45,7 @@ describe('input config', () => { output: 'src/client', }; - const result = getInput(userConfig); + const result = getInput(userConfig)[0]!; expect(result.path).toBe( 'https://dash.readme.com/api/v1/api-registry/test-uuid-123', ); @@ -64,7 +64,7 @@ describe('input config', () => { output: 'src/client', }; - const result = getInput(userConfig); + const result = getInput(userConfig)[0]!; expect(result.path).toBe( 'https://dash.readme.com/api/v1/api-registry/abc123', ); @@ -79,7 +79,7 @@ describe('input config', () => { output: 'src/client', }; - const result = getInput(userConfig); + const result = getInput(userConfig)[0]!; expect(result.path).toBe( 'https://dash.readme.com/api/v1/api-registry/uuid', ); @@ -95,7 +95,7 @@ describe('input config', () => { output: 'src/client', }; - const result = getInput(userConfig); + const result = getInput(userConfig)[0]!; expect(result.path).toBe('https://get.heyapi.dev/myorg/myproject'); }); @@ -108,7 +108,7 @@ describe('input config', () => { output: 'src/client', }; - const result = getInput(userConfig); + const result = getInput(userConfig)[0]!; expect(result.path).toEqual({ info: { title: 'Test API', version: '1.0.0' }, openapi: '3.0.0', @@ -125,7 +125,7 @@ describe('input config', () => { inputs.forEach((input) => { const userConfig: UserConfig = { input, output: 'src/client' }; - const result = getInput(userConfig); + const result = getInput(userConfig)[0]!; expect(result.path).toBe(input); }); }); @@ -140,7 +140,7 @@ describe('input config', () => { }, }; - const result = getInput(userConfig); + const result = getInput(userConfig)[0]!; expect(result.path).toBe( 'https://dash.readme.com/api/v1/api-registry/abc123', ); @@ -160,7 +160,7 @@ describe('input config', () => { output: 'src/client', }; - const result = getInput(userConfig); + const result = getInput(userConfig)[0]!; expect(result.path).toBe( 'https://dash.readme.com/api/v1/api-registry/test123', ); diff --git a/packages/openapi-ts/src/config/init.ts b/packages/openapi-ts/src/config/init.ts index 0a7b8c5353..adc457d5af 100644 --- a/packages/openapi-ts/src/config/init.ts +++ b/packages/openapi-ts/src/config/init.ts @@ -1,10 +1,13 @@ import path from 'node:path'; +import colors from 'ansi-colors'; import { loadConfig } from 'c12'; import { ConfigError } from '../error'; import type { Config, UserConfig } from '../types/config'; +import type { ArrayOnly } from '../types/utils'; import { isLegacyClient, setConfig } from '../utils/config'; +import type { Logger } from '../utils/logger'; import { getInput } from './input'; import { getLogs } from './logs'; import { mergeConfigs } from './merge'; @@ -13,6 +16,17 @@ import { getProjectDependencies } from './packages'; import { getParser } from './parser'; import { getPlugins } from './plugins'; +type ConfigResult = { + config: Config; + errors: ReadonlyArray; + jobIndex: number; +}; + +export type Configs = { + dependencies: Record; + results: ReadonlyArray; +}; + /** * Detect if the current session is interactive based on TTY status and environment variables. * This is used as a fallback when the user doesn't explicitly set the interactive option. @@ -30,43 +44,81 @@ export const detectInteractiveSession = (): boolean => /** * @internal */ -export const initConfigs = async ( - userConfig: UserConfig | undefined, -): Promise<{ - dependencies: Record; - results: ReadonlyArray<{ - config: Config; - errors: ReadonlyArray; - }>; -}> => { - let configurationFile: string | undefined = undefined; - if (userConfig?.configFile) { - const parts = userConfig.configFile.split('.'); - configurationFile = parts.slice(0, parts.length - 1).join('.'); - } - - const { config: configFromFile, configFile: loadedConfigFile } = - await loadConfig({ - configFile: configurationFile, - name: 'openapi-ts', - }); +export const initConfigs = async ({ + logger, + userConfigs, +}: { + logger: Logger; + userConfigs: ReadonlyArray; +}): Promise => { + const configs: Array = []; + let dependencies: Record = {}; + + const eventLoad = logger.timeEvent('load'); + for (const userConfig of userConfigs) { + let configurationFile: string | undefined = undefined; + if (userConfig?.configFile) { + const parts = userConfig.configFile.split('.'); + configurationFile = parts.slice(0, parts.length - 1).join('.'); + } - const dependencies = getProjectDependencies( - Object.keys(configFromFile).length ? loadedConfigFile : undefined, - ); + const eventC12 = logger.timeEvent('c12'); + const { config: configFromFile, configFile: loadedConfigFile } = + await loadConfig({ + configFile: configurationFile, + name: 'openapi-ts', + }); + eventC12.timeEnd(); + + if (!Object.keys(dependencies).length) { + // TODO: handle dependencies for multiple configs properly? + dependencies = getProjectDependencies( + Object.keys(configFromFile).length ? loadedConfigFile : undefined, + ); + } - const userConfigs: ReadonlyArray = Array.isArray(userConfig) - ? userConfig - : Array.isArray(configFromFile) - ? configFromFile.map((config) => mergeConfigs(config, userConfig)) - : [mergeConfigs(configFromFile, userConfig)]; + const mergedConfigs = + configFromFile instanceof Array + ? configFromFile.map((config) => mergeConfigs(config, userConfig)) + : [mergeConfigs(configFromFile, userConfig)]; + + for (const mergedConfig of mergedConfigs) { + const input = getInput(mergedConfig); + + if (mergedConfig.output instanceof Array) { + const countInputs = input.length; + const countOutputs = mergedConfig.output.length; + if (countOutputs > 1) { + if (countInputs !== countOutputs) { + console.warn( + `⚙️ ${colors.yellow('Warning:')} You provided ${colors.cyan(String(countInputs))} ${colors.cyan(countInputs === 1 ? 'input' : 'inputs')} and ${colors.yellow(String(countOutputs))} ${colors.yellow('outputs')}. This is probably not what you want as it will produce identical output in multiple locations. You most likely want to provide a single output or the same number of outputs as inputs.`, + ); + for (const output of mergedConfig.output) { + configs.push({ ...mergedConfig, input, output }); + } + } else { + mergedConfig.output.forEach((output, index) => { + configs.push({ ...mergedConfig, input: input[index]!, output }); + }); + } + } else { + configs.push({ + ...mergedConfig, + input, + output: mergedConfig.output[0] ?? '', + }); + } + } else { + configs.push({ ...mergedConfig, input }); + } + } + } + eventLoad.timeEnd(); - const results: Array<{ - config: Config; - errors: Array; - }> = []; + const results: Array> = []; - for (const userConfig of userConfigs) { + const eventBuild = logger.timeEvent('build'); + for (const userConfig of configs) { const { base, configFile = '', @@ -83,19 +135,15 @@ export const initConfigs = async ( ? userConfig.interactive : detectInteractiveSession(); - const errors: Array = []; - const logs = getLogs(userConfig); - if (logs.level === 'debug') { - console.warn('userConfig:', userConfig); - } - const input = getInput(userConfig); const output = getOutput(userConfig); const parser = getParser(userConfig); - if (!input.path) { + const errors: Array = []; + + if (!input.length) { errors.push( new ConfigError( 'missing input - which OpenAPI specification should we use to generate your output?', @@ -149,15 +197,16 @@ export const initConfigs = async ( }); config.exportCore = isLegacyClient(config) ? exportCore : false; + const jobIndex = results.length; + if (logs.level === 'debug') { - console.warn('config:', config); + const jobPrefix = colors.gray(`[Job ${jobIndex + 1}] `); + console.warn(`${jobPrefix}${colors.cyan('config:')}`, config); } - results.push({ - config, - errors, - }); + results.push({ config, errors, jobIndex }); } + eventBuild.timeEnd(); return { dependencies, results }; }; diff --git a/packages/openapi-ts/src/config/input.ts b/packages/openapi-ts/src/config/input.ts index b1d18adb87..39629a80bb 100644 --- a/packages/openapi-ts/src/config/input.ts +++ b/packages/openapi-ts/src/config/input.ts @@ -1,17 +1,16 @@ import type { Config, UserConfig } from '../types/config'; -import type { Input } from '../types/input'; +import type { Input, Watch } from '../types/input'; import { inputToApiRegistry } from '../utils/input'; import { heyApiRegistryBaseUrl } from '../utils/input/heyApi'; -const defaultWatch: Config['input']['watch'] = { +const defaultWatch: Watch = { enabled: false, interval: 1_000, timeout: 60_000, }; -const getWatch = ( - input: Pick, -): Config['input']['watch'] => { +// watch only remote files +const getWatch = (input: Pick): Watch => { let watch = { ...defaultWatch }; // we cannot watch spec passed as an object @@ -35,52 +34,61 @@ const getWatch = ( }; export const getInput = (userConfig: UserConfig): Config['input'] => { - let input: Config['input'] = { - path: '', - watch: defaultWatch, - }; + const userInputs = + userConfig.input instanceof Array ? userConfig.input : [userConfig.input]; - if (typeof userConfig.input === 'string') { - input.path = userConfig.input; - } else if ( - userConfig.input && - (userConfig.input.path !== undefined || - userConfig.input.organization !== undefined) - ) { - // @ts-expect-error - input = { - ...input, - path: heyApiRegistryBaseUrl, - ...userConfig.input, + const inputs: Array = []; + + for (const userInput of userInputs) { + let input: Input = { + path: '', + watch: defaultWatch, }; - // watch only remote files - if (input.watch !== undefined) { - input.watch = getWatch(input); + if (typeof userInput === 'string') { + input.path = userInput; + } else if ( + userInput && + (userInput.path !== undefined || userInput.organization !== undefined) + ) { + // @ts-expect-error + input = { + ...input, + path: heyApiRegistryBaseUrl, + ...userInput, + }; + + if (input.watch !== undefined) { + input.watch = getWatch(input); + } + } else { + input = { + ...input, + path: userInput, + }; } - } else { - input = { - ...input, - path: userConfig.input as Record, - }; - } - if (typeof input.path === 'string') { - inputToApiRegistry(input as Input & { path: string }); - } + if (typeof input.path === 'string') { + inputToApiRegistry(input as Input & { path: string }); + } - if ( - userConfig.watch !== undefined && - input.watch.enabled === defaultWatch.enabled && - input.watch.interval === defaultWatch.interval && - input.watch.timeout === defaultWatch.timeout - ) { - input.watch = getWatch({ - path: input.path, - // @ts-expect-error - watch: userConfig.watch, - }); + if ( + userConfig.watch !== undefined && + input.watch.enabled === defaultWatch.enabled && + input.watch.interval === defaultWatch.interval && + input.watch.timeout === defaultWatch.timeout + ) { + input.watch = getWatch({ + path: input.path, + // @ts-expect-error + watch: userConfig.watch, + }); + } + + if (input.path) { + inputs.push(input); + } } - return input; + return inputs; }; diff --git a/packages/openapi-ts/src/config/logs.ts b/packages/openapi-ts/src/config/logs.ts index 1fef415f7c..ada1300ef7 100644 --- a/packages/openapi-ts/src/config/logs.ts +++ b/packages/openapi-ts/src/config/logs.ts @@ -1,6 +1,8 @@ import type { Config, UserConfig } from '../types/config'; -export const getLogs = (userConfig: UserConfig | undefined): Config['logs'] => { +export const getLogs = ( + userConfig: Pick | undefined, +): Config['logs'] => { let logs: Config['logs'] = { file: true, level: 'info', diff --git a/packages/openapi-ts/src/config/output.ts b/packages/openapi-ts/src/config/output.ts index 06e51ad70e..e03fff8391 100644 --- a/packages/openapi-ts/src/config/output.ts +++ b/packages/openapi-ts/src/config/output.ts @@ -5,6 +5,12 @@ import type { Config, UserConfig } from '../types/config'; import { valueToObject } from './utils/config'; export const getOutput = (userConfig: UserConfig): Config['output'] => { + if (userConfig.output instanceof Array) { + throw new Error( + 'Unexpected array of outputs in user configuration. This should have been expanded already.', + ); + } + const output = valueToObject({ defaultValue: { clean: true, diff --git a/packages/openapi-ts/src/createClient.ts b/packages/openapi-ts/src/createClient.ts index fc28ce4bb3..ecdf586d23 100644 --- a/packages/openapi-ts/src/createClient.ts +++ b/packages/openapi-ts/src/createClient.ts @@ -1,5 +1,6 @@ import path from 'node:path'; +import { $RefParser } from '@hey-api/json-schema-ref-parser'; import colors from 'ansi-colors'; import { generateLegacyOutput } from './generate/legacy/output'; @@ -11,35 +12,33 @@ import { patchOpenApiSpec } from './openApi/shared/utils/patch'; import { processOutput } from './processOutput'; import type { Client } from './types/client'; import type { Config } from './types/config'; +import type { Input } from './types/input'; import type { WatchValues } from './types/types'; import { isLegacyClient, legacyNameFromConfig } from './utils/config'; import type { Templates } from './utils/handlebars'; -import { heyApiRegistryBaseUrl } from './utils/input/heyApi'; import type { Logger } from './utils/logger'; import { postProcessClient } from './utils/postprocess'; -const isHeyApiRegistryPath = (path: string) => - path.startsWith(heyApiRegistryBaseUrl); -// || path.startsWith('http://localhost:4000') - -export const compileInputPath = (input: Omit) => { +export const compileInputPath = (input: Omit) => { const result: Pick< - Partial, + Partial, | 'api_key' | 'branch' | 'commit_sha' | 'organization' | 'project' + | 'registry' | 'tags' | 'version' > & - Pick, 'path'> = { + Pick = { + ...input, path: '', }; if ( input.path && - (typeof input.path !== 'string' || !isHeyApiRegistryPath(input.path)) + (typeof input.path !== 'string' || input.registry !== 'hey-api') ) { result.path = input.path; return result; @@ -131,94 +130,193 @@ export const compileInputPath = (input: Omit) => { return result; }; -const logInputPath = (inputPath: ReturnType) => { - const baseString = colors.cyan('Generating from'); +const logInputPaths = ( + inputPaths: ReadonlyArray>, + jobIndex: number, +) => { + const lines: Array = []; + + const jobPrefix = colors.gray(`[Job ${jobIndex + 1}] `); + const count = inputPaths.length; + const baseString = colors.cyan( + `Generating from ${count} ${count === 1 ? 'input' : 'inputs'}:`, + ); + lines.push(`${jobPrefix}⏳ ${baseString}`); + + inputPaths.forEach((inputPath, index) => { + const itemPrefixStr = ` [${index + 1}] `; + const itemPrefix = colors.cyan(itemPrefixStr); + const detailIndent = ' '.repeat(itemPrefixStr.length); + + if (typeof inputPath.path !== 'string') { + lines.push(`${jobPrefix}${itemPrefix}raw OpenAPI specification`); + return; + } - if (typeof inputPath.path === 'string') { - const baseInput = isHeyApiRegistryPath(inputPath.path) - ? `${inputPath.organization ?? ''}/${inputPath.project ?? ''}` - : inputPath.path; - console.log(`⏳ ${baseString} ${baseInput}`); - if (isHeyApiRegistryPath(inputPath.path)) { - if (inputPath.branch) { - console.log( - `${colors.gray('branch:')} ${colors.green(inputPath.branch)}`, + switch (inputPath.registry) { + case 'hey-api': { + const baseInput = [inputPath.organization, inputPath.project] + .filter(Boolean) + .join('/'); + lines.push(`${jobPrefix}${itemPrefix}${baseInput}`); + if (inputPath.branch) { + lines.push( + `${jobPrefix}${detailIndent}${colors.gray('branch:')} ${colors.green( + inputPath.branch, + )}`, + ); + } + if (inputPath.commit_sha) { + lines.push( + `${jobPrefix}${detailIndent}${colors.gray('commit:')} ${colors.green( + inputPath.commit_sha, + )}`, + ); + } + if (inputPath.tags?.length) { + lines.push( + `${jobPrefix}${detailIndent}${colors.gray('tags:')} ${colors.green( + inputPath.tags.join(', '), + )}`, + ); + } + if (inputPath.version) { + lines.push( + `${jobPrefix}${detailIndent}${colors.gray('version:')} ${colors.green( + inputPath.version, + )}`, + ); + } + lines.push( + `${jobPrefix}${detailIndent}${colors.gray('registry:')} ${colors.green('Hey API')}`, ); + break; } - if (inputPath.commit_sha) { - console.log( - `${colors.gray('commit:')} ${colors.green(inputPath.commit_sha)}`, + case 'readme': { + const baseInput = [inputPath.organization, inputPath.project] + .filter(Boolean) + .join('/'); + if (!baseInput) { + lines.push(`${jobPrefix}${itemPrefix}${inputPath.path}`); + } else { + lines.push(`${jobPrefix}${itemPrefix}${baseInput}`); + } + // @ts-expect-error + if (inputPath.uuid) { + lines.push( + `${jobPrefix}${detailIndent}${colors.gray('uuid:')} ${colors.green( + // @ts-expect-error + inputPath.uuid, + )}`, + ); + } + lines.push( + `${jobPrefix}${detailIndent}${colors.gray('registry:')} ${colors.green('ReadMe')}`, ); + break; } - if (inputPath.tags?.length) { - console.log( - `${colors.gray('tags:')} ${colors.green(inputPath.tags.join(', '))}`, - ); - } - if (inputPath.version) { - console.log( - `${colors.gray('version:')} ${colors.green(inputPath.version)}`, + case 'scalar': { + const baseInput = [inputPath.organization, inputPath.project] + .filter(Boolean) + .join('/'); + lines.push(`${jobPrefix}${itemPrefix}${baseInput}`); + lines.push( + `${jobPrefix}${detailIndent}${colors.gray('registry:')} ${colors.green('Scalar')}`, ); + break; } + default: + lines.push(`${jobPrefix}${itemPrefix}${inputPath.path}`); + break; } - } else { - console.log(`⏳ ${baseString} raw OpenAPI specification`); + }); + + for (const line of lines) { + console.log(line); } }; export const createClient = async ({ config, dependencies, + jobIndex, logger, templates, - watch: _watch, + watches: _watches, }: { config: Config; dependencies: Record; + jobIndex: number; logger: Logger; templates: Templates; /** - * Always falsy on the first run, truthy on subsequent runs. + * Always undefined on the first run, defined on subsequent runs. */ - watch?: WatchValues; -}) => { - const inputPath = compileInputPath(config.input); - const { timeout } = config.input.watch; + watches?: ReadonlyArray; +}): Promise => { + const watches: ReadonlyArray = + _watches || + Array.from({ length: config.input.length }, () => ({ + headers: new Headers(), + })); - const watch: WatchValues = _watch || { headers: new Headers() }; + const inputPaths = config.input.map((input) => compileInputPath(input)); // on first run, print the message as soon as possible - if (config.logs.level !== 'silent' && !_watch) { - logInputPath(inputPath); + if (config.logs.level !== 'silent' && !_watches) { + logInputPaths(inputPaths, jobIndex); } - const eventSpec = logger.timeEvent('spec'); - const { data, error, response } = await getSpec({ - fetchOptions: config.input.fetch, - inputPath: inputPath.path, - timeout, - watch, - }); - eventSpec.timeEnd(); + const getSpecData = async (input: Input, index: number) => { + const eventSpec = logger.timeEvent('spec'); + const { arrayBuffer, error, resolvedInput, response } = await getSpec({ + fetchOptions: input.fetch, + inputPath: inputPaths[index]!.path, + timeout: input.watch.timeout, + watch: watches[index]!, + }); + eventSpec.timeEnd(); + + // throw on first run if there's an error to preserve user experience + // if in watch mode, subsequent errors won't throw to gracefully handle + // cases where server might be reloading + if (error && !_watches) { + throw new Error( + `Request failed with status ${response.status}: ${response.statusText}`, + ); + } - // throw on first run if there's an error to preserve user experience - // if in watch mode, subsequent errors won't throw to gracefully handle - // cases where server might be reloading - if (error && !_watch) { - throw new Error( - `Request failed with status ${response.status}: ${response.statusText}`, - ); - } + return { arrayBuffer, resolvedInput }; + }; + const specData = ( + await Promise.all( + config.input.map((input, index) => getSpecData(input, index)), + ) + ).filter((data) => data.arrayBuffer || data.resolvedInput); let client: Client | undefined; let context: IR.Context | undefined; - if (data) { - // on subsequent runs in watch mode, print the mssage only if we know we're + if (specData.length) { + const refParser = new $RefParser(); + const data = + specData.length > 1 + ? await refParser.bundleMany({ + arrayBuffer: specData.map((data) => data.arrayBuffer!), + pathOrUrlOrSchemas: [], + resolvedInputs: specData.map((data) => data.resolvedInput!), + }) + : await refParser.bundle({ + arrayBuffer: specData[0]!.arrayBuffer, + pathOrUrlOrSchema: undefined, + resolvedInput: specData[0]!.resolvedInput, + }); + + // on subsequent runs in watch mode, print the message only if we know we're // generating the output - if (config.logs.level !== 'silent' && _watch) { + if (config.logs.level !== 'silent' && _watches) { console.clear(); - logInputPath(inputPath); + logInputPaths(inputPaths, jobIndex); } const eventInputPatch = logger.timeEvent('input.patch'); @@ -257,18 +355,31 @@ export const createClient = async ({ const outputPath = process.env.INIT_CWD ? `./${path.relative(process.env.INIT_CWD, config.output.path)}` : config.output.path; + const jobPrefix = colors.gray(`[Job ${jobIndex + 1}] `); console.log( - `${colors.green('🚀 Done!')} Your output is in ${colors.cyanBright(outputPath)}`, + `${jobPrefix}${colors.green('✅ Done!')} Your output is in ${colors.cyanBright(outputPath)}`, ); } } eventPostprocess.timeEnd(); } - if (config.input.watch.enabled && typeof inputPath.path === 'string') { + const watchedInput = config.input.find( + (input, index) => + input.watch.enabled && typeof inputPaths[index]!.path === 'string', + ); + + if (watchedInput) { setTimeout(() => { - createClient({ config, dependencies, logger, templates, watch }); - }, config.input.watch.interval); + createClient({ + config, + dependencies, + jobIndex, + logger, + templates, + watches, + }); + }, watchedInput.watch.interval); } return context || client; diff --git a/packages/openapi-ts/src/debug/ir.ts b/packages/openapi-ts/src/debug/ir.ts index 3a0fc0c2c2..1a0f62d39e 100644 --- a/packages/openapi-ts/src/debug/ir.ts +++ b/packages/openapi-ts/src/debug/ir.ts @@ -43,7 +43,7 @@ const print = (ir: IR.Model, options: PrintOptions = {}) => { if (verbosity === 'summary' && obj && typeof obj === 'object') { if (kind === 'responses') { const count = Object.keys(obj).length; - const noun = count !== 1 ? 'codes' : 'code'; + const noun = count === 1 ? 'code' : 'codes'; log(`responses: ${colors.yellow(`${count} ${noun}`)}`, level); } else if (kind === 'requestBody') { log(`requestBody: ${Object.keys(obj).join(', ')}`, level); diff --git a/packages/openapi-ts/src/error.ts b/packages/openapi-ts/src/error.ts index a4a1146c2a..94173f6dd4 100644 --- a/packages/openapi-ts/src/error.ts +++ b/packages/openapi-ts/src/error.ts @@ -3,10 +3,55 @@ import path from 'node:path'; import colors from 'ansi-colors'; -import { findPackageJson } from './generate/tsConfig'; +import { loadPackageJson } from './generate/tsConfig'; import { ensureDirSync } from './generate/utils'; -export class ConfigError extends Error {} +type IJobError = { + error: Error; + jobIndex: number; +}; + +/** + * Represents a single configuration error. + * + * Used for reporting issues with a specific config instance. + */ +export class ConfigError extends Error { + constructor(message: string) { + super(message); + this.name = 'ConfigError'; + } +} + +/** + * Aggregates multiple config errors with their job indices for reporting. + */ +export class ConfigValidationError extends Error { + readonly errors: ReadonlyArray; + + constructor(errors: Array) { + super( + `Found ${errors.length} configuration ${errors.length === 1 ? 'error' : 'errors'}.`, + ); + this.name = 'ConfigValidationError'; + this.errors = errors; + } +} + +/** + * Represents a runtime error originating from a specific job. + * + * Used for reporting job-level failures that are not config validation errors. + */ +export class JobError extends Error { + readonly originalError: IJobError; + + constructor(message: string, error: IJobError) { + super(message); + this.name = 'JobError'; + this.originalError = error; + } +} export class HeyApiError extends Error { args: ReadonlyArray; @@ -42,10 +87,14 @@ export const logCrashReport = ( error: unknown, logsDir: string, ): string | undefined => { - if (error instanceof ConfigError) { + if (error instanceof ConfigError || error instanceof ConfigValidationError) { return; } + if (error instanceof JobError) { + error = error.originalError.error; + } + const logName = `openapi-ts-error-${Date.now()}.log`; const fullDir = path.resolve(process.cwd(), logsDir); ensureDirSync(fullDir); @@ -75,6 +124,13 @@ export const logCrashReport = ( }; export const openGitHubIssueWithCrashReport = async (error: unknown) => { + const packageJson = loadPackageJson(); + if (!packageJson.bugs.url) return; + + if (error instanceof JobError) { + error = error.originalError.error; + } + let body = ''; if (error instanceof HeyApiError) { @@ -98,29 +154,9 @@ export const openGitHubIssueWithCrashReport = async (error: unknown) => { labels: 'bug 🔥', title: 'Crash Report', }); - - const packageJson = findPackageJson(); - let bugsUrl: string | undefined; - if ( - packageJson && - typeof packageJson === 'object' && - 'bugs' in packageJson && - packageJson.bugs && - typeof packageJson.bugs === 'object' && - 'url' in packageJson.bugs && - typeof packageJson.bugs.url === 'string' - ) { - bugsUrl = packageJson.bugs.url; - if (bugsUrl && !bugsUrl.endsWith('/')) { - bugsUrl += '/'; - } - } - - if (bugsUrl) { - const url = `${bugsUrl}new?${search.toString()}`; - const open = (await import('open')).default; - await open(url); - } + const url = `${packageJson.bugs.url}new?${search.toString()}`; + const open = (await import('open')).default; + await open(url); }; export const printCrashReport = ({ @@ -130,24 +166,50 @@ export const printCrashReport = ({ error: unknown; logPath: string | undefined; }) => { - const packageJson = findPackageJson(); - let name: string | undefined; - if ( - packageJson && - typeof packageJson === 'object' && - 'name' in packageJson && - typeof packageJson.name === 'string' - ) { - name = packageJson.name; + if (error instanceof ConfigValidationError && error.errors.length) { + const groupByJob = new Map>(); + for (const { error: err, jobIndex } of error.errors) { + if (!groupByJob.has(jobIndex)) { + groupByJob.set(jobIndex, []); + } + groupByJob.get(jobIndex)!.push(err); + } + + for (const [jobIndex, errors] of groupByJob.entries()) { + const jobPrefix = colors.gray(`[Job ${jobIndex + 1}] `); + const count = errors.length; + const baseString = colors.red( + `Found ${count} configuration ${count === 1 ? 'error' : 'errors'}:`, + ); + console.error(`${jobPrefix}❗️ ${baseString}`); + errors.forEach((err, index) => { + const itemPrefixStr = ` [${index + 1}] `; + const itemPrefix = colors.red(itemPrefixStr); + console.error(`${jobPrefix}${itemPrefix}${colors.white(err.message)}`); + }); + } + } else { + let jobPrefix = colors.gray('[root] '); + if (error instanceof JobError) { + jobPrefix = colors.gray(`[Job ${error.originalError.jobIndex + 1}] `); + error = error.originalError.error; + } + + const baseString = colors.red('Failed with the message:'); + console.error(`${jobPrefix}❌ ${baseString}`); + const itemPrefixStr = ` `; + const itemPrefix = colors.red(itemPrefixStr); + console.error( + `${jobPrefix}${itemPrefix}${typeof error === 'string' ? error : error instanceof Error ? error.message : 'Unknown error'}`, + ); + } + + if (logPath) { + const jobPrefix = colors.gray('[root] '); + console.error( + `${jobPrefix}${colors.cyan('📄 Crash log saved to:')} ${colors.gray(logPath)}`, + ); } - process.stderr.write( - `\n🛑 ${colors.cyan(name || '')} ${colors.red('encountered an error.')}` + - `\n\n${colors.red('❗️ Error:')} ${colors.white(typeof error === 'string' ? error : error instanceof Error ? error.message : 'Unknown error')}` + - (logPath - ? `\n\n${colors.cyan('📄 Crash log saved to:')} ${colors.gray(logPath)}` - : '') + - '\n', - ); }; export const shouldReportCrash = async ({ @@ -157,13 +219,18 @@ export const shouldReportCrash = async ({ error: unknown; isInteractive: boolean | undefined; }): Promise => { - if (!isInteractive || error instanceof ConfigError) { + if ( + !isInteractive || + error instanceof ConfigError || + error instanceof ConfigValidationError + ) { return false; } return new Promise((resolve) => { - process.stdout.write( - `${colors.yellow('\n📢 Open a GitHub issue with crash details?')} ${colors.yellow('(y/N):')}`, + const jobPrefix = colors.gray('[root] '); + console.log( + `${jobPrefix}${colors.yellow('📢 Open a GitHub issue with crash details? (y/N):')}`, ); process.stdin.setEncoding('utf8'); process.stdin.once('data', (data: string) => { diff --git a/packages/openapi-ts/src/generate/__tests__/class.test.ts b/packages/openapi-ts/src/generate/__tests__/class.test.ts index 3b73b5d614..343b519c58 100644 --- a/packages/openapi-ts/src/generate/__tests__/class.test.ts +++ b/packages/openapi-ts/src/generate/__tests__/class.test.ts @@ -17,14 +17,16 @@ describe('generateLegacyClientClass', () => { dryRun: false, experimentalParser: false, exportCore: true, - input: { - path: '', - watch: { - enabled: false, - interval: 1_000, - timeout: 60_000, + input: [ + { + path: '', + watch: { + enabled: false, + interval: 1_000, + timeout: 60_000, + }, }, - }, + ], interactive: false, logs: { file: true, diff --git a/packages/openapi-ts/src/generate/__tests__/core.test.ts b/packages/openapi-ts/src/generate/__tests__/core.test.ts index 84384d2aaa..2f62ed8740 100644 --- a/packages/openapi-ts/src/generate/__tests__/core.test.ts +++ b/packages/openapi-ts/src/generate/__tests__/core.test.ts @@ -32,14 +32,16 @@ describe('generateLegacyCore', () => { dryRun: false, experimentalParser: false, exportCore: true, - input: { - path: '', - watch: { - enabled: false, - interval: 1_000, - timeout: 60_000, + input: [ + { + path: '', + watch: { + enabled: false, + interval: 1_000, + timeout: 60_000, + }, }, - }, + ], interactive: false, logs: { file: true, @@ -187,14 +189,16 @@ describe('generateLegacyCore', () => { dryRun: false, experimentalParser: false, exportCore: true, - input: { - path: '', - watch: { - enabled: false, - interval: 1_000, - timeout: 60_000, + input: [ + { + path: '', + watch: { + enabled: false, + interval: 1_000, + timeout: 60_000, + }, }, - }, + ], interactive: false, logs: { file: true, @@ -325,14 +329,16 @@ describe('generateLegacyCore', () => { dryRun: false, experimentalParser: false, exportCore: true, - input: { - path: '', - watch: { - enabled: false, - interval: 1_000, - timeout: 60_000, + input: [ + { + path: '', + watch: { + enabled: false, + interval: 1_000, + timeout: 60_000, + }, }, - }, + ], interactive: false, logs: { file: true, diff --git a/packages/openapi-ts/src/generate/legacy/__tests__/index.test.ts b/packages/openapi-ts/src/generate/legacy/__tests__/index.test.ts index b16e707804..1745853d10 100644 --- a/packages/openapi-ts/src/generate/legacy/__tests__/index.test.ts +++ b/packages/openapi-ts/src/generate/legacy/__tests__/index.test.ts @@ -17,14 +17,16 @@ describe('generateIndexFile', () => { dryRun: false, experimentalParser: false, exportCore: true, - input: { - path: '', - watch: { - enabled: false, - interval: 1_000, - timeout: 60_000, + input: [ + { + path: '', + watch: { + enabled: false, + interval: 1_000, + timeout: 60_000, + }, }, - }, + ], interactive: false, logs: { file: true, diff --git a/packages/openapi-ts/src/generate/legacy/__tests__/output.test.ts b/packages/openapi-ts/src/generate/legacy/__tests__/output.test.ts index 55b6680aab..3a7a2cfdbc 100644 --- a/packages/openapi-ts/src/generate/legacy/__tests__/output.test.ts +++ b/packages/openapi-ts/src/generate/legacy/__tests__/output.test.ts @@ -30,14 +30,16 @@ describe('generateLegacyOutput', () => { dryRun: false, experimentalParser: false, exportCore: true, - input: { - path: '', - watch: { - enabled: false, - interval: 1_000, - timeout: 60_000, + input: [ + { + path: '', + watch: { + enabled: false, + interval: 1_000, + timeout: 60_000, + }, }, - }, + ], interactive: false, logs: { file: true, diff --git a/packages/openapi-ts/src/generate/tsConfig.ts b/packages/openapi-ts/src/generate/tsConfig.ts index 35990a0c92..f2040ce68f 100644 --- a/packages/openapi-ts/src/generate/tsConfig.ts +++ b/packages/openapi-ts/src/generate/tsConfig.ts @@ -30,6 +30,46 @@ export const findPackageJson = (): unknown | undefined => { return; }; +export const loadPackageJson = () => { + const packageJson = findPackageJson(); + + const safePackage = { + bugs: { + url: '', + }, + name: '', + version: '', + }; + + if (packageJson && typeof packageJson === 'object') { + if ('name' in packageJson && typeof packageJson.name === 'string') { + safePackage.name = packageJson.name; + } + + if ('version' in packageJson && typeof packageJson.version === 'string') { + safePackage.version = packageJson.version; + } + + if ( + 'bugs' in packageJson && + packageJson.bugs && + typeof packageJson.bugs === 'object' + ) { + if ( + 'url' in packageJson.bugs && + typeof packageJson.bugs.url === 'string' + ) { + safePackage.bugs.url = packageJson.bugs.url; + if (safePackage.bugs.url && !safePackage.bugs.url.endsWith('/')) { + safePackage.bugs.url += '/'; + } + } + } + } + + return safePackage; +}; + export const findTsConfigPath = ( tsConfigPath?: UserOutput['tsConfigPath'], ): string | null => { diff --git a/packages/openapi-ts/src/getSpec.ts b/packages/openapi-ts/src/getSpec.ts index def64e47c9..1d5d0f0cfe 100644 --- a/packages/openapi-ts/src/getSpec.ts +++ b/packages/openapi-ts/src/getSpec.ts @@ -1,25 +1,22 @@ -import { - $RefParser, - getResolvedInput, - type JSONSchema, - sendRequest, -} from '@hey-api/json-schema-ref-parser'; +import { getResolvedInput, sendRequest } from '@hey-api/json-schema-ref-parser'; import { mergeHeaders } from './plugins/@hey-api/client-fetch/bundle'; -import type { Config } from './types/config'; +import type { Input } from './types/input'; import type { WatchValues } from './types/types'; -interface SpecResponse { - data: JSONSchema; - error?: undefined; - response?: undefined; -} +type SpecResponse = { + arrayBuffer: ArrayBuffer | undefined; + error?: never; + resolvedInput: ReturnType; + response?: never; +}; -interface SpecError { - data?: undefined; +type SpecError = { + arrayBuffer?: never; error: 'not-modified' | 'not-ok'; + resolvedInput?: never; response: Response; -} +}; /** * @internal @@ -31,11 +28,10 @@ export const getSpec = async ({ watch, }: { fetchOptions?: RequestInit; - inputPath: Config['input']['path']; + inputPath: Input['path']; timeout: number | undefined; watch: WatchValues; }): Promise => { - const refParser = new $RefParser(); const resolvedInput = getResolvedInput({ pathOrUrlOrSchema: inputPath }); let arrayBuffer: ArrayBuffer | undefined; @@ -182,13 +178,8 @@ export const getSpec = async ({ }; } - const data = await refParser.bundle({ + return { arrayBuffer, - pathOrUrlOrSchema: undefined, resolvedInput, - }); - - return { - data, }; }; diff --git a/packages/openapi-ts/src/index.ts b/packages/openapi-ts/src/index.ts index fcf954b5db..5a2ed939ac 100644 --- a/packages/openapi-ts/src/index.ts +++ b/packages/openapi-ts/src/index.ts @@ -5,10 +5,13 @@ import colors from 'ansi-colors'; import colorSupport from 'color-support'; import { checkNodeVersion } from './config/engine'; +import type { Configs } from './config/init'; import { initConfigs } from './config/init'; import { getLogs } from './config/logs'; import { createClient as pCreateClient } from './createClient'; import { + ConfigValidationError, + JobError, logCrashReport, openGitHubIssueWithCrashReport, printCrashReport, @@ -16,27 +19,39 @@ import { } from './error'; import type { IR } from './ir/types'; import type { Client } from './types/client'; -import type { Config, UserConfig } from './types/config'; +import type { UserConfig } from './types/config'; +import type { LazyOrAsync, MaybeArray } from './types/utils'; +import { printCliIntro } from './utils/cli'; import { registerHandlebarTemplates } from './utils/handlebars'; import { Logger } from './utils/logger'; -type Configs = UserConfig | (() => UserConfig) | (() => Promise); - colors.enabled = colorSupport().hasBasic; /** * Generate a client from the provided configuration. * - * @param userConfig User provided {@link UserConfig} configuration. + * @param userConfig User provided {@link UserConfig} configuration(s). */ export const createClient = async ( - userConfig?: Configs, + userConfig?: LazyOrAsync>, logger = new Logger(), ): Promise> => { const resolvedConfig = typeof userConfig === 'function' ? await userConfig() : userConfig; + const userConfigs = resolvedConfig + ? resolvedConfig instanceof Array + ? resolvedConfig + : [resolvedConfig] + : []; + + let rawLogs = userConfigs.find( + (config) => getLogs(config).level !== 'silent', + )?.logs; + if (typeof rawLogs === 'string') { + rawLogs = getLogs({ logs: rawLogs }); + } - const configs: Array = []; + let configs: Configs | undefined; try { checkNodeVersion(); @@ -44,28 +59,43 @@ export const createClient = async ( const eventCreateClient = logger.timeEvent('createClient'); const eventConfig = logger.timeEvent('config'); - const configResults = await initConfigs(resolvedConfig); - for (const result of configResults.results) { - configs.push(result.config); - if (result.errors.length) { - throw result.errors[0]; - } + configs = await initConfigs({ logger, userConfigs }); + const printIntro = configs.results.some( + (result) => result.config.logs.level !== 'silent', + ); + if (printIntro) { + printCliIntro(); } eventConfig.timeEnd(); + const allConfigErrors = configs.results.flatMap((result) => + result.errors.map((error) => ({ error, jobIndex: result.jobIndex })), + ); + if (allConfigErrors.length) { + throw new ConfigValidationError(allConfigErrors); + } + const eventHandlebars = logger.timeEvent('handlebars'); const templates = registerHandlebarTemplates(); eventHandlebars.timeEnd(); const clients = await Promise.all( - configs.map((config) => - pCreateClient({ - config, - dependencies: configResults.dependencies, - logger, - templates, - }), - ), + configs.results.map(async (result) => { + try { + return await pCreateClient({ + config: result.config, + dependencies: configs!.dependencies, + jobIndex: result.jobIndex, + logger, + templates, + }); + } catch (error) { + throw new JobError('', { + error, + jobIndex: result.jobIndex, + }); + } + }), ); const result = clients.filter((client) => Boolean(client)) as ReadonlyArray< Client | IR.Context @@ -73,26 +103,33 @@ export const createClient = async ( eventCreateClient.timeEnd(); - const config = configs[0]; - logger.report(config && config.logs.level === 'debug'); + const printLogs = configs.results.some( + (result) => result.config.logs.level === 'debug', + ); + logger.report(printLogs); return result; } catch (error) { - const config = configs[0] as Config | undefined; - const dryRun = config ? config.dryRun : resolvedConfig?.dryRun; - const isInteractive = config - ? config.interactive - : resolvedConfig?.interactive; - const logs = config?.logs ?? getLogs(resolvedConfig); - - let logPath: string | undefined; - - if (logs.level !== 'silent' && logs.file && !dryRun) { - logPath = logCrashReport(error, logs.path ?? ''); - } + const results = configs?.results ?? []; + + const logs = + results.find((result) => result.config.logs.level !== 'silent')?.config + .logs ?? rawLogs; + if (!logs || logs.level !== 'silent') { + const dryRun = + results.some((result) => result.config.dryRun) ?? + userConfigs.some((config) => config.dryRun) ?? + false; + const logPath = + logs?.file && !dryRun + ? logCrashReport(error, logs.path ?? '') + : undefined; - if (logs.level !== 'silent') { printCrashReport({ error, logPath }); + const isInteractive = + results.some((result) => result.config.interactive) ?? + userConfigs.some((config) => config.interactive) ?? + false; if (await shouldReportCrash({ error, isInteractive })) { await openGitHubIssueWithCrashReport(error); } @@ -103,10 +140,11 @@ export const createClient = async ( }; /** - * Type helper for openapi-ts.config.ts, returns {@link UserConfig} object + * Type helper for openapi-ts.config.ts, returns {@link MaybeArray} object(s) */ -export const defineConfig = async (config: Configs): Promise => - typeof config === 'function' ? await config() : config; +export const defineConfig = async >( + config: LazyOrAsync, +): Promise => (typeof config === 'function' ? await config() : config); export { defaultPaginationKeywords } from './config/parser'; export { defaultPlugins } from './config/plugins'; diff --git a/packages/openapi-ts/src/openApi/2.0.x/parser/__tests__/server.test.ts b/packages/openapi-ts/src/openApi/2.0.x/parser/__tests__/server.test.ts index bcfdc961cd..e1a64a0cc7 100644 --- a/packages/openapi-ts/src/openApi/2.0.x/parser/__tests__/server.test.ts +++ b/packages/openapi-ts/src/openApi/2.0.x/parser/__tests__/server.test.ts @@ -8,10 +8,12 @@ describe('parseServers', () => { it('host + basePath + schemes', () => { const context: Partial>> = { config: { - // @ts-expect-error - input: { - path: '', - }, + input: [ + // @ts-expect-error + { + path: '', + }, + ], }, ir: {}, spec: { @@ -34,10 +36,12 @@ describe('parseServers', () => { it('schemes + host', () => { const context: Partial>> = { config: { - // @ts-expect-error - input: { - path: '', - }, + input: [ + // @ts-expect-error + { + path: '', + }, + ], }, ir: {}, spec: { @@ -56,10 +60,12 @@ describe('parseServers', () => { it('host + basePath', () => { const context: Partial>> = { config: { - // @ts-expect-error - input: { - path: '', - }, + input: [ + // @ts-expect-error + { + path: '', + }, + ], }, ir: {}, spec: { @@ -78,10 +84,12 @@ describe('parseServers', () => { it('host', () => { const context: Partial>> = { config: { - // @ts-expect-error - input: { - path: '', - }, + input: [ + // @ts-expect-error + { + path: '', + }, + ], }, ir: {}, spec: { @@ -99,10 +107,12 @@ describe('parseServers', () => { it('basePath', () => { const context: Partial>> = { config: { - // @ts-expect-error - input: { - path: '', - }, + input: [ + // @ts-expect-error + { + path: '', + }, + ], }, ir: {}, spec: { diff --git a/packages/openapi-ts/src/openApi/2.0.x/parser/schema.ts b/packages/openapi-ts/src/openApi/2.0.x/parser/schema.ts index b2357d7cc2..6b03b24f1a 100644 --- a/packages/openapi-ts/src/openApi/2.0.x/parser/schema.ts +++ b/packages/openapi-ts/src/openApi/2.0.x/parser/schema.ts @@ -232,7 +232,7 @@ const parseObject = ({ const isEmptyObjectInAllOf = state.inAllOf && schema.additionalProperties === false && - (!schema.properties || Object.keys(schema.properties).length === 0); + (!schema.properties || !Object.keys(schema.properties).length); if (!isEmptyObjectInAllOf) { irSchema.additionalProperties = { diff --git a/packages/openapi-ts/src/openApi/2.0.x/parser/server.ts b/packages/openapi-ts/src/openApi/2.0.x/parser/server.ts index e6050f379b..b33f50d30c 100644 --- a/packages/openapi-ts/src/openApi/2.0.x/parser/server.ts +++ b/packages/openapi-ts/src/openApi/2.0.x/parser/server.ts @@ -6,17 +6,19 @@ export const parseServers = ({ context }: { context: IR.Context }) => { let host = context.spec.host ?? ''; const path = context.spec.basePath ?? ''; - if (typeof context.config.input.path === 'string') { - const url = parseUrl(context.config.input.path); + for (const input of context.config.input) { + if (typeof input.path === 'string') { + const url = parseUrl(input.path); - if (!schemes.length) { - if (url.protocol) { - schemes = [url.protocol] as typeof schemes; + if (!schemes.length) { + if (url.protocol) { + schemes = [url.protocol] as typeof schemes; + } } - } - if (!host) { - host = `${url.host}${url.port ? `:${url.port}` : ''}`; + if (!host) { + host = `${url.host}${url.port ? `:${url.port}` : ''}`; + } } } diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts index 1cd74e69c3..b80ca1949d 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts @@ -238,7 +238,7 @@ const parseObject = ({ const isEmptyObjectInAllOf = state.inAllOf && schema.additionalProperties === false && - (!schema.properties || Object.keys(schema.properties).length === 0); + (!schema.properties || !Object.keys(schema.properties).length); if (!isEmptyObjectInAllOf) { irSchema.additionalProperties = { diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/server.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/server.ts index 361af22514..8fb309f001 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/server.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/server.ts @@ -7,13 +7,15 @@ export const parseServers = ({ context }: { context: IR.Context }) => { return; } - if (typeof context.config.input.path === 'string') { - const url = parseUrl(context.config.input.path); - context.ir.servers = [ - { - url: `${url.protocol ? `${url.protocol}://` : ''}${url.host}${url.port ? `:${url.port}` : ''}`, - }, - ]; + for (const input of context.config.input) { + if (typeof input.path === 'string') { + const url = parseUrl(input.path); + context.ir.servers = [ + { + url: `${url.protocol ? `${url.protocol}://` : ''}${url.host}${url.port ? `:${url.port}` : ''}`, + }, + ]; + } } if (!context.ir.servers) { diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts index 970fbe6225..b2ac1c3f7a 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts @@ -292,9 +292,9 @@ const parseObject = ({ const isEmptyObjectInAllOf = state.inAllOf && schema.additionalProperties === false && - (!schema.properties || Object.keys(schema.properties).length === 0) && + (!schema.properties || !Object.keys(schema.properties).length) && (!schema.patternProperties || - Object.keys(schema.patternProperties).length === 0); + !Object.keys(schema.patternProperties).length); if (!isEmptyObjectInAllOf) { irSchema.additionalProperties = { diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/server.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/server.ts index 361af22514..8fb309f001 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/server.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/server.ts @@ -7,13 +7,15 @@ export const parseServers = ({ context }: { context: IR.Context }) => { return; } - if (typeof context.config.input.path === 'string') { - const url = parseUrl(context.config.input.path); - context.ir.servers = [ - { - url: `${url.protocol ? `${url.protocol}://` : ''}${url.host}${url.port ? `:${url.port}` : ''}`, - }, - ]; + for (const input of context.config.input) { + if (typeof input.path === 'string') { + const url = parseUrl(input.path); + context.ir.servers = [ + { + url: `${url.protocol ? `${url.protocol}://` : ''}${url.host}${url.port ? `:${url.port}` : ''}`, + }, + ]; + } } if (!context.ir.servers) { diff --git a/packages/openapi-ts/src/openApi/3.1.x/types/json-schema-draft-2020-12.d.ts b/packages/openapi-ts/src/openApi/3.1.x/types/json-schema-draft-2020-12.d.ts index 807a630e71..68ce48afe4 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/types/json-schema-draft-2020-12.d.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/types/json-schema-draft-2020-12.d.ts @@ -1,3 +1,4 @@ +import type { MaybeArray } from '../../../types/utils'; import type { EnumExtensions } from '../../shared/types/openapi-spec-extensions'; import type { OpenApiSchemaExtensions } from './spec-extensions'; @@ -144,7 +145,7 @@ export interface JsonSchemaDraft2020_12 /** * If it is an array, it must be an array of strings, where each string is the name of one of the basic types, and each element is unique. In this case, the JSON snippet is valid if it matches any of the given types. */ - type?: JsonSchemaTypes | ReadonlyArray; + type?: MaybeArray; /** * The boolean keywords `readOnly` and `writeOnly` are typically used in an API context. `readOnly` indicates that a value should not be modified. It could be used to indicate that a `PUT` request that changes a value would result in a `400 Bad Request` response. `writeOnly` indicates that a value may be set, but will remain hidden. In could be used to indicate you can set a value with a `PUT` request, but it would not be included when retrieving that record with a `GET` request. */ diff --git a/packages/openapi-ts/src/openApi/shared/transforms/readWrite.ts b/packages/openapi-ts/src/openApi/shared/transforms/readWrite.ts index 884c0def1e..ce09fb10c6 100644 --- a/packages/openapi-ts/src/openApi/shared/transforms/readWrite.ts +++ b/packages/openapi-ts/src/openApi/shared/transforms/readWrite.ts @@ -233,7 +233,7 @@ const pruneSchemaByScope = ( (prop) => !removedProperties.has(prop), ); - if (filteredRequired.length === 0) { + if (!filteredRequired.length) { delete (schema as Record).required; } else { (schema as Record).required = filteredRequired; diff --git a/packages/openapi-ts/src/plugins/@hey-api/schemas/__tests__/schemas.test.ts b/packages/openapi-ts/src/plugins/@hey-api/schemas/__tests__/schemas.test.ts index 23142baed9..366c65684e 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/schemas/__tests__/schemas.test.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/schemas/__tests__/schemas.test.ts @@ -20,14 +20,16 @@ describe('generateLegacySchemas', () => { dryRun: false, experimentalParser: false, exportCore: true, - input: { - path: '', - watch: { - enabled: false, - interval: 1_000, - timeout: 60_000, + input: [ + { + path: '', + watch: { + enabled: false, + interval: 1_000, + timeout: 60_000, + }, }, - }, + ], interactive: false, logs: { file: true, @@ -180,14 +182,16 @@ describe('generateLegacySchemas', () => { dryRun: false, experimentalParser: false, exportCore: true, - input: { - path: '', - watch: { - enabled: false, - interval: 1_000, - timeout: 60_000, + input: [ + { + path: '', + watch: { + enabled: false, + interval: 1_000, + timeout: 60_000, + }, }, - }, + ], interactive: false, logs: { file: true, diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/__tests__/plugin.test.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/__tests__/plugin.test.ts index f7e41c0f73..c750068069 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/__tests__/plugin.test.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/__tests__/plugin.test.ts @@ -23,14 +23,16 @@ describe('handlerLegacy', () => { dryRun: false, experimentalParser: false, exportCore: true, - input: { - path: '', - watch: { - enabled: false, - interval: 1_000, - timeout: 60_000, + input: [ + { + path: '', + watch: { + enabled: false, + interval: 1_000, + timeout: 60_000, + }, }, - }, + ], interactive: false, logs: { file: true, @@ -256,14 +258,16 @@ describe('methodNameBuilder', () => { dryRun: false, experimentalParser: false, exportCore: true, - input: { - path: '', - watch: { - enabled: false, - interval: 1_000, - timeout: 60_000, + input: [ + { + path: '', + watch: { + enabled: false, + interval: 1_000, + timeout: 60_000, + }, }, - }, + ], interactive: false, logs: { file: true, @@ -411,14 +415,16 @@ describe('methodNameBuilder', () => { dryRun: false, experimentalParser: false, exportCore: true, - input: { - path: '', - watch: { - enabled: false, - interval: 1_000, - timeout: 60_000, + input: [ + { + path: '', + watch: { + enabled: false, + interval: 1_000, + timeout: 60_000, + }, }, - }, + ], interactive: false, logs: { file: true, @@ -569,14 +575,16 @@ describe('methodNameBuilder', () => { dryRun: false, experimentalParser: false, exportCore: true, - input: { - path: '', - watch: { - enabled: false, - interval: 1_000, - timeout: 60_000, + input: [ + { + path: '', + watch: { + enabled: false, + interval: 1_000, + timeout: 60_000, + }, }, - }, + ], interactive: false, logs: { file: true, diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/__tests__/plugin.test.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/__tests__/plugin.test.ts index 3ddd9dd0de..b7e6847105 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/__tests__/plugin.test.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/__tests__/plugin.test.ts @@ -20,14 +20,16 @@ describe('generateLegacyTypes', () => { dryRun: false, experimentalParser: false, exportCore: true, - input: { - path: '', - watch: { - enabled: false, - interval: 1_000, - timeout: 60_000, + input: [ + { + path: '', + watch: { + enabled: false, + interval: 1_000, + timeout: 60_000, + }, }, - }, + ], interactive: false, logs: { file: true, diff --git a/packages/openapi-ts/src/types/config.d.ts b/packages/openapi-ts/src/types/config.d.ts index 2654762562..2d585e68df 100644 --- a/packages/openapi-ts/src/types/config.d.ts +++ b/packages/openapi-ts/src/types/config.d.ts @@ -1,9 +1,10 @@ import type { PluginConfigMap } from '../plugins/config'; import type { Plugin, PluginNames } from '../plugins/types'; -import type { Input, Watch } from './input'; +import type { Input, UserInput, Watch } from './input'; import type { Logs } from './logs'; import type { Output, UserOutput } from './output'; import type { Parser, UserParser } from './parser'; +import type { MaybeArray } from './utils'; export interface UserConfig { /** @@ -27,16 +28,12 @@ export interface UserConfig { * object directly if you're fetching the file yourself. * * Alternatively, you can define a configuration object with more options. + * + * If you define an array, we will generate a single output from multiple + * inputs. If you define an array of outputs with the same length, we will + * generate multiple outputs, one for each input. */ - input: - | `https://get.heyapi.dev/${string}/${string}` - | `${string}/${string}` - | `readme:@${string}/${string}#${string}` - | `readme:${string}` - | `scalar:@${string}/${string}` - | (string & {}) - | (Record & { path?: never }) - | Input; + input: MaybeArray['path']>; /** * Show an interactive error reporting tool when the program crashes? You * generally want to keep this disabled (default). @@ -52,8 +49,11 @@ export interface UserConfig { logs?: string | Logs; /** * Path to the output folder. + * + * If you define an array of outputs with the same length as inputs, we will + * generate multiple outputs, one for each input. */ - output: string | UserOutput; + output: MaybeArray; /** * Customize how the input is parsed and transformed before it's passed to * plugins. @@ -142,9 +142,14 @@ export type Config = Omit< | 'watch' > & Pick & { - input: Omit & - Pick, 'path'> & { watch: Watch }; + /** + * Path to the input specification. + */ + input: ReadonlyArray; logs: Logs; + /** + * Path to the output folder. + */ output: Output; /** * Customize how the input is parsed and transformed before it's passed to diff --git a/packages/openapi-ts/src/types/input.d.ts b/packages/openapi-ts/src/types/input.d.ts index e5b26e5ff0..4821d19a1d 100644 --- a/packages/openapi-ts/src/types/input.d.ts +++ b/packages/openapi-ts/src/types/input.d.ts @@ -1,4 +1,13 @@ -export type Input = { +type JsonSchema = Record; + +type ApiRegistryShorthands = + | `https://get.heyapi.dev/${string}/${string}` + | `${string}/${string}` + | `readme:@${string}/${string}#${string}` + | `readme:${string}` + | `scalar:@${string}/${string}`; + +export type UserInput = { /** * **Requires `path` to start with `https://get.heyapi.dev` or be undefined** * @@ -44,14 +53,7 @@ export type Input = { * Both JSON and YAML file formats are supported. You can also pass the parsed * object directly if you're fetching the file yourself. */ - path?: - | `https://get.heyapi.dev/${string}/${string}` - | `${string}/${string}` - | `readme:@${string}/${string}#${string}` - | `readme:${string}` - | `scalar:@${string}/${string}` - | (string & {}) - | Record; + path?: ApiRegistryShorthands | (string & {}) | JsonSchema; /** * **Requires `path` to start with `https://get.heyapi.dev` or be undefined** * @@ -83,6 +85,89 @@ export type Input = { watch?: boolean | number | Watch; }; +export type Input = { + /** + * **Requires `path` to start with `https://get.heyapi.dev` or be undefined** + * + * Projects are private by default, you will need to be authenticated + * to download OpenAPI specifications. We recommend using project API + * keys in CI workflows and personal API keys for local development. + * + * API key isn't required for public projects. You can also omit this + * parameter and provide an environment variable `HEY_API_TOKEN`. + */ + api_key?: string; + /** + * **Requires `path` to start with `https://get.heyapi.dev` or be undefined** + * + * You can fetch the last build from branch by providing the branch + * name. + */ + branch?: string; + /** + * **Requires `path` to start with `https://get.heyapi.dev` or be undefined** + * + * You can fetch an exact specification by providing a commit sha. + * This will always return the same file. + */ + commit_sha?: string; + /** + * You can pass any valid Fetch API options to the request for fetching your + * specification. This is useful if your file is behind auth for example. + */ + fetch?: RequestInit; + /** + * **Requires `path` to start with `https://get.heyapi.dev` or be undefined** + * + * Organization created in Hey API Platform. + */ + organization?: string; + /** + * Path to the OpenAPI specification. This can be: + * - path + * - URL + * - API registry shorthand + * + * Both JSON and YAML file formats are supported. You can also pass the parsed + * object directly if you're fetching the file yourself. + */ + path: ApiRegistryShorthands | (string & {}) | JsonSchema; + /** + * **Requires `path` to start with `https://get.heyapi.dev` or be undefined** + * + * Project created in Hey API Platform. + */ + project?: string; + /** + * If input path was resolved to a registry, this contains the registry + * identifier so we don't need to parse it again. + * + * @default undefined + */ + registry?: 'hey-api' | 'readme' | 'scalar'; + /** + * **Requires `path` to start with `https://get.heyapi.dev` or be undefined** + * + * If you're tagging your specifications with custom tags, you can use + * them to filter the results. When you provide multiple tags, only + * the first match will be returned. + */ + tags?: ReadonlyArray; + /** + * **Requires `path` to start with `https://get.heyapi.dev` or be undefined** + * + * Every OpenAPI document contains a required version field. You can + * use this value to fetch the last uploaded specification matching + * the value. + */ + version?: string; + /** + * Regenerate the client when the input file changes? You can alternatively + * pass a numeric value for the interval in ms. + */ + watch: Watch; +}; + export type Watch = { /** * Regenerate the client when the input file changes? diff --git a/packages/openapi-ts/src/types/utils.d.ts b/packages/openapi-ts/src/types/utils.d.ts index 7db0b17699..ef9b7ae814 100644 --- a/packages/openapi-ts/src/types/utils.d.ts +++ b/packages/openapi-ts/src/types/utils.d.ts @@ -1,6 +1,15 @@ import type { GeneratedFile } from '../generate/file'; -/** Recursively make all non-function properties optional */ +/** + * Converts all top-level ReadonlyArray properties to Array (shallow). + */ +export type ArrayOnly = { + [K in keyof T]: T[K] extends ReadonlyArray ? Array : T[K]; +}; + +/** + * Recursively makes all non-function properties optional. + */ export type DeepPartial = { [K in keyof T]?: T[K] extends (...args: any[]) => any ? T[K] @@ -9,4 +18,22 @@ export type DeepPartial = { : T[K]; }; +/** @deprecated */ export type Files = Record; + +/** + * Accepts a value, a function returning a value, or a function returning a promise of a value. + */ +export type LazyOrAsync = T | (() => T) | (() => Promise); + +/** + * Accepts a value or a readonly array of values of type T. + */ +export type MaybeArray = T | ReadonlyArray; + +/** + * Converts all top-level Array properties to ReadonlyArray (shallow). + */ +export type ReadonlyArrayOnly = { + [K in keyof T]: T[K] extends Array ? ReadonlyArray : T[K]; +}; diff --git a/packages/openapi-ts/src/utils/__tests__/handlebars.test.ts b/packages/openapi-ts/src/utils/__tests__/handlebars.test.ts index 53d4dba6f1..051bd15b96 100644 --- a/packages/openapi-ts/src/utils/__tests__/handlebars.test.ts +++ b/packages/openapi-ts/src/utils/__tests__/handlebars.test.ts @@ -15,14 +15,16 @@ describe('registerHandlebarHelpers', () => { dryRun: false, experimentalParser: false, exportCore: true, - input: { - path: '', - watch: { - enabled: false, - interval: 1_000, - timeout: 60_000, + input: [ + { + path: '', + watch: { + enabled: false, + interval: 1_000, + timeout: 60_000, + }, }, - }, + ], interactive: false, logs: { file: true, @@ -143,14 +145,16 @@ describe('registerHandlebarTemplates', () => { dryRun: false, experimentalParser: false, exportCore: true, - input: { - path: '', - watch: { - enabled: false, - interval: 1_000, - timeout: 60_000, + input: [ + { + path: '', + watch: { + enabled: false, + interval: 1_000, + timeout: 60_000, + }, }, - }, + ], interactive: false, logs: { file: true, diff --git a/packages/openapi-ts/src/utils/__tests__/parse.test.ts b/packages/openapi-ts/src/utils/__tests__/parse.test.ts index 76f1b1aeab..d297664e11 100644 --- a/packages/openapi-ts/src/utils/__tests__/parse.test.ts +++ b/packages/openapi-ts/src/utils/__tests__/parse.test.ts @@ -9,14 +9,16 @@ describe('operationNameFn', () => { dryRun: true, experimentalParser: false, exportCore: false, - input: { - path: '', - watch: { - enabled: false, - interval: 1_000, - timeout: 60_000, + input: [ + { + path: '', + watch: { + enabled: false, + interval: 1_000, + timeout: 60_000, + }, }, - }, + ], interactive: false, logs: { file: true, diff --git a/packages/openapi-ts/src/utils/cli.ts b/packages/openapi-ts/src/utils/cli.ts new file mode 100644 index 0000000000..371bda7bd0 --- /dev/null +++ b/packages/openapi-ts/src/utils/cli.ts @@ -0,0 +1,63 @@ +import colors from 'ansi-colors'; + +import { loadPackageJson } from '../generate/tsConfig'; + +const logoAscii = ` +db e888888e + d8 Y88Y ~8b +dY "" "" Yb +8 88 88 8 +8 o 8 8 + Yb b8d eb ,b + Yb_____Y ,b + 8 dY + 8 e 8 +`; + +const textAscii = ` +888 | e 888~-_ 888 +888___| e88~~8e Y88b / d8b 888 \\ 888 +888 | d888 88b Y888/ /Y88b 888 | 888 +888 | 8888__888 Y8/ / Y88b 888 / 888 +888 | Y888 , Y /____Y88b 888_-~ 888 +888 | "88___/ / / Y88b 888 888 + _/ +`; + +const asciiToLines = (ascii: string) => { + const lines: Array = []; + let maxLineLength = 0; + let line = ''; + for (const char of ascii) { + if (char === '\n') { + if (line) { + lines.push(line); + maxLineLength = Math.max(maxLineLength, line.length); + line = ''; + } + } else { + line += char; + } + } + return { lines, maxLineLength }; +}; + +export function printCliIntro() { + const packageJson = loadPackageJson(); + const logo = asciiToLines(logoAscii); + const text = asciiToLines(textAscii); + const padding = Math.floor((logo.lines.length - text.lines.length) / 2); + const lines = logo.lines.map((logoLine, index) => { + let line = logoLine.padEnd(logo.maxLineLength); + if (index >= padding && logo.lines.length - index - 1 >= padding) { + line += ` ${text.lines[index - padding]}`; + } + return line; + }); + for (const line of lines) { + console.log(colors.cyan(line)); + } + console.log(''); + console.log(colors.gray(`${packageJson.name} v${packageJson.version}`)); + console.log(''); +} diff --git a/packages/openapi-ts/src/utils/input/__tests__/readme.test.ts b/packages/openapi-ts/src/utils/input/__tests__/readme.test.ts index 0c524737f7..29440027df 100644 --- a/packages/openapi-ts/src/utils/input/__tests__/readme.test.ts +++ b/packages/openapi-ts/src/utils/input/__tests__/readme.test.ts @@ -91,14 +91,22 @@ describe('readme utils', () => { describe('inputToReadmePath', () => { it('should transform simple UUID format to API URL', () => { const result = inputToReadmePath('readme:abc123'); - expect(result).toBe('https://dash.readme.com/api/v1/api-registry/abc123'); + expect(result).toEqual({ + path: 'https://dash.readme.com/api/v1/api-registry/abc123', + registry: 'readme', + uuid: 'abc123', + }); }); it('should transform full format to API URL', () => { const result = inputToReadmePath('readme:@myorg/myproject#uuid123'); - expect(result).toBe( - 'https://dash.readme.com/api/v1/api-registry/uuid123', - ); + expect(result).toEqual({ + organization: 'myorg', + path: 'https://dash.readme.com/api/v1/api-registry/uuid123', + project: 'myproject', + registry: 'readme', + uuid: 'uuid123', + }); }); it('should throw error for invalid inputs', () => { @@ -137,9 +145,13 @@ describe('readme utils', () => { 'should handle $input correctly', ({ expected, input }) => { expect(parseShorthand(input)).toEqual(expected); - expect(inputToReadmePath(`readme:${input}`)).toBe( - `https://dash.readme.com/api/v1/api-registry/${expected.uuid}`, - ); + expect(inputToReadmePath(`readme:${input}`)).toEqual({ + organization: expected.organization, + path: `https://dash.readme.com/api/v1/api-registry/${expected.uuid}`, + project: expected.project, + registry: 'readme', + uuid: expected.uuid, + }); }, ); diff --git a/packages/openapi-ts/src/utils/input/__tests__/scalar.test.ts b/packages/openapi-ts/src/utils/input/__tests__/scalar.test.ts index 80d591073f..da896e144f 100644 --- a/packages/openapi-ts/src/utils/input/__tests__/scalar.test.ts +++ b/packages/openapi-ts/src/utils/input/__tests__/scalar.test.ts @@ -76,9 +76,12 @@ describe('readme utils', () => { describe('inputToScalarPath', () => { it('should transform full format to API URL', () => { const result = inputToScalarPath('scalar:@foo/bar'); - expect(result).toBe( - 'https://registry.scalar.com/@foo/apis/bar/latest?format=json', - ); + expect(result).toEqual({ + organization: '@foo', + path: 'https://registry.scalar.com/@foo/apis/bar/latest?format=json', + project: 'bar', + registry: 'scalar', + }); }); it('should throw error for invalid inputs', () => { @@ -110,9 +113,12 @@ describe('readme utils', () => { 'should handle $input correctly', ({ expected, input }) => { expect(parseShorthand(input)).toEqual(expected); - expect(inputToScalarPath(`scalar:${input}`)).toBe( - `https://registry.scalar.com/${expected.organization}/apis/${expected.project}/latest?format=json`, - ); + expect(inputToScalarPath(`scalar:${input}`)).toEqual({ + organization: expected.organization, + path: `https://registry.scalar.com/${expected.organization}/apis/${expected.project}/latest?format=json`, + project: expected.project, + registry: 'scalar', + }); }, ); diff --git a/packages/openapi-ts/src/utils/input/heyApi.ts b/packages/openapi-ts/src/utils/input/heyApi.ts index 4eae02ff05..08af7efc76 100644 --- a/packages/openapi-ts/src/utils/input/heyApi.ts +++ b/packages/openapi-ts/src/utils/input/heyApi.ts @@ -1,7 +1,6 @@ -// Regular expression to match Hey API Registry input formats: - import type { Input } from '../../types/input'; +// Regular expression to match Hey API Registry input formats: // - {organization}/{project}?{queryParams} const registryRegExp = /^([\w-]+)\/([\w-]+)(?:\?([\w=&.-]*))?$/; @@ -36,7 +35,7 @@ export interface Parsed { * @throws Error if the input format is invalid */ export const parseShorthand = ( - input: Omit & { + input: Input & { path: string; }, ): Parsed => { @@ -82,14 +81,17 @@ export const parseShorthand = ( * @returns The Hey API Registry URL */ export const inputToHeyApiPath = ( - input: Omit & { + input: Input & { path: string; }, -): string => { +): Partial => { const parsed = parseShorthand(input); - return getRegistryUrl( - parsed.organization, - parsed.project, - parsed.queryParams, - ); + return { + path: getRegistryUrl( + parsed.organization, + parsed.project, + parsed.queryParams, + ), + registry: 'hey-api', + }; }; diff --git a/packages/openapi-ts/src/utils/input/index.ts b/packages/openapi-ts/src/utils/input/index.ts index 1da55b0b58..c0b96b5c15 100644 --- a/packages/openapi-ts/src/utils/input/index.ts +++ b/packages/openapi-ts/src/utils/input/index.ts @@ -9,12 +9,12 @@ export const inputToApiRegistry = ( }, ) => { if (input.path.startsWith('readme:')) { - input.path = inputToReadmePath(input.path); + Object.assign(input, inputToReadmePath(input.path)); return; } if (input.path.startsWith('scalar:')) { - input.path = inputToScalarPath(input.path); + Object.assign(input, inputToScalarPath(input.path)); return; } @@ -24,13 +24,13 @@ export const inputToApiRegistry = ( if (input.path.startsWith(heyApiRegistryBaseUrl)) { input.path = input.path.slice(heyApiRegistryBaseUrl.length + 1); - input.path = inputToHeyApiPath(input as Input & { path: string }); + Object.assign(input, inputToHeyApiPath(input as Input & { path: string })); return; } const parts = input.path.split('/'); - const cleanParts = parts.filter(Boolean); - if (parts.length === 2 && cleanParts.length === 2) { - input.path = inputToHeyApiPath(input as Input & { path: string }); + if (parts.length === 2 && parts.filter(Boolean).length === 2) { + Object.assign(input, inputToHeyApiPath(input as Input & { path: string })); + return; } }; diff --git a/packages/openapi-ts/src/utils/input/readme.ts b/packages/openapi-ts/src/utils/input/readme.ts index cbdb8fb956..3ed5a5cdfb 100644 --- a/packages/openapi-ts/src/utils/input/readme.ts +++ b/packages/openapi-ts/src/utils/input/readme.ts @@ -1,3 +1,5 @@ +import type { Input } from '../../types/input'; + // Regular expression to match ReadMe API Registry input formats: // - @{organization}/{project}#{uuid} // - {uuid} @@ -57,8 +59,12 @@ export const parseShorthand = (shorthand: string): Parsed => { * @param input - ReadMe format string * @returns The ReadMe API Registry URL */ -export const inputToReadmePath = (input: string): string => { +export const inputToReadmePath = (input: string): Partial => { const shorthand = input.slice(`${namespace}:`.length); const parsed = parseShorthand(shorthand); - return getRegistryUrl(parsed.uuid); + return { + ...parsed, + path: getRegistryUrl(parsed.uuid), + registry: 'readme', + }; }; diff --git a/packages/openapi-ts/src/utils/input/scalar.ts b/packages/openapi-ts/src/utils/input/scalar.ts index f8d16b38b4..5686ae1144 100644 --- a/packages/openapi-ts/src/utils/input/scalar.ts +++ b/packages/openapi-ts/src/utils/input/scalar.ts @@ -1,3 +1,5 @@ +import type { Input } from '../../types/input'; + // Regular expression to match Scalar API Registry input formats: // - @{organization}/{project} const registryRegExp = /^(@[\w-]+)\/([\w.-]+)$/; @@ -59,8 +61,12 @@ export const parseShorthand = (shorthand: string): Parsed => { * @param input - Scalar format string * @returns The Scalar API Registry URL */ -export const inputToScalarPath = (input: string): string => { +export const inputToScalarPath = (input: string): Partial => { const shorthand = input.slice(`${namespace}:`.length); const parsed = parseShorthand(shorthand); - return getRegistryUrl(parsed.organization, parsed.project); + return { + ...parsed, + path: getRegistryUrl(parsed.organization, parsed.project), + registry: 'scalar', + }; }; diff --git a/packages/openapi-ts/src/utils/logger.ts b/packages/openapi-ts/src/utils/logger.ts index c0cde26497..321271f8f6 100644 --- a/packages/openapi-ts/src/utils/logger.ts +++ b/packages/openapi-ts/src/utils/logger.ts @@ -135,11 +135,11 @@ export class Logger { if (severity?.type === 'percentage') { percentageLabel = severity.color(percentageLabel); } + const jobPrefix = colors.gray('[root] '); console.log( - colors.gray(prefix) + - color( - `${event.name.padEnd(maxLength)} ${durationLabel} (${percentageLabel})`, - ), + `${jobPrefix}${colors.gray(prefix)}${color( + `${event.name.padEnd(maxLength)} ${durationLabel} (${percentageLabel})`, + )}`, ); this.reportEvent({ ...event, indent: indent + 1, measure }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e0ca17d83..0523b2bf08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,8 +92,8 @@ importers: specifier: 8.4.0 version: 8.4.0(jiti@2.5.1)(postcss@8.5.6)(typescript@5.8.3)(yaml@2.8.0) turbo: - specifier: 2.5.6 - version: 2.5.6 + specifier: 2.5.8 + version: 2.5.8 typescript: specifier: 5.8.3 version: 5.8.3 @@ -1265,14 +1265,14 @@ importers: specifier: workspace:^0.2.0 version: link:../codegen-core '@hey-api/json-schema-ref-parser': - specifier: 1.1.0 - version: 1.1.0 + specifier: 1.2.0 + version: 1.2.0 ansi-colors: specifier: 4.1.3 version: 4.1.3 c12: - specifier: 2.0.1 - version: 2.0.1(magicast@0.3.5) + specifier: 3.3.0 + version: 3.3.0(magicast@0.3.5) color-support: specifier: 1.1.3 version: 1.1.3 @@ -1345,7 +1345,7 @@ importers: version: 3.3.2 nuxt: specifier: 3.14.1592 - version: 3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(sass@1.85.0)(terser@5.43.1)(typescript@5.8.3)(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) + version: 3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(sass@1.85.0)(terser@5.43.1)(typescript@5.8.3)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)) ofetch: specifier: 1.4.1 version: 1.4.1 @@ -3953,8 +3953,8 @@ packages: '@fontsource/fira-mono@5.0.0': resolution: {integrity: sha512-IsinH/oLYJyv/sQv7SbKmjoAXZsSjm6Q1Tz5GBBXCXi3Jg9MzXmKvWm9bSLC8lFI6CDsi8GkH/DAgZ98t8bhTQ==} - '@hey-api/json-schema-ref-parser@1.1.0': - resolution: {integrity: sha512-+5eg9pgAAM9oSqJQuUtfTKbLz8yieFKN91myyXiLnprqFj8ROfxUKJLr9DKq/hGKyybKT1WxFSetDqCFm80pCA==} + '@hey-api/json-schema-ref-parser@1.2.0': + resolution: {integrity: sha512-BMnIuhVgNmSudadw1GcTsP18Yk5l8FrYrg/OSYNxz0D2E0vf4D5e4j5nUbuY8MU6p1vp7ev0xrfP6A/NWazkzQ==} engines: {node: '>= 16'} '@humanfs/core@0.19.1': @@ -7452,6 +7452,14 @@ packages: magicast: optional: true + c12@3.3.0: + resolution: {integrity: sha512-K9ZkuyeJQeqLEyqldbYLG3wjqwpw4BVaAqvmxq3GYKK0b1A/yYQdIcJxkzAOWcNVWhJpRXAPfZFueekiY/L8Dw==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -8219,6 +8227,10 @@ packages: resolution: {integrity: sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==} engines: {node: '>=12'} + dotenv@17.2.2: + resolution: {integrity: sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -12751,38 +12763,38 @@ packages: resolution: {integrity: sha512-3T3T04WzowbwV2FDiGXBbr81t64g1MUGGJRgT4x5o97N+8ArdhVCAF9IxFrxuSJmM3E5Asn7nKHkao0ibcZXAg==} engines: {node: ^18.17.0 || >=20.5.0} - turbo-darwin-64@2.5.6: - resolution: {integrity: sha512-3C1xEdo4aFwMJAPvtlPqz1Sw/+cddWIOmsalHFMrsqqydcptwBfu26WW2cDm3u93bUzMbBJ8k3zNKFqxJ9ei2A==} + turbo-darwin-64@2.5.8: + resolution: {integrity: sha512-Dh5bCACiHO8rUXZLpKw+m3FiHtAp2CkanSyJre+SInEvEr5kIxjGvCK/8MFX8SFRjQuhjtvpIvYYZJB4AGCxNQ==} cpu: [x64] os: [darwin] - turbo-darwin-arm64@2.5.6: - resolution: {integrity: sha512-LyiG+rD7JhMfYwLqB6k3LZQtYn8CQQUePbpA8mF/hMLPAekXdJo1g0bUPw8RZLwQXUIU/3BU7tXENvhSGz5DPA==} + turbo-darwin-arm64@2.5.8: + resolution: {integrity: sha512-f1H/tQC9px7+hmXn6Kx/w8Jd/FneIUnvLlcI/7RGHunxfOkKJKvsoiNzySkoHQ8uq1pJnhJ0xNGTlYM48ZaJOQ==} cpu: [arm64] os: [darwin] - turbo-linux-64@2.5.6: - resolution: {integrity: sha512-GOcUTT0xiT/pSnHL4YD6Yr3HreUhU8pUcGqcI2ksIF9b2/r/kRHwGFcsHgpG3+vtZF/kwsP0MV8FTlTObxsYIA==} + turbo-linux-64@2.5.8: + resolution: {integrity: sha512-hMyvc7w7yadBlZBGl/bnR6O+dJTx3XkTeyTTH4zEjERO6ChEs0SrN8jTFj1lueNXKIHh1SnALmy6VctKMGnWfw==} cpu: [x64] os: [linux] - turbo-linux-arm64@2.5.6: - resolution: {integrity: sha512-10Tm15bruJEA3m0V7iZcnQBpObGBcOgUcO+sY7/2vk1bweW34LMhkWi8svjV9iDF68+KJDThnYDlYE/bc7/zzQ==} + turbo-linux-arm64@2.5.8: + resolution: {integrity: sha512-LQELGa7bAqV2f+3rTMRPnj5G/OHAe2U+0N9BwsZvfMvHSUbsQ3bBMWdSQaYNicok7wOZcHjz2TkESn1hYK6xIQ==} cpu: [arm64] os: [linux] - turbo-windows-64@2.5.6: - resolution: {integrity: sha512-FyRsVpgaj76It0ludwZsNN40ytHN+17E4PFJyeliBEbxrGTc5BexlXVpufB7XlAaoaZVxbS6KT8RofLfDRyEPg==} + turbo-windows-64@2.5.8: + resolution: {integrity: sha512-3YdcaW34TrN1AWwqgYL9gUqmZsMT4T7g8Y5Azz+uwwEJW+4sgcJkIi9pYFyU4ZBSjBvkfuPZkGgfStir5BBDJQ==} cpu: [x64] os: [win32] - turbo-windows-arm64@2.5.6: - resolution: {integrity: sha512-j/tWu8cMeQ7HPpKri6jvKtyXg9K1gRyhdK4tKrrchH8GNHscPX/F71zax58yYtLRWTiK04zNzPcUJuoS0+v/+Q==} + turbo-windows-arm64@2.5.8: + resolution: {integrity: sha512-eFC5XzLmgXJfnAK3UMTmVECCwuBcORrWdewoiXBnUm934DY6QN8YowC/srhNnROMpaKaqNeRpoB5FxCww3eteQ==} cpu: [arm64] os: [win32] - turbo@2.5.6: - resolution: {integrity: sha512-gxToHmi9oTBNB05UjUsrWf0OyN5ZXtD0apOarC1KIx232Vp3WimRNy3810QzeNSgyD5rsaIDXlxlbnOzlouo+w==} + turbo@2.5.8: + resolution: {integrity: sha512-5c9Fdsr9qfpT3hA0EyYSFRZj1dVVsb6KIWubA9JBYZ/9ZEAijgUEae0BBR/Xl/wekt4w65/lYLTFaP3JmwSO8w==} hasBin: true type-check@0.4.0: @@ -14110,7 +14122,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.1902.0(chokidar@4.0.3) - '@angular-devkit/build-webpack': 0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.0)))(webpack@5.98.0(esbuild@0.25.0)) + '@angular-devkit/build-webpack': 0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.9)))(webpack@5.98.0(esbuild@0.25.0)) '@angular-devkit/core': 19.2.0(chokidar@4.0.3) '@angular/build': 19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(typescript@5.8.3))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/platform-server@19.2.0(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@19.2.0(@angular/animations@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))))(@angular/ssr@19.2.15(5c03da8199d2fcdf9ff93b70f9349edd))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.5.1)(karma@6.4.4)(less@4.2.2)(postcss@8.5.2)(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.8.3)))(terser@5.39.0)(typescript@5.8.3)(yaml@2.8.0) '@angular/compiler-cli': 19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(typescript@5.8.3) @@ -14162,7 +14174,7 @@ snapshots: typescript: 5.8.3 webpack: 5.98.0(esbuild@0.25.9) webpack-dev-middleware: 7.4.2(webpack@5.98.0(esbuild@0.25.9)) - webpack-dev-server: 5.2.0(webpack@5.98.0(esbuild@0.25.0)) + webpack-dev-server: 5.2.0(webpack@5.98.0(esbuild@0.25.9)) webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(webpack@5.98.0(esbuild@0.25.9)) optionalDependencies: @@ -14198,7 +14210,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.1902.0(chokidar@4.0.3) - '@angular-devkit/build-webpack': 0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.0)))(webpack@5.98.0(esbuild@0.25.0)) + '@angular-devkit/build-webpack': 0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.9)))(webpack@5.98.0(esbuild@0.25.0)) '@angular-devkit/core': 19.2.0(chokidar@4.0.3) '@angular/build': 19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(typescript@5.8.3))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/platform-server@19.2.0(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@19.2.0(@angular/animations@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))))(@angular/ssr@19.2.15(5c03da8199d2fcdf9ff93b70f9349edd))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.5.1)(karma@6.4.4)(less@4.2.2)(postcss@8.5.2)(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.8.3)))(terser@5.39.0)(typescript@5.8.3)(yaml@2.8.0) '@angular/compiler-cli': 19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(typescript@5.8.3) @@ -14250,7 +14262,7 @@ snapshots: typescript: 5.8.3 webpack: 5.98.0(esbuild@0.25.9) webpack-dev-middleware: 7.4.2(webpack@5.98.0(esbuild@0.25.9)) - webpack-dev-server: 5.2.0(webpack@5.98.0(esbuild@0.25.0)) + webpack-dev-server: 5.2.0(webpack@5.98.0(esbuild@0.25.9)) webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(webpack@5.98.0(esbuild@0.25.9)) optionalDependencies: @@ -14457,12 +14469,12 @@ snapshots: - webpack-cli - yaml - '@angular-devkit/build-webpack@0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.0)))(webpack@5.98.0(esbuild@0.25.0))': + '@angular-devkit/build-webpack@0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.9)))(webpack@5.98.0(esbuild@0.25.0))': dependencies: '@angular-devkit/architect': 0.1902.0(chokidar@4.0.3) rxjs: 7.8.1 webpack: 5.98.0(esbuild@0.25.9) - webpack-dev-server: 5.2.0(webpack@5.98.0(esbuild@0.25.0)) + webpack-dev-server: 5.2.0(webpack@5.98.0(esbuild@0.25.9)) transitivePeerDependencies: - chokidar @@ -17242,7 +17254,7 @@ snapshots: '@fontsource/fira-mono@5.0.0': {} - '@hey-api/json-schema-ref-parser@1.1.0': + '@hey-api/json-schema-ref-parser@1.2.0': dependencies: '@jsdevtools/ono': 7.1.3 '@types/json-schema': 7.0.15 @@ -17942,32 +17954,32 @@ snapshots: '@nuxt/devalue@2.0.2': {} - '@nuxt/devtools-kit@1.7.0(magicast@0.3.5)(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))': + '@nuxt/devtools-kit@1.7.0(magicast@0.3.5)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1))': dependencies: '@nuxt/kit': 3.15.4(magicast@0.3.5) '@nuxt/schema': 3.16.2 execa: 7.2.0 - vite: 7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) + vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1) transitivePeerDependencies: - magicast - supports-color - '@nuxt/devtools-kit@1.7.0(magicast@0.3.5)(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))': + '@nuxt/devtools-kit@1.7.0(magicast@0.3.5)(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))': dependencies: '@nuxt/kit': 3.15.4(magicast@0.3.5) '@nuxt/schema': 3.16.2 execa: 7.2.0 - vite: 7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) + vite: 7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) transitivePeerDependencies: - magicast - supports-color - '@nuxt/devtools-kit@1.7.0(magicast@0.3.5)(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))': + '@nuxt/devtools-kit@1.7.0(magicast@0.3.5)(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))': dependencies: '@nuxt/kit': 3.15.4(magicast@0.3.5) '@nuxt/schema': 3.16.2 execa: 7.2.0 - vite: 7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) + vite: 7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) transitivePeerDependencies: - magicast - supports-color @@ -18018,7 +18030,7 @@ snapshots: semver: 7.7.2 simple-git: 3.28.0 sirv: 3.0.1 - tinyglobby: 0.2.10 + tinyglobby: 0.2.14 unimport: 3.14.6(rollup@3.29.5) vite: 7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) vite-plugin-inspect: 0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@3.29.5)(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) @@ -18032,13 +18044,13 @@ snapshots: - utf-8-validate - vue - '@nuxt/devtools@1.7.0(rollup@4.50.0)(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.2))': + '@nuxt/devtools@1.7.0(rollup@4.50.0)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1))(vue@3.5.13(typescript@5.8.3))': dependencies: '@antfu/utils': 0.7.10 - '@nuxt/devtools-kit': 1.7.0(magicast@0.3.5)(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) + '@nuxt/devtools-kit': 1.7.0(magicast@0.3.5)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)) '@nuxt/devtools-wizard': 1.7.0 '@nuxt/kit': 3.15.4(magicast@0.3.5) - '@vue/devtools-core': 7.6.8(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.2)) + '@vue/devtools-core': 7.6.8(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1))(vue@3.5.13(typescript@5.8.3)) '@vue/devtools-kit': 7.6.8 birpc: 0.2.19 consola: 3.4.2 @@ -18065,11 +18077,11 @@ snapshots: semver: 7.7.2 simple-git: 3.28.0 sirv: 3.0.1 - tinyglobby: 0.2.10 + tinyglobby: 0.2.14 unimport: 3.14.6(rollup@4.50.0) - vite: 7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) - vite-plugin-inspect: 0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) - vite-plugin-vue-inspector: 5.3.2(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) + vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1) + vite-plugin-inspect: 0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)) + vite-plugin-vue-inspector: 5.3.2(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)) which: 3.0.1 ws: 8.18.3 transitivePeerDependencies: @@ -18079,13 +18091,13 @@ snapshots: - utf-8-validate - vue - '@nuxt/devtools@1.7.0(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(vue@3.5.13(typescript@5.8.3))': + '@nuxt/devtools@1.7.0(rollup@4.50.0)(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.2))': dependencies: '@antfu/utils': 0.7.10 - '@nuxt/devtools-kit': 1.7.0(magicast@0.3.5)(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) + '@nuxt/devtools-kit': 1.7.0(magicast@0.3.5)(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) '@nuxt/devtools-wizard': 1.7.0 '@nuxt/kit': 3.15.4(magicast@0.3.5) - '@vue/devtools-core': 7.6.8(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(vue@3.5.13(typescript@5.8.3)) + '@vue/devtools-core': 7.6.8(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.2)) '@vue/devtools-kit': 7.6.8 birpc: 0.2.19 consola: 3.4.2 @@ -18112,11 +18124,11 @@ snapshots: semver: 7.7.2 simple-git: 3.28.0 sirv: 3.0.1 - tinyglobby: 0.2.10 + tinyglobby: 0.2.14 unimport: 3.14.6(rollup@4.50.0) - vite: 7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) - vite-plugin-inspect: 0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) - vite-plugin-vue-inspector: 5.3.2(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) + vite: 7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) + vite-plugin-inspect: 0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) + vite-plugin-vue-inspector: 5.3.2(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) which: 3.0.1 ws: 8.18.3 transitivePeerDependencies: @@ -18126,13 +18138,13 @@ snapshots: - utf-8-validate - vue - '@nuxt/devtools@1.7.0(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.8.3))': + '@nuxt/devtools@1.7.0(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(vue@3.5.13(typescript@5.8.3))': dependencies: '@antfu/utils': 0.7.10 - '@nuxt/devtools-kit': 1.7.0(magicast@0.3.5)(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) + '@nuxt/devtools-kit': 1.7.0(magicast@0.3.5)(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) '@nuxt/devtools-wizard': 1.7.0 '@nuxt/kit': 3.15.4(magicast@0.3.5) - '@vue/devtools-core': 7.6.8(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.8.3)) + '@vue/devtools-core': 7.6.8(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(vue@3.5.13(typescript@5.8.3)) '@vue/devtools-kit': 7.6.8 birpc: 0.2.19 consola: 3.4.2 @@ -18159,11 +18171,11 @@ snapshots: semver: 7.7.2 simple-git: 3.28.0 sirv: 3.0.1 - tinyglobby: 0.2.10 + tinyglobby: 0.2.14 unimport: 3.14.6(rollup@4.50.0) - vite: 7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) - vite-plugin-inspect: 0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) - vite-plugin-vue-inspector: 5.3.2(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) + vite: 7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) + vite-plugin-inspect: 0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) + vite-plugin-vue-inspector: 5.3.2(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) which: 3.0.1 ws: 8.18.3 transitivePeerDependencies: @@ -20478,8 +20490,8 @@ snapshots: dependencies: '@mapbox/node-pre-gyp': 2.0.0(encoding@0.1.13) '@rollup/pluginutils': 5.2.0(rollup@4.50.0) - acorn: 8.14.0 - acorn-import-attributes: 1.9.5(acorn@8.14.0) + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) async-sema: 3.1.1 bindings: 1.5.0 estree-walker: 2.0.2 @@ -20863,19 +20875,19 @@ snapshots: dependencies: '@vue/devtools-kit': 8.0.2 - '@vue/devtools-core@7.6.8(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.8.3))': + '@vue/devtools-core@7.6.8(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1))(vue@3.5.13(typescript@5.8.3))': dependencies: '@vue/devtools-kit': 7.7.7 '@vue/devtools-shared': 7.7.7 mitt: 3.0.1 nanoid: 5.1.5 pathe: 1.1.2 - vite-hot-client: 0.2.4(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) + vite-hot-client: 0.2.4(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)) vue: 3.5.13(typescript@5.8.3) transitivePeerDependencies: - vite - '@vue/devtools-core@7.6.8(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.2))': + '@vue/devtools-core@7.6.8(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.8.3))': dependencies: '@vue/devtools-kit': 7.7.7 '@vue/devtools-shared': 7.7.7 @@ -20883,30 +20895,30 @@ snapshots: nanoid: 5.1.5 pathe: 1.1.2 vite-hot-client: 0.2.4(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) - vue: 3.5.13(typescript@5.9.2) + vue: 3.5.13(typescript@5.8.3) transitivePeerDependencies: - vite - '@vue/devtools-core@7.6.8(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(vue@3.5.13(typescript@5.8.3))': + '@vue/devtools-core@7.6.8(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.2))': dependencies: '@vue/devtools-kit': 7.7.7 '@vue/devtools-shared': 7.7.7 mitt: 3.0.1 nanoid: 5.1.5 pathe: 1.1.2 - vite-hot-client: 0.2.4(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) - vue: 3.5.13(typescript@5.8.3) + vite-hot-client: 0.2.4(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) + vue: 3.5.13(typescript@5.9.2) transitivePeerDependencies: - vite - '@vue/devtools-core@7.6.8(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.8.3))': + '@vue/devtools-core@7.6.8(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(vue@3.5.13(typescript@5.8.3))': dependencies: '@vue/devtools-kit': 7.7.7 '@vue/devtools-shared': 7.7.7 mitt: 3.0.1 nanoid: 5.1.5 pathe: 1.1.2 - vite-hot-client: 0.2.4(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) + vite-hot-client: 0.2.4(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) vue: 3.5.13(typescript@5.8.3) transitivePeerDependencies: - vite @@ -21223,9 +21235,9 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 - acorn-import-attributes@1.9.5(acorn@8.14.0): + acorn-import-attributes@1.9.5(acorn@8.15.0): dependencies: - acorn: 8.14.0 + acorn: 8.15.0 acorn-jsx@5.3.2(acorn@7.4.1): dependencies: @@ -21761,6 +21773,23 @@ snapshots: optionalDependencies: magicast: 0.3.5 + c12@3.3.0(magicast@0.3.5): + dependencies: + chokidar: 4.0.3 + confbox: 0.2.2 + defu: 6.1.4 + dotenv: 17.2.2 + exsolve: 1.0.7 + giget: 2.0.0 + jiti: 2.5.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 2.0.0 + pkg-types: 2.3.0 + rc9: 2.1.2 + optionalDependencies: + magicast: 0.3.5 + cac@6.7.14: {} cacache@19.0.1: @@ -22493,6 +22522,8 @@ snapshots: dotenv@17.2.1: {} + dotenv@17.2.2: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -22930,8 +22961,8 @@ snapshots: '@typescript-eslint/parser': 8.29.1(eslint@9.17.0(jiti@2.5.1))(typescript@5.8.3) eslint: 9.17.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.17.0(jiti@2.5.1)))(eslint@9.17.0(jiti@2.5.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.5.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.17.0(jiti@2.5.1)))(eslint@9.17.0(jiti@2.5.1)))(eslint@9.17.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.17.0(jiti@2.5.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.5.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.17.0(jiti@2.5.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.17.0(jiti@2.5.1)) eslint-plugin-react: 7.37.5(eslint@9.17.0(jiti@2.5.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.17.0(jiti@2.5.1)) @@ -22954,7 +22985,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.17.0(jiti@2.5.1)))(eslint@9.17.0(jiti@2.5.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.17.0(jiti@2.5.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 @@ -22965,22 +22996,22 @@ snapshots: tinyglobby: 0.2.14 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.5.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.17.0(jiti@2.5.1)))(eslint@9.17.0(jiti@2.5.1)))(eslint@9.17.0(jiti@2.5.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.5.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.17.0(jiti@2.5.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.5.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.17.0(jiti@2.5.1)))(eslint@9.17.0(jiti@2.5.1)))(eslint@9.17.0(jiti@2.5.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.5.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.17.0(jiti@2.5.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.29.1(eslint@9.17.0(jiti@2.5.1))(typescript@5.8.3) eslint: 9.17.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.17.0(jiti@2.5.1)))(eslint@9.17.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.17.0(jiti@2.5.1)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.5.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.17.0(jiti@2.5.1)))(eslint@9.17.0(jiti@2.5.1)))(eslint@9.17.0(jiti@2.5.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.5.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.17.0(jiti@2.5.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -22991,7 +23022,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.17.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.5.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.17.0(jiti@2.5.1)))(eslint@9.17.0(jiti@2.5.1)))(eslint@9.17.0(jiti@2.5.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.5.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.17.0(jiti@2.5.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -25500,7 +25531,7 @@ snapshots: '@rollup/plugin-terser': 0.4.4(rollup@4.50.0) '@vercel/nft': 0.29.4(encoding@0.1.13)(rollup@4.50.0) archiver: 7.0.1 - c12: 3.2.0(magicast@0.3.5) + c12: 3.3.0(magicast@0.3.5) chokidar: 4.0.3 citty: 0.1.6 compatx: 0.2.0 @@ -25984,10 +26015,10 @@ snapshots: - vue-tsc - xml2js - nuxt@3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(sass@1.85.0)(terser@5.43.1)(typescript@5.8.3)(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): + nuxt@3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(sass@1.85.0)(terser@5.43.1)(typescript@5.8.3)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)): dependencies: '@nuxt/devalue': 2.0.2 - '@nuxt/devtools': 1.7.0(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.8.3)) + '@nuxt/devtools': 1.7.0(rollup@4.50.0)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1))(vue@3.5.13(typescript@5.8.3)) '@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.50.0) '@nuxt/schema': 3.14.1592(magicast@0.3.5)(rollup@4.50.0) '@nuxt/telemetry': 2.6.6(magicast@0.3.5) @@ -28313,7 +28344,7 @@ snapshots: term-size@2.2.1: {} - terser-webpack-plugin@5.3.14(esbuild@0.25.4)(webpack@5.98.0(esbuild@0.25.9)): + terser-webpack-plugin@5.3.14(esbuild@0.25.0)(webpack@5.98.0(esbuild@0.25.9)): dependencies: '@jridgewell/trace-mapping': 0.3.30 jest-worker: 27.5.1 @@ -28322,9 +28353,9 @@ snapshots: terser: 5.43.1 webpack: 5.98.0(esbuild@0.25.9) optionalDependencies: - esbuild: 0.25.4 + esbuild: 0.25.0 - terser-webpack-plugin@5.3.14(esbuild@0.25.9)(webpack@5.98.0(esbuild@0.25.0)): + terser-webpack-plugin@5.3.14(esbuild@0.25.4)(webpack@5.98.0(esbuild@0.25.9)): dependencies: '@jridgewell/trace-mapping': 0.3.30 jest-worker: 27.5.1 @@ -28333,7 +28364,7 @@ snapshots: terser: 5.43.1 webpack: 5.98.0(esbuild@0.25.9) optionalDependencies: - esbuild: 0.25.9 + esbuild: 0.25.4 terser@5.39.0: dependencies: @@ -28546,32 +28577,32 @@ snapshots: transitivePeerDependencies: - supports-color - turbo-darwin-64@2.5.6: + turbo-darwin-64@2.5.8: optional: true - turbo-darwin-arm64@2.5.6: + turbo-darwin-arm64@2.5.8: optional: true - turbo-linux-64@2.5.6: + turbo-linux-64@2.5.8: optional: true - turbo-linux-arm64@2.5.6: + turbo-linux-arm64@2.5.8: optional: true - turbo-windows-64@2.5.6: + turbo-windows-64@2.5.8: optional: true - turbo-windows-arm64@2.5.6: + turbo-windows-arm64@2.5.8: optional: true - turbo@2.5.6: + turbo@2.5.8: optionalDependencies: - turbo-darwin-64: 2.5.6 - turbo-darwin-arm64: 2.5.6 - turbo-linux-64: 2.5.6 - turbo-linux-arm64: 2.5.6 - turbo-windows-64: 2.5.6 - turbo-windows-arm64: 2.5.6 + turbo-darwin-64: 2.5.8 + turbo-darwin-arm64: 2.5.8 + turbo-linux-64: 2.5.8 + turbo-linux-arm64: 2.5.8 + turbo-windows-64: 2.5.8 + turbo-windows-arm64: 2.5.8 type-check@0.4.0: dependencies: @@ -28756,7 +28787,7 @@ snapshots: unimport@3.14.6(rollup@3.29.5): dependencies: '@rollup/pluginutils': 5.2.0(rollup@3.29.5) - acorn: 8.14.0 + acorn: 8.15.0 escape-string-regexp: 5.0.0 estree-walker: 3.0.3 fast-glob: 3.3.3 @@ -28775,7 +28806,7 @@ snapshots: unimport@3.14.6(rollup@4.50.0): dependencies: '@rollup/pluginutils': 5.2.0(rollup@4.50.0) - acorn: 8.14.0 + acorn: 8.15.0 escape-string-regexp: 5.0.0 estree-walker: 3.0.3 fast-glob: 3.3.3 @@ -28952,12 +28983,12 @@ snapshots: unplugin@1.16.1: dependencies: - acorn: 8.14.0 + acorn: 8.15.0 webpack-virtual-modules: 0.6.2 unplugin@2.0.0-beta.1: dependencies: - acorn: 8.14.0 + acorn: 8.15.0 webpack-virtual-modules: 0.6.2 unplugin@2.3.10: @@ -29123,6 +29154,10 @@ snapshots: vite: 7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) vite-hot-client: 2.1.0(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) + vite-hot-client@0.2.4(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)): + dependencies: + vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1) + vite-hot-client@0.2.4(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): dependencies: vite: 7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) @@ -29131,10 +29166,6 @@ snapshots: dependencies: vite: 7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) - vite-hot-client@0.2.4(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): - dependencies: - vite: 7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) - vite-hot-client@2.1.0(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): dependencies: vite: 7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) @@ -29301,7 +29332,7 @@ snapshots: - rollup - supports-color - vite-plugin-inspect@0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): + vite-plugin-inspect@0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.2.0(rollup@4.50.0) @@ -29312,14 +29343,14 @@ snapshots: perfect-debounce: 1.0.0 picocolors: 1.1.1 sirv: 3.0.1 - vite: 7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) + vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1) optionalDependencies: '@nuxt/kit': 3.15.4(magicast@0.3.5) transitivePeerDependencies: - rollup - supports-color - vite-plugin-inspect@0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)): + vite-plugin-inspect@0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.2.0(rollup@4.50.0) @@ -29330,14 +29361,14 @@ snapshots: perfect-debounce: 1.0.0 picocolors: 1.1.1 sirv: 3.0.1 - vite: 7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) + vite: 7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) optionalDependencies: '@nuxt/kit': 3.15.4(magicast@0.3.5) transitivePeerDependencies: - rollup - supports-color - vite-plugin-inspect@0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): + vite-plugin-inspect@0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.2.0(rollup@4.50.0) @@ -29348,7 +29379,7 @@ snapshots: perfect-debounce: 1.0.0 picocolors: 1.1.1 sirv: 3.0.1 - vite: 7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) + vite: 7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) optionalDependencies: '@nuxt/kit': 3.15.4(magicast@0.3.5) transitivePeerDependencies: @@ -29385,7 +29416,7 @@ snapshots: - supports-color - vue - vite-plugin-vue-inspector@5.3.2(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): + vite-plugin-vue-inspector@5.3.2(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)): dependencies: '@babel/core': 7.28.3 '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.3) @@ -29396,11 +29427,11 @@ snapshots: '@vue/compiler-dom': 3.5.21 kolorist: 1.8.0 magic-string: 0.30.18 - vite: 7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) + vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1) transitivePeerDependencies: - supports-color - vite-plugin-vue-inspector@5.3.2(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)): + vite-plugin-vue-inspector@5.3.2(vite@7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): dependencies: '@babel/core': 7.28.3 '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.3) @@ -29411,11 +29442,11 @@ snapshots: '@vue/compiler-dom': 3.5.21 kolorist: 1.8.0 magic-string: 0.30.18 - vite: 7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) + vite: 7.1.2(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) transitivePeerDependencies: - supports-color - vite-plugin-vue-inspector@5.3.2(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): + vite-plugin-vue-inspector@5.3.2(vite@7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)): dependencies: '@babel/core': 7.28.3 '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.3) @@ -29426,7 +29457,7 @@ snapshots: '@vue/compiler-dom': 3.5.21 kolorist: 1.8.0 magic-string: 0.30.18 - vite: 7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) + vite: 7.1.5(@types/node@22.10.5)(jiti@2.5.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) transitivePeerDependencies: - supports-color @@ -29894,7 +29925,7 @@ snapshots: optionalDependencies: webpack: 5.98.0(esbuild@0.25.9) - webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.0)): + webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.9)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -30036,7 +30067,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.2 tapable: 2.2.3 - terser-webpack-plugin: 5.3.14(esbuild@0.25.9)(webpack@5.98.0(esbuild@0.25.0)) + terser-webpack-plugin: 5.3.14(esbuild@0.25.0)(webpack@5.98.0(esbuild@0.25.9)) watchpack: 2.4.4 webpack-sources: 3.3.3 transitivePeerDependencies: