diff --git a/.changeset/bright-falcons-chew.md b/.changeset/bright-falcons-chew.md new file mode 100644 index 000000000..1c3e9a490 --- /dev/null +++ b/.changeset/bright-falcons-chew.md @@ -0,0 +1,5 @@ +--- +'@hey-api/types': patch +--- + +feat: add `ToArray`, `ToReadonlyArray`, and `AnyObject` types diff --git a/.changeset/clever-toys-dress.md b/.changeset/clever-toys-dress.md new file mode 100644 index 000000000..ad10ced8c --- /dev/null +++ b/.changeset/clever-toys-dress.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +**cli**: clean up interface diff --git a/.changeset/fluffy-pens-admire.md b/.changeset/fluffy-pens-admire.md new file mode 100644 index 000000000..8f94228b9 --- /dev/null +++ b/.changeset/fluffy-pens-admire.md @@ -0,0 +1,5 @@ +--- +'@hey-api/codegen-core': patch +--- + +**config**: export `loadConfigFile` function (moved from `@hey-api/openapi-ts`) diff --git a/.changeset/stupid-news-wash.md b/.changeset/stupid-news-wash.md new file mode 100644 index 000000000..0e89c1920 --- /dev/null +++ b/.changeset/stupid-news-wash.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +**config**: move `loadConfigFile` function to `@hey-api/codegen-core` diff --git a/.vscode/launch.json b/.vscode/launch.json index aa4048fe8..eaea5c38c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,6 +16,7 @@ "cwd": "${workspaceFolder}/dev", "runtimeExecutable": "node", "program": "${workspaceFolder}/packages/openapi-ts/dist/run.mjs", + "args": [], "env": { "DEBUG": "false" } @@ -28,6 +29,7 @@ "cwd": "${workspaceFolder}/dev", "runtimeExecutable": "node", "program": "${workspaceFolder}/packages/openapi-python/dist/run.mjs", + "args": [], "env": { "DEBUG": "false" } diff --git a/packages/codegen-core/package.json b/packages/codegen-core/package.json index 92bed14ca..253a45b9f 100644 --- a/packages/codegen-core/package.json +++ b/packages/codegen-core/package.json @@ -63,6 +63,7 @@ "dependencies": { "@hey-api/types": "workspace:*", "ansi-colors": "4.1.3", + "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { diff --git a/packages/codegen-core/src/__tests__/exports.test.ts b/packages/codegen-core/src/__tests__/exports.test.ts index ca2c3cc5a..e6e2d7067 100644 --- a/packages/codegen-core/src/__tests__/exports.test.ts +++ b/packages/codegen-core/src/__tests__/exports.test.ts @@ -8,13 +8,16 @@ const constExports = [ 'File', 'fromRef', 'fromRefs', + 'detectInteractiveSession', 'isNode', 'isNodeRef', 'isRef', 'isSymbol', 'isSymbolRef', + 'loadConfigFile', 'log', 'Logger', + 'mergeConfigs', 'nodeBrand', 'Project', 'ref', diff --git a/packages/codegen-core/src/config/interactive.ts b/packages/codegen-core/src/config/interactive.ts new file mode 100644 index 000000000..baf0570b0 --- /dev/null +++ b/packages/codegen-core/src/config/interactive.ts @@ -0,0 +1,14 @@ +/** + * 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. + * @internal + */ +export function detectInteractiveSession(): boolean { + return Boolean( + process.stdin.isTTY && + process.stdout.isTTY && + !process.env.CI && + !process.env.NO_INTERACTIVE && + !process.env.NO_INTERACTION, + ); +} diff --git a/packages/codegen-core/src/config/load.ts b/packages/codegen-core/src/config/load.ts new file mode 100644 index 000000000..00fb5b4cd --- /dev/null +++ b/packages/codegen-core/src/config/load.ts @@ -0,0 +1,42 @@ +import type { Logger } from '@hey-api/codegen-core'; +import type { AnyObject, MaybeArray } from '@hey-api/types'; + +import { mergeConfigs } from './merge'; + +export async function loadConfigFile({ + configFile, + logger, + name, + userConfig, +}: { + configFile: string | undefined; + logger: Logger; + name: string; + userConfig: T; +}): Promise<{ + configFile: string | undefined; + configs: ReadonlyArray; + foundConfig: boolean; +}> { + const eventC12 = logger.timeEvent('c12'); + // c12 is ESM-only since v3 + const { loadConfig } = await import('c12'); + + const { config: fileConfig, configFile: loadedConfigFile } = await loadConfig< + MaybeArray + >({ + configFile, + name, + }); + eventC12.timeEnd(); + + const fileConfigs = fileConfig instanceof Array ? fileConfig : [fileConfig]; + const mergedConfigs = fileConfigs.map((config) => + mergeConfigs(config, userConfig), + ); + const foundConfig = fileConfigs.some( + (config) => Object.keys(config).length > 0, + ); + + return { configFile: loadedConfigFile, configs: mergedConfigs, foundConfig }; +} diff --git a/packages/codegen-core/src/config/merge.ts b/packages/codegen-core/src/config/merge.ts new file mode 100644 index 000000000..2dbff0bc2 --- /dev/null +++ b/packages/codegen-core/src/config/merge.ts @@ -0,0 +1,28 @@ +import type { AnyObject } from '@hey-api/types'; + +function isPlainObject(value: unknown): value is AnyObject { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +export function mergeConfigs( + configA: T | undefined, + configB: T | undefined, +): T { + const a = (configA || {}) as AnyObject; + const b = (configB || {}) as AnyObject; + + const result: AnyObject = { ...a }; + + for (const key of Object.keys(b)) { + const valueA = a[key]; + const valueB = b[key]; + + if (isPlainObject(valueA) && isPlainObject(valueB)) { + result[key] = mergeConfigs(valueA, valueB); + } else { + result[key] = valueB; + } + } + + return result as T; +} diff --git a/packages/codegen-core/src/index.ts b/packages/codegen-core/src/index.ts index 924115f4a..87b1eaa4d 100644 --- a/packages/codegen-core/src/index.ts +++ b/packages/codegen-core/src/index.ts @@ -5,6 +5,9 @@ export type { ImportModule, } from './bindings'; export { nodeBrand, symbolBrand } from './brands'; +export { detectInteractiveSession } from './config/interactive'; +export { loadConfigFile } from './config/load'; +export { mergeConfigs } from './config/merge'; export type { IProjectRenderMeta as ProjectRenderMeta, ISymbolMeta as SymbolMeta, diff --git a/packages/openapi-python/package.json b/packages/openapi-python/package.json index df1af4f19..6dd1bf09d 100644 --- a/packages/openapi-python/package.json +++ b/packages/openapi-python/package.json @@ -77,7 +77,6 @@ "@hey-api/json-schema-ref-parser": "1.2.2", "@hey-api/types": "workspace:*", "ansi-colors": "4.1.3", - "c12": "3.3.3", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", diff --git a/packages/openapi-python/src/cli.ts b/packages/openapi-python/src/cli.ts deleted file mode 100644 index 675e2e9e3..000000000 --- a/packages/openapi-python/src/cli.ts +++ /dev/null @@ -1,120 +0,0 @@ -import type { OptionValues } from 'commander'; -import { Command } from 'commander'; - -import { createClient } from '~/index'; - -import pkg from '../package.json' assert { type: 'json' }; - -const stringToBoolean = ( - value: string | undefined, -): boolean | string | undefined => { - if (value === 'true') return true; - if (value === 'false') return false; - return value; -}; - -const processParams = ( - obj: OptionValues, - booleanKeys: ReadonlyArray, -): OptionValues => { - for (const key of booleanKeys) { - const value = obj[key]; - if (typeof value === 'string') { - const parsedValue = stringToBoolean(value); - delete obj[key]; - obj[key] = parsedValue; - } - } - return obj; -}; - -export const runCli = async (): Promise => { - const params = new Command() - .name(Object.keys(pkg.bin)[0]!) - .usage('[options]') - .version(pkg.version) - .option('-c, --client ', 'HTTP client to generate') - .option('-d, --debug', 'Set log level to debug') - .option('--dry-run [value]', 'Skip writing files to disk?') - .option('-f, --file [value]', 'Path to the config file') - .option( - '-i, --input ', - 'OpenAPI specification (path, url, or string content)', - ) - .option('-l, --logs [value]', 'Logs folder') - .option('-o, --output ', 'Output folder') - .option('-p, --plugins [value...]', "List of plugins you'd like to use") - .option('-s, --silent', 'Set log level to silent') - .option( - '--no-log-file', - 'Disable writing a log file. Works like --silent but without suppressing console output', - ) - .option( - '-w, --watch [value]', - 'Regenerate the client when the input file changes?', - ) - .parse(process.argv) - .opts(); - - let userConfig: Record; - - try { - userConfig = processParams(params, ['dryRun', 'logFile']); - - if (userConfig.file) { - userConfig.configFile = userConfig.file; - delete userConfig.file; - } - - if (params.plugins === true) { - userConfig.plugins = []; - } else if (params.plugins) { - userConfig.plugins = params.plugins; - } else if (userConfig.client) { - userConfig.plugins = ['@hey-api/sdk']; - } - - if (userConfig.client) { - (userConfig.plugins as Array).push(userConfig.client as string); - delete userConfig.client; - } - - userConfig.logs = userConfig.logs - ? { - path: userConfig.logs, - } - : {}; - - if (userConfig.debug) { - (userConfig.logs as Record).level = 'debug'; - delete userConfig.debug; - } else if (userConfig.silent) { - (userConfig.logs as Record).level = 'silent'; - delete userConfig.silent; - } - - (userConfig.logs as Record).file = userConfig.logFile; - delete userConfig.logFile; - - if (typeof params.watch === 'string') { - userConfig.watch = Number.parseInt(params.watch, 10); - } - - if (!Object.keys(userConfig.logs as Record).length) { - delete userConfig.logs; - } - - const context = await createClient( - userConfig as unknown as Required>[0], - ); - if ( - !context[0]?.config.input.some( - (input) => input.watch && input.watch.enabled, - ) - ) { - process.exit(0); - } - } catch { - process.exit(1); - } -}; diff --git a/packages/openapi-python/src/cli/adapter.ts b/packages/openapi-python/src/cli/adapter.ts new file mode 100644 index 000000000..64666ce28 --- /dev/null +++ b/packages/openapi-python/src/cli/adapter.ts @@ -0,0 +1,40 @@ +// import type { ToArray } from '@hey-api/types'; + +import type { UserConfig } from '~/config/types'; + +import type { CliOptions } from './schema'; + +export const cliToConfig = (cli: CliOptions): Partial => { + const config: Partial = {}; + + if (cli.input) config.input = cli.input; + if (cli.output) config.output = cli.output; + if (cli.file) config.configFile = cli.file; + if (cli.dryRun !== undefined) config.dryRun = cli.dryRun; + + // const plugins: ToArray = []; + // if (cli.plugins instanceof Array && cli.plugins.length > 0) { + // plugins.push(...cli.plugins); + // } + // if (cli.client) plugins.push(cli.client); + // if (plugins.length > 0) config.plugins = plugins; + + if (cli.debug || cli.silent || cli.logs || cli.logFile !== undefined) { + config.logs = { + ...(cli.logs && { path: cli.logs }), + ...(cli.debug && { level: 'debug' as const }), + ...(cli.silent && { level: 'silent' as const }), + ...(cli.logFile !== undefined && { file: cli.logFile }), + }; + } + + if (cli.watch !== undefined) { + if (typeof cli.watch === 'string') { + config.watch = Number.parseInt(cli.watch, 10); + } else { + config.watch = cli.watch; + } + } + + return config; +}; diff --git a/packages/openapi-python/src/cli/index.ts b/packages/openapi-python/src/cli/index.ts new file mode 100644 index 000000000..d900f7bf9 --- /dev/null +++ b/packages/openapi-python/src/cli/index.ts @@ -0,0 +1,67 @@ +import { Command, CommanderError } from 'commander'; + +import { createClient } from '~/index'; + +import pkg from '../../package.json' assert { type: 'json' }; +import { cliToConfig } from './adapter'; + +const binName = Object.keys(pkg.bin)[0]!; + +const program = new Command() + .name(binName) + .description('Generate Python code from OpenAPI specifications') + .version(pkg.version); + +program + .option( + '-i, --input ', + 'OpenAPI specification (path, URL, or string)', + ) + .option('-o, --output ', 'Output folder(s)') + .option('-c, --client ', 'HTTP client to generate') + .option('-p, --plugins [names...]', 'Plugins to use') + .option('-f, --file ', 'Path to config file') + .option('-d, --debug', 'Enable debug logging') + .option('-s, --silent', 'Suppress all output') + .option('-l, --logs ', 'Logs folder path') + .option('--no-log-file', 'Disable log file output') + .option('--dry-run', 'Skip writing files') + .option('-w, --watch [interval]', 'Watch for changes') + .action(async (options) => { + const config = cliToConfig(options); + + const context = await createClient( + config as Parameters[0], + ); + + const hasActiveWatch = context[0]?.config.input.some( + (input) => input.watch?.enabled, + ); + + if (!hasActiveWatch) { + process.exit(0); + } + }); + +export async function runCli(): Promise { + try { + await program.parseAsync(process.argv); + } catch (error) { + if (error instanceof CommanderError && 'code' in error) { + if (error.code === 'commander.optionMissingArgument') { + console.error( + `\nMissing required argument. Run '${binName} --help' for usage.\n`, + ); + } else if (error.code === 'commander.unknownOption') { + console.error( + `\nUnknown option. Run '${binName} --help' for available options.\n`, + ); + } + + process.exit(error.exitCode); + } + + console.error('Unexpected error:', error); + process.exit(1); + } +} diff --git a/packages/openapi-python/src/cli/schema.ts b/packages/openapi-python/src/cli/schema.ts new file mode 100644 index 000000000..f49160f37 --- /dev/null +++ b/packages/openapi-python/src/cli/schema.ts @@ -0,0 +1,17 @@ +import type { MaybeArray } from '@hey-api/types'; + +// import type { PluginClientNames, PluginNames } from "~/plugins/types"; + +export interface CliOptions { + // client?: PluginClientNames; + debug?: boolean; + dryRun?: boolean; + file?: string; + input?: MaybeArray; + logFile?: boolean; + logs?: string; + output?: MaybeArray; + // plugins?: ReadonlyArray; + silent?: boolean; + watch?: boolean | string; +} diff --git a/packages/openapi-python/src/config/types.d.ts b/packages/openapi-python/src/config/types.d.ts index 6692ab0b2..220745b6b 100644 --- a/packages/openapi-python/src/config/types.d.ts +++ b/packages/openapi-python/src/config/types.d.ts @@ -1,4 +1,4 @@ -// import { MaybeArray } from "@hey-api/types"; +import type { MaybeArray } from '@hey-api/types'; export interface UserConfig { /** @@ -28,6 +28,7 @@ export interface UserConfig { * generate multiple outputs, one for each input. */ // input: MaybeArray['path']>; + input: MaybeArray; /** * Show an interactive error reporting tool when the program crashes? You * generally want to keep this disabled (default). @@ -41,6 +42,11 @@ export interface UserConfig { * @default process.cwd() */ // logs?: string | Logs; + logs?: + | string + | { + level?: 'debug' | 'info' | 'warn' | 'error' | 'silent'; + }; /** * Path to the output folder. * @@ -48,6 +54,7 @@ export interface UserConfig { * generate multiple outputs, one for each input. */ // output: MaybeArray; + output: MaybeArray; /** * Customize how the input is parsed and transformed before it's passed to * plugins. @@ -68,4 +75,8 @@ export interface UserConfig { // }; // }[PluginNames] // >; + /** + * @deprecated use `input.watch` instead + */ + watch?: boolean | number; } diff --git a/packages/openapi-python/src/index.ts b/packages/openapi-python/src/index.ts index f2bc208d1..e7b9c61fc 100644 --- a/packages/openapi-python/src/index.ts +++ b/packages/openapi-python/src/index.ts @@ -79,11 +79,13 @@ colors.enabled = colorSupport().hasBasic; export { createClient } from '~/generate'; /** - * Type helper for openapi-ts.config.ts, returns {@link MaybeArray} object(s) + * Type helper for configuration object, returns {@link MaybeArray} object(s) */ -export const defineConfig = async >( +export async function defineConfig>( config: LazyOrAsync, -): Promise => (typeof config === 'function' ? await config() : config); +): Promise { + return typeof config === 'function' ? await config() : config; +} export { Logger } from '@hey-api/codegen-core'; // export { defaultPaginationKeywords } from '~/config/parser'; diff --git a/packages/openapi-ts-tests/main/test/cli.test.ts b/packages/openapi-ts-tests/main/test/cli.test.ts index 9bb58351e..5143df63e 100755 --- a/packages/openapi-ts-tests/main/test/cli.test.ts +++ b/packages/openapi-ts-tests/main/test/cli.test.ts @@ -15,7 +15,6 @@ describe('bin', () => { '--output', path.resolve(__dirname, '.gen'), '--dry-run', - 'true', ]); expect(result.error).toBeFalsy(); expect(result.status).toBe(0); diff --git a/packages/openapi-ts/package.json b/packages/openapi-ts/package.json index 840e8dfbf..fbf5a8227 100644 --- a/packages/openapi-ts/package.json +++ b/packages/openapi-ts/package.json @@ -93,7 +93,6 @@ "@hey-api/json-schema-ref-parser": "1.2.2", "@hey-api/types": "workspace:*", "ansi-colors": "4.1.3", - "c12": "3.3.3", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", diff --git a/packages/openapi-ts/src/__tests__/cli.test.ts b/packages/openapi-ts/src/__tests__/cli.test.ts index e5db904ee..29fb83a2c 100755 --- a/packages/openapi-ts/src/__tests__/cli.test.ts +++ b/packages/openapi-ts/src/__tests__/cli.test.ts @@ -53,11 +53,11 @@ describe('cli', () => { process.argv = originalArgv; } expect(spy).toHaveBeenCalledWith({ - input: 'foo.json', + input: ['foo.json'], logs: { file: true, }, - output: 'bar', + output: ['bar'], }); }); @@ -77,7 +77,6 @@ describe('cli', () => { logs: { file: true, }, - plugins: [], }); }); @@ -102,7 +101,7 @@ describe('cli', () => { }); }); - it('with default plugins', async () => { + it('with client plugin', async () => { const originalArgv = process.argv.slice(); try { process.argv = [ @@ -119,7 +118,7 @@ describe('cli', () => { logs: { file: true, }, - plugins: ['@hey-api/typescript', '@hey-api/sdk', 'foo'], + plugins: ['foo'], }); }); @@ -214,12 +213,12 @@ describe('cli', () => { expect(spy).toHaveBeenCalledWith({ configFile: 'bar', dryRun: true, - input: 'baz', + input: ['baz'], logs: { file: true, path: 'qux', }, - output: 'quux', + output: ['quux'], plugins: ['foo'], watch: true, }); diff --git a/packages/openapi-ts/src/__tests__/interactive.test.ts b/packages/openapi-ts/src/__tests__/interactive.test.ts index 766ca3e6a..3f525cf7b 100644 --- a/packages/openapi-ts/src/__tests__/interactive.test.ts +++ b/packages/openapi-ts/src/__tests__/interactive.test.ts @@ -1,12 +1,15 @@ -import { Logger } from '@hey-api/codegen-core'; +import { + detectInteractiveSession, + Logger, + mergeConfigs, +} from '@hey-api/codegen-core'; import { afterEach, describe, expect, it } from 'vitest'; -import { detectInteractiveSession, initConfigs } from '~/config/init'; -import { mergeConfigs } from '~/config/merge'; +import { resolveJobs } from '~/config/init'; describe('interactive config', () => { it('should use detectInteractiveSession when not provided', async () => { - const result = await initConfigs({ + const result = await resolveJobs({ logger: new Logger(), userConfigs: [ { @@ -17,11 +20,11 @@ describe('interactive config', () => { }); // In test environment, TTY is typically not available, so it should be false - expect(result.results[0]?.config.interactive).toBe(false); + expect(result.jobs[0]?.config.interactive).toBe(false); }); it('should respect user config when set to true', async () => { - const result = await initConfigs({ + const result = await resolveJobs({ logger: new Logger(), userConfigs: [ { @@ -32,11 +35,11 @@ describe('interactive config', () => { ], }); - expect(result.results[0]?.config.interactive).toBe(true); + expect(result.jobs[0]?.config.interactive).toBe(true); }); it('should respect user config when set to false', async () => { - const result = await initConfigs({ + const result = await resolveJobs({ logger: new Logger(), userConfigs: [ { @@ -47,7 +50,7 @@ describe('interactive config', () => { ], }); - expect(result.results[0]?.config.interactive).toBe(false); + expect(result.jobs[0]?.config.interactive).toBe(false); }); it('should allow file config to set interactive when CLI does not provide it', () => { @@ -76,7 +79,10 @@ describe('interactive config', () => { }; // After fix: file config's interactive should be preserved - const mergedCorrect = mergeConfigs(fileConfig, cliConfigWithoutInteractive); + const mergedCorrect = mergeConfigs>( + fileConfig, + cliConfigWithoutInteractive, + ); expect(mergedCorrect.interactive).toBe(false); // Before fix: CLI's auto-detected interactive would override file config diff --git a/packages/openapi-ts/src/cli.ts b/packages/openapi-ts/src/cli.ts deleted file mode 100644 index 75f528a46..000000000 --- a/packages/openapi-ts/src/cli.ts +++ /dev/null @@ -1,120 +0,0 @@ -import type { OptionValues } from 'commander'; -import { Command } from 'commander'; - -import { createClient } from '~/index'; - -import pkg from '../package.json' assert { type: 'json' }; - -const stringToBoolean = ( - value: string | undefined, -): boolean | string | undefined => { - if (value === 'true') return true; - if (value === 'false') return false; - return value; -}; - -const processParams = ( - obj: OptionValues, - booleanKeys: ReadonlyArray, -): OptionValues => { - for (const key of booleanKeys) { - const value = obj[key]; - if (typeof value === 'string') { - const parsedValue = stringToBoolean(value); - delete obj[key]; - obj[key] = parsedValue; - } - } - return obj; -}; - -export const runCli = async (): Promise => { - const params = new Command() - .name(Object.keys(pkg.bin)[0]!) - .usage('[options]') - .version(pkg.version) - .option('-c, --client ', 'HTTP client to generate') - .option('-d, --debug', 'Set log level to debug') - .option('--dry-run [value]', 'Skip writing files to disk?') - .option('-f, --file [value]', 'Path to the config file') - .option( - '-i, --input ', - 'OpenAPI specification (path, url, or string content)', - ) - .option('-l, --logs [value]', 'Logs folder') - .option('-o, --output ', 'Output folder') - .option('-p, --plugins [value...]', "List of plugins you'd like to use") - .option('-s, --silent', 'Set log level to silent') - .option( - '--no-log-file', - 'Disable writing a log file. Works like --silent but without suppressing console output', - ) - .option( - '-w, --watch [value]', - 'Regenerate the client when the input file changes?', - ) - .parse(process.argv) - .opts(); - - let userConfig: Record; - - try { - userConfig = processParams(params, ['dryRun', 'logFile']); - - if (userConfig.file) { - userConfig.configFile = userConfig.file; - delete userConfig.file; - } - - if (params.plugins === true) { - userConfig.plugins = []; - } else if (params.plugins) { - userConfig.plugins = params.plugins; - } else if (userConfig.client) { - userConfig.plugins = ['@hey-api/typescript', '@hey-api/sdk']; - } - - if (userConfig.client) { - (userConfig.plugins as Array).push(userConfig.client as string); - delete userConfig.client; - } - - userConfig.logs = userConfig.logs - ? { - path: userConfig.logs, - } - : {}; - - if (userConfig.debug) { - (userConfig.logs as Record).level = 'debug'; - delete userConfig.debug; - } else if (userConfig.silent) { - (userConfig.logs as Record).level = 'silent'; - delete userConfig.silent; - } - - (userConfig.logs as Record).file = userConfig.logFile; - delete userConfig.logFile; - - if (typeof params.watch === 'string') { - userConfig.watch = Number.parseInt(params.watch, 10); - } - - if (!Object.keys(userConfig.logs as Record).length) { - delete userConfig.logs; - } - - const context = await createClient( - userConfig as unknown as Required>[0], - ); - if ( - !context[0]?.config.input.some( - (input) => input.watch && input.watch.enabled, - ) - ) { - process.exit(0); - } - } catch { - process.exit(1); - } -}; diff --git a/packages/openapi-ts/src/cli/adapter.ts b/packages/openapi-ts/src/cli/adapter.ts new file mode 100644 index 000000000..222a088fa --- /dev/null +++ b/packages/openapi-ts/src/cli/adapter.ts @@ -0,0 +1,40 @@ +import type { ToArray } from '@hey-api/types'; + +import type { UserConfig } from '~/config/types'; + +import type { CliOptions } from './schema'; + +export const cliToConfig = (cli: CliOptions): Partial => { + const config: Partial = {}; + + if (cli.input) config.input = cli.input; + if (cli.output) config.output = cli.output; + if (cli.file) config.configFile = cli.file; + if (cli.dryRun !== undefined) config.dryRun = cli.dryRun; + + const plugins: ToArray = []; + if (cli.plugins instanceof Array && cli.plugins.length > 0) { + plugins.push(...cli.plugins); + } + if (cli.client) plugins.push(cli.client); + if (plugins.length > 0) config.plugins = plugins; + + if (cli.debug || cli.silent || cli.logs || cli.logFile !== undefined) { + config.logs = { + ...(cli.logs && { path: cli.logs }), + ...(cli.debug && { level: 'debug' as const }), + ...(cli.silent && { level: 'silent' as const }), + ...(cli.logFile !== undefined && { file: cli.logFile }), + }; + } + + if (cli.watch !== undefined) { + if (typeof cli.watch === 'string') { + config.watch = Number.parseInt(cli.watch, 10); + } else { + config.watch = cli.watch; + } + } + + return config; +}; diff --git a/packages/openapi-ts/src/cli/index.ts b/packages/openapi-ts/src/cli/index.ts new file mode 100644 index 000000000..7c02bd893 --- /dev/null +++ b/packages/openapi-ts/src/cli/index.ts @@ -0,0 +1,67 @@ +import { Command, CommanderError } from 'commander'; + +import { createClient } from '~/index'; + +import pkg from '../../package.json' assert { type: 'json' }; +import { cliToConfig } from './adapter'; + +const binName = Object.keys(pkg.bin)[0]!; + +const program = new Command() + .name(binName) + .description('Generate TypeScript code from OpenAPI specifications') + .version(pkg.version); + +program + .option( + '-i, --input ', + 'OpenAPI specification (path, URL, or string)', + ) + .option('-o, --output ', 'Output folder(s)') + .option('-c, --client ', 'HTTP client to generate') + .option('-p, --plugins [names...]', 'Plugins to use') + .option('-f, --file ', 'Path to config file') + .option('-d, --debug', 'Enable debug logging') + .option('-s, --silent', 'Suppress all output') + .option('-l, --logs ', 'Logs folder path') + .option('--no-log-file', 'Disable log file output') + .option('--dry-run', 'Skip writing files') + .option('-w, --watch [interval]', 'Watch for changes') + .action(async (options) => { + const config = cliToConfig(options); + + const context = await createClient( + config as Parameters[0], + ); + + const hasActiveWatch = context[0]?.config.input.some( + (input) => input.watch?.enabled, + ); + + if (!hasActiveWatch) { + process.exit(0); + } + }); + +export async function runCli(): Promise { + try { + await program.parseAsync(process.argv); + } catch (error) { + if (error instanceof CommanderError && 'code' in error) { + if (error.code === 'commander.optionMissingArgument') { + console.error( + `\nMissing required argument. Run '${binName} --help' for usage.\n`, + ); + } else if (error.code === 'commander.unknownOption') { + console.error( + `\nUnknown option. Run '${binName} --help' for available options.\n`, + ); + } + + process.exit(error.exitCode); + } + + console.error('Unexpected error:', error); + process.exit(1); + } +} diff --git a/packages/openapi-ts/src/cli/schema.ts b/packages/openapi-ts/src/cli/schema.ts new file mode 100644 index 000000000..0cd428a83 --- /dev/null +++ b/packages/openapi-ts/src/cli/schema.ts @@ -0,0 +1,17 @@ +import type { MaybeArray } from '@hey-api/types'; + +import type { PluginClientNames, PluginNames } from '~/plugins/types'; + +export interface CliOptions { + client?: PluginClientNames; + debug?: boolean; + dryRun?: boolean; + file?: string; + input?: MaybeArray; + logFile?: boolean; + logs?: string; + output?: MaybeArray; + plugins?: ReadonlyArray; + silent?: boolean; + watch?: boolean | string; +} diff --git a/packages/openapi-ts/src/config/expand.ts b/packages/openapi-ts/src/config/expand.ts new file mode 100644 index 000000000..0c78e1d75 --- /dev/null +++ b/packages/openapi-ts/src/config/expand.ts @@ -0,0 +1,54 @@ +import colors from 'ansi-colors'; + +import { getInput } from './input'; +import type { UserConfig } from './types'; + +export interface Job { + config: UserConfig; + index: number; +} + +export function expandToJobs( + configs: ReadonlyArray, +): ReadonlyArray { + const jobs: Array = []; + let jobIndex = 0; + + for (const config of configs) { + const inputs = getInput(config); + const outputs = + config.output instanceof Array ? config.output : [config.output]; + + if (outputs.length === 1) { + jobs.push({ + config: { + ...config, + input: inputs, + output: outputs[0]!, // output array with single item + }, + index: jobIndex++, + }); + } else if (outputs.length > 1 && inputs.length !== outputs.length) { + // Warn and create job per output (all with same inputs) + console.warn( + `⚙️ ${colors.yellow('Warning:')} You provided ${colors.cyan(String(inputs.length))} ${colors.cyan(inputs.length === 1 ? 'input' : 'inputs')} and ${colors.yellow(String(outputs.length))} ${colors.yellow('outputs')}. This will produce identical output in multiple locations. You likely want to provide a single output or the same number of outputs as inputs.`, + ); + for (const output of outputs) { + jobs.push({ + config: { ...config, input: inputs, output }, + index: jobIndex++, + }); + } + } else if (outputs.length > 1) { + // Pair inputs with outputs by index + outputs.forEach((output, index) => { + jobs.push({ + config: { ...config, input: inputs[index]!, output }, + index: jobIndex++, + }); + }); + } + } + + return jobs; +} diff --git a/packages/openapi-ts/src/config/init.ts b/packages/openapi-ts/src/config/init.ts index 7068e3368..a668b35ed 100644 --- a/packages/openapi-ts/src/config/init.ts +++ b/packages/openapi-ts/src/config/init.ts @@ -1,184 +1,71 @@ -import path from 'node:path'; - import type { Logger } from '@hey-api/codegen-core'; -import type { ArrayOnly } from '@hey-api/types'; -import colors from 'ansi-colors'; - -import { ConfigError } from '~/error'; +import { loadConfigFile } from '@hey-api/codegen-core'; -import { getInput } from './input'; -import { getLogs } from './logs'; -import { mergeConfigs } from './merge'; -import { getOutput } from './output'; +import { expandToJobs } from './expand'; import { getProjectDependencies } from './packages'; -import { getParser } from './parser'; -import { getPlugins } from './plugins'; -import type { Config, UserConfig } from './types'; - -type ConfigResult = { - config: Config; - errors: ReadonlyArray; - jobIndex: number; -}; +import type { ResolvedJob } from './resolve'; +import { resolveConfig } from './resolve'; +import type { UserConfig } from './types'; +import { validateJobs } from './validate'; export type Configs = { dependencies: Record; - results: ReadonlyArray; + jobs: ReadonlyArray; + /** + * @deprecated Use `jobs` instead. + */ + 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. - * @internal - */ -export const detectInteractiveSession = (): boolean => - Boolean( - process.stdin.isTTY && - process.stdout.isTTY && - !process.env.CI && - !process.env.NO_INTERACTIVE && - !process.env.NO_INTERACTION, - ); - /** * @internal */ -export const initConfigs = async ({ +export async function resolveJobs({ logger, userConfigs, }: { logger: Logger; userConfigs: ReadonlyArray; -}): Promise => { +}): 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) { + let configFile: string | undefined; + if (userConfig.configFile) { const parts = userConfig.configFile.split('.'); - configurationFile = parts.slice(0, parts.length - 1).join('.'); + configFile = parts.slice(0, parts.length - 1).join('.'); } - const eventC12 = logger.timeEvent('c12'); - // c12 is ESM-only since v3 - const { loadConfig } = await import('c12'); - const { config: configFromFile, configFile: loadedConfigFile } = - await loadConfig({ - configFile: configurationFile, - name: 'openapi-ts', - }); - eventC12.timeEnd(); + const loaded = await loadConfigFile({ + configFile, + logger, + name: 'openapi-ts', + userConfig, + }); if (!Object.keys(dependencies).length) { // TODO: handle dependencies for multiple configs properly? dependencies = getProjectDependencies( - Object.keys(configFromFile).length ? loadedConfigFile : undefined, + loaded.foundConfig ? loaded.configFile : undefined, ); } - 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 }); - } - } + configs.push(...loaded.configs); } eventLoad.timeEnd(); - const results: Array> = []; - const eventBuild = logger.timeEvent('build'); - for (const userConfig of configs) { - const logs = getLogs(userConfig); - const input = getInput(userConfig); - const output = getOutput(userConfig); - const parser = getParser(userConfig); - - const errors: Array = []; - - if (!input.length) { - errors.push( - new ConfigError( - 'missing input - which OpenAPI specification should we use to generate your output?', - ), - ); - } - - if (!output.path) { - errors.push( - new ConfigError( - 'missing output - where should we generate your output?', - ), - ); - } - - output.path = path.resolve(process.cwd(), output.path); - - let plugins: Pick; - - try { - plugins = getPlugins({ dependencies, userConfig }); - } catch (error) { - errors.push(error); - plugins = { - pluginOrder: [], - plugins: {}, - }; - } - - const config: Config = { - configFile: userConfig.configFile ?? '', - dryRun: userConfig.dryRun ?? false, - input, - interactive: userConfig.interactive ?? detectInteractiveSession(), - logs, - output, - parser, - pluginOrder: plugins.pluginOrder, - plugins: plugins.plugins, - }; - - const jobIndex = results.length; - - if (logs.level === 'debug') { - const jobPrefix = colors.gray(`[Job ${jobIndex + 1}] `); - console.warn(`${jobPrefix}${colors.cyan('config:')}`, config); - } - - results.push({ config, errors, jobIndex }); - } + const jobs = validateJobs(expandToJobs(configs)); + const resolvedJobs = jobs.map((validated) => + resolveConfig(validated, dependencies), + ); eventBuild.timeEnd(); - return { dependencies, results }; -}; + return { + dependencies, + jobs: resolvedJobs, + results: resolvedJobs, + }; +} diff --git a/packages/openapi-ts/src/config/merge.ts b/packages/openapi-ts/src/config/merge.ts deleted file mode 100644 index a4ce11067..000000000 --- a/packages/openapi-ts/src/config/merge.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { UserConfig } from './types'; - -const mergeObjects = ( - objA: Record | undefined, - objB: Record | undefined, -): Record => { - const a = objA || {}; - const b = objB || {}; - return { - ...a, - ...b, - }; -}; - -export const mergeConfigs = ( - configA: UserConfig | undefined, - configB: UserConfig | undefined, -): UserConfig => { - const a: Partial = configA || {}; - const b: Partial = configB || {}; - const merged: UserConfig = { - ...(a as UserConfig), - ...(b as UserConfig), - }; - if (typeof merged.logs === 'object') { - merged.logs = mergeObjects( - a.logs as Record, - b.logs as Record, - ); - } - return merged; -}; diff --git a/packages/openapi-ts/src/config/resolve.ts b/packages/openapi-ts/src/config/resolve.ts new file mode 100644 index 000000000..19352320e --- /dev/null +++ b/packages/openapi-ts/src/config/resolve.ts @@ -0,0 +1,65 @@ +import path from 'node:path'; + +import { detectInteractiveSession } from '@hey-api/codegen-core'; +import colors from 'ansi-colors'; + +import { getInput } from './input'; +import { getLogs } from './logs'; +import { getOutput } from './output'; +import { getParser } from './parser'; +import { getPlugins } from './plugins'; +import type { Config } from './types'; +import type { ValidationResult } from './validate'; + +export type ResolvedJob = { + config: Config; + errors: Array; + index: number; +}; + +export const resolveConfig = ( + validated: ValidationResult, + dependencies: Record, +): ResolvedJob => { + const logs = getLogs(validated.job.config); + const input = getInput(validated.job.config); + const output = getOutput(validated.job.config); + const parser = getParser(validated.job.config); + + output.path = path.resolve(process.cwd(), output.path); + + let plugins: Pick; + + try { + plugins = getPlugins({ dependencies, userConfig: validated.job.config }); + } catch (error) { + validated.errors.push(error); + plugins = { + pluginOrder: [], + plugins: {}, + }; + } + + const config: Config = { + configFile: validated.job.config.configFile ?? '', + dryRun: validated.job.config.dryRun ?? false, + input, + interactive: validated.job.config.interactive ?? detectInteractiveSession(), + logs, + output, + parser, + pluginOrder: plugins.pluginOrder, + plugins: plugins.plugins, + }; + + if (logs.level === 'debug') { + const jobPrefix = colors.gray(`[Job ${validated.job.index}] `); + console.warn(`${jobPrefix}${colors.cyan('config:')}`, config); + } + + return { + config, + errors: validated.errors, + index: validated.job.index, + }; +}; diff --git a/packages/openapi-ts/src/config/types.d.ts b/packages/openapi-ts/src/config/types.d.ts index 940423b18..c6568a935 100644 --- a/packages/openapi-ts/src/config/types.d.ts +++ b/packages/openapi-ts/src/config/types.d.ts @@ -9,7 +9,7 @@ import type { Parser, UserParser } from '~/types/parser'; import type { Output, UserOutput } from './output'; -export interface UserConfig { +export type UserConfig = { /** * Path to the config file. Set this value if you don't use the default * config file name, or it's not located in the project root. @@ -81,7 +81,7 @@ export interface UserConfig { * @deprecated use `input.watch` instead */ watch?: boolean | number | Watch; -} +}; export type Config = Omit< Required, diff --git a/packages/openapi-ts/src/config/validate.ts b/packages/openapi-ts/src/config/validate.ts new file mode 100644 index 000000000..2be89f161 --- /dev/null +++ b/packages/openapi-ts/src/config/validate.ts @@ -0,0 +1,39 @@ +import { ConfigError } from '~/error'; + +import type { Job } from './expand'; +import { getInput } from './input'; +import { getOutput } from './output'; + +export interface ValidationResult { + errors: Array; + job: Job; +} + +export function validateJobs( + jobs: ReadonlyArray, +): ReadonlyArray { + return jobs.map((job) => { + const errors: Array = []; + const { config } = job; + + const inputs = getInput(config); + if (!inputs.length) { + errors.push( + new ConfigError( + 'missing input - which OpenAPI specification should we use to generate your output?', + ), + ); + } + + const output = getOutput(config); + if (!output.path) { + errors.push( + new ConfigError( + 'missing output - where should we generate your output?', + ), + ); + } + + return { errors, job }; + }); +} diff --git a/packages/openapi-ts/src/generate.ts b/packages/openapi-ts/src/generate.ts index 6aba3be51..bdbb3907c 100644 --- a/packages/openapi-ts/src/generate.ts +++ b/packages/openapi-ts/src/generate.ts @@ -3,7 +3,7 @@ import type { LazyOrAsync, MaybeArray } from '@hey-api/types'; import { checkNodeVersion } from '~/config/engine'; import type { Configs } from '~/config/init'; -import { initConfigs } from '~/config/init'; +import { resolveJobs } from '~/config/init'; import { getLogs } from '~/config/logs'; import type { UserConfig } from '~/config/types'; import { createClient as pCreateClient } from '~/createClient'; @@ -23,10 +23,10 @@ import { printCliIntro } from '~/utils/cli'; * * @param userConfig User provided {@link UserConfig} configuration(s). */ -export const createClient = async ( +export async function createClient( userConfig?: LazyOrAsync>, logger = new Logger(), -): Promise> => { +): Promise> { const resolvedConfig = typeof userConfig === 'function' ? await userConfig() : userConfig; const userConfigs = resolvedConfig @@ -42,7 +42,7 @@ export const createClient = async ( rawLogs = getLogs({ logs: rawLogs }); } - let configs: Configs | undefined; + let jobs: Configs['jobs'] = []; try { checkNodeVersion(); @@ -50,61 +50,51 @@ export const createClient = async ( const eventCreateClient = logger.timeEvent('createClient'); const eventConfig = logger.timeEvent('config'); - configs = await initConfigs({ logger, userConfigs }); - const printIntro = configs.results.some( - (result) => result.config.logs.level !== 'silent', - ); - if (printIntro) { - printCliIntro(); - } + const resolved = await resolveJobs({ logger, userConfigs }); + const dependencies = resolved.dependencies; + jobs = resolved.jobs; + const printIntro = jobs.some((job) => job.config.logs.level !== 'silent'); + if (printIntro) printCliIntro(); eventConfig.timeEnd(); - const allConfigErrors = configs.results.flatMap((result) => - result.errors.map((error) => ({ error, jobIndex: result.jobIndex })), + const configErrors = jobs.flatMap((job) => + job.errors.map((error) => ({ error, jobIndex: job.index })), ); - if (allConfigErrors.length) { - throw new ConfigValidationError(allConfigErrors); + if (configErrors.length > 0) { + throw new ConfigValidationError(configErrors); } - const clients = await Promise.all( - configs.results.map(async (result) => { + const outputs = await Promise.all( + jobs.map(async (job) => { try { return await pCreateClient({ - config: result.config, - dependencies: configs!.dependencies, - jobIndex: result.jobIndex, + config: job.config, + dependencies, + jobIndex: job.index, logger, }); } catch (error) { throw new JobError('', { error, - jobIndex: result.jobIndex, + jobIndex: job.index, }); } }), ); - const result = clients.filter((client) => - Boolean(client), - ) as ReadonlyArray; + const contexts = outputs.filter((ctx): ctx is Context => ctx !== undefined); eventCreateClient.timeEnd(); - const printLogs = configs.results.some( - (result) => result.config.logs.level === 'debug', - ); - logger.report(printLogs); + logger.report(jobs.some((job) => job.config.logs.level === 'debug')); - return result; + return contexts; } catch (error) { - const results = configs?.results ?? []; - const logs = - results.find((result) => result.config.logs.level !== 'silent')?.config - .logs ?? - results[0]?.config.logs ?? + jobs.find((job) => job.config.logs.level !== 'silent')?.config.logs ?? + jobs[0]?.config.logs ?? rawLogs; const dryRun = - results.some((result) => result.config.dryRun) ?? + jobs.some((job) => job.config.dryRun) ?? userConfigs.some((config) => config.dryRun) ?? false; const logPath = @@ -114,7 +104,7 @@ export const createClient = async ( if (!logs || logs.level !== 'silent') { printCrashReport({ error, logPath }); const isInteractive = - results.some((result) => result.config.interactive) ?? + jobs.some((job) => job.config.interactive) ?? userConfigs.some((config) => config.interactive) ?? false; if (await shouldReportCrash({ error, isInteractive })) { @@ -124,4 +114,4 @@ export const createClient = async ( throw error; } -}; +} diff --git a/packages/openapi-ts/src/index.ts b/packages/openapi-ts/src/index.ts index 63ea62111..f2bf9a14e 100644 --- a/packages/openapi-ts/src/index.ts +++ b/packages/openapi-ts/src/index.ts @@ -79,11 +79,13 @@ colors.enabled = colorSupport().hasBasic; export { createClient } from '~/generate'; /** - * Type helper for openapi-ts.config.ts, returns {@link MaybeArray} object(s) + * Type helper for configuration object, returns {@link MaybeArray} object(s) */ -export const defineConfig = async >( +export async function defineConfig>( config: LazyOrAsync, -): Promise => (typeof config === 'function' ? await config() : config); +): Promise { + return typeof config === 'function' ? await config() : config; +} export { Logger } from '@hey-api/codegen-core'; export { defaultPaginationKeywords } from '~/config/parser'; diff --git a/packages/openapi-ts/src/internal.ts b/packages/openapi-ts/src/internal.ts index ec80d9485..5ed164152 100644 --- a/packages/openapi-ts/src/internal.ts +++ b/packages/openapi-ts/src/internal.ts @@ -1,3 +1,3 @@ -export { initConfigs } from './config/init'; +export { resolveJobs as initConfigs } from './config/init'; export { getSpec } from './getSpec'; export { parseOpenApiSpec } from './openApi'; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 01ab8a619..2a866c98d 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,8 +1,13 @@ +/** + * An object with string keys and unknown values. + */ +export type AnyObject = Record; + /** * Converts all top-level ReadonlyArray properties to Array (shallow). */ export type ArrayOnly = { - [K in keyof T]: T[K] extends ReadonlyArray ? Array : T[K]; + [K in keyof T]: ToArray; }; /** @@ -42,5 +47,16 @@ export type MaybePromise = T | Promise; * Converts all top-level Array properties to ReadonlyArray (shallow). */ export type ReadonlyArrayOnly = { - [K in keyof T]: T[K] extends Array ? ReadonlyArray : T[K]; + [K in keyof T]: ToReadonlyArray; }; + +/** + * Converts ReadonlyArray to Array, preserving unions. + */ +export type ToArray = T extends ReadonlyArray ? Array : T; + +/** + * Converts Array to ReadonlyArray, preserving unions. + */ +export type ToReadonlyArray = + T extends ReadonlyArray ? ReadonlyArray : T; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04b8bcda9..083800e11 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1253,6 +1253,9 @@ importers: ansi-colors: specifier: 4.1.3 version: 4.1.3 + c12: + specifier: 3.3.3 + version: 3.3.3(magicast@0.3.5) color-support: specifier: 1.1.3 version: 1.1.3 @@ -1346,9 +1349,6 @@ importers: ansi-colors: specifier: 4.1.3 version: 4.1.3 - c12: - specifier: 3.3.3 - version: 3.3.3(magicast@0.3.5) color-support: specifier: 1.1.3 version: 1.1.3 @@ -1401,9 +1401,6 @@ importers: ansi-colors: specifier: 4.1.3 version: 4.1.3 - c12: - specifier: 3.3.3 - version: 3.3.3(magicast@0.3.5) color-support: specifier: 1.1.3 version: 1.1.3 @@ -7908,14 +7905,6 @@ packages: magicast: optional: true - c12@3.3.2: - resolution: {integrity: sha512-QkikB2X5voO1okL3QsES0N690Sn/K9WokXqUsDQsWy5SnYb+psYQFGA10iy1bZHj3fjISKsI67Q90gruvWWM3A==} - peerDependencies: - magicast: '*' - peerDependenciesMeta: - magicast: - optional: true - c12@3.3.3: resolution: {integrity: sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==} peerDependencies: @@ -14693,7 +14682,7 @@ snapshots: '@vitejs/plugin-basic-ssl': 1.2.0(vite@7.2.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.2)) ansi-colors: 4.1.3 autoprefixer: 10.4.20(postcss@8.5.2) - babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0) + babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0(esbuild@0.25.0)) browserslist: 4.25.4 copy-webpack-plugin: 12.0.2(webpack@5.98.0) css-loader: 7.1.2(webpack@5.98.0) @@ -14713,7 +14702,7 @@ snapshots: picomatch: 4.0.2 piscina: 4.8.0 postcss: 8.5.2 - postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0) + postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)) resolve-url-loader: 5.0.0 rxjs: 7.8.1 sass: 1.85.0 @@ -14781,7 +14770,7 @@ snapshots: '@vitejs/plugin-basic-ssl': 1.2.0(vite@7.2.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.2)) ansi-colors: 4.1.3 autoprefixer: 10.4.20(postcss@8.5.2) - babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0) + babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0(esbuild@0.25.0)) browserslist: 4.25.4 copy-webpack-plugin: 12.0.2(webpack@5.98.0) css-loader: 7.1.2(webpack@5.98.0) @@ -14801,7 +14790,7 @@ snapshots: picomatch: 4.0.2 piscina: 4.8.0 postcss: 8.5.2 - postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0) + postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)) resolve-url-loader: 5.0.0 rxjs: 7.8.1 sass: 1.85.0 @@ -14889,7 +14878,7 @@ snapshots: picomatch: 4.0.2 piscina: 4.8.0 postcss: 8.5.2 - postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0) + postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)) resolve-url-loader: 5.0.0 rxjs: 7.8.1 sass: 1.85.0 @@ -19166,7 +19155,7 @@ snapshots: '@nuxt/test-utils@3.21.0(@vue/test-utils@2.4.6)(jsdom@23.0.0)(magicast@0.3.5)(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.10.5)(jiti@2.6.1)(jsdom@23.0.0)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.2))': dependencies: '@nuxt/kit': 3.20.2(magicast@0.3.5) - c12: 3.3.2(magicast@0.3.5) + c12: 3.3.3(magicast@0.3.5) consola: 3.4.2 defu: 6.1.4 destr: 2.0.5 @@ -22379,7 +22368,7 @@ snapshots: schema-utils: 4.3.2 webpack: 5.98.0(esbuild@0.25.0) - babel-loader@9.2.1(@babel/core@7.26.9)(webpack@5.98.0): + babel-loader@9.2.1(@babel/core@7.26.9)(webpack@5.98.0(esbuild@0.25.0)): dependencies: '@babel/core': 7.26.9 find-cache-dir: 4.0.0 @@ -22596,23 +22585,6 @@ snapshots: optionalDependencies: magicast: 0.3.5 - c12@3.3.2(magicast@0.3.5): - dependencies: - chokidar: 4.0.3 - confbox: 0.2.2 - defu: 6.1.4 - dotenv: 17.2.3 - exsolve: 1.0.8 - giget: 2.0.0 - jiti: 2.6.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 - c12@3.3.3(magicast@0.3.5): dependencies: chokidar: 5.0.0 @@ -27869,7 +27841,7 @@ snapshots: ts-node: 10.9.2(@types/node@22.10.5)(typescript@5.9.3) optional: true - postcss-loader@8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0): + postcss-loader@8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)): dependencies: cosmiconfig: 9.0.0(typescript@5.8.3) jiti: 1.21.7