diff --git a/.gitignore b/.gitignore --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ .turbo # test files +.gen/ test/generated # debug files diff --git a/package.json b/package.json --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@config/vite-base": "workspace:*", "@eslint/js": "9.32.0", "@hey-api/custom-client": "workspace:*", + "@hey-api/openapi-ts": "workspace:*", "@types/node": "22.10.5", "@typescript-eslint/eslint-plugin": "8.29.1", "@vitest/coverage-v8": "3.1.1", @@ -65,6 +66,7 @@ "prettier": "3.4.2", "rollup": "4.31.0", "rollup-plugin-dts": "6.1.1", + "ts-node": "10.9.2", "tsdown": "0.15.8", "turbo": "2.5.8", "typescript": "5.9.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,6 +37,9 @@ '@hey-api/custom-client': specifier: workspace:* version: link:packages/custom-client + '@hey-api/openapi-ts': + specifier: workspace:* + version: link:packages/openapi-ts '@types/node': specifier: 22.10.5 version: 22.10.5 @@ -88,6 +91,9 @@ rollup-plugin-dts: specifier: 6.1.1 version: 6.1.1(rollup@4.31.0)(typescript@5.9.3) + ts-node: + specifier: 10.9.2 + version: 10.9.2(@types/node@22.10.5)(typescript@5.9.3) tsdown: specifier: 0.15.8 version: 0.15.8(@arethetypeswrong/core@0.18.2)(typescript@5.9.3) @@ -170,7 +176,7 @@ devDependencies: '@angular-devkit/build-angular': specifier: 19.2.0 - version: 19.2.0(3d70bfaaf57777265a4e229c41aeb4c6) + version: 19.2.0(a969acb6c0970aa981df1cb7672d7c2e) '@angular/cli': specifier: 19.2.0 version: 19.2.0(@types/node@22.10.5)(chokidar@4.0.3) @@ -264,7 +270,7 @@ devDependencies: '@angular-devkit/build-angular': specifier: 19.2.0 - version: 19.2.0(a969acb6c0970aa981df1cb7672d7c2e) + version: 19.2.0(3d70bfaaf57777265a4e229c41aeb4c6) '@angular/cli': specifier: 19.2.0 version: 19.2.0(@types/node@22.10.5)(chokidar@4.0.3) @@ -23183,7 +23189,7 @@ eslint: 9.17.0(jiti@2.6.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.6.1))(typescript@5.8.3))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.17.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.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.6.1))(typescript@5.8.3))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.17.0(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.17.0(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.17.0(jiti@2.6.1)) @@ -23221,7 +23227,7 @@ 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.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.17.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.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.6.1))(typescript@5.8.3))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -23236,7 +23242,7 @@ transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.17.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.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.6.1))(typescript@5.8.3))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,12 +3,5 @@ - examples/**/* - packages/**/* -onlyBuiltDependencies: - - esbuild - - lmdb - - msgpackr-extract - - sharp - - unrs-resolver - patchedDependencies: vitepress: patches/vitepress.patch diff --git a/.changeset/short-phones-count.md b/.changeset/short-phones-count.md new file mode 100644 --- /dev/null +++ b/.changeset/short-phones-count.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +fix(cli): move cli script to typescript diff --git a/.vscode/launch.json b/.vscode/launch.json --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,12 +14,9 @@ "name": "openapi-ts", "skipFiles": ["/**"], "runtimeExecutable": "node", - "runtimeArgs": [], - "program": "${workspaceFolder}/packages/openapi-ts/bin/index.cjs", - "args": [ - "-f", - "${workspaceFolder}/packages/openapi-ts-tests/main/test/openapi-ts.config.ts" - ] + "runtimeArgs": ["-r", "ts-node/register/transpile-only"], + "program": "${workspaceFolder}/packages/openapi-ts/src/cli.ts", + "args": ["-f", "${workspaceFolder}/dev/openapi-ts.config.ts"] } ] } diff --git a/debug-helpers/graph-hotspots.js b/debug-helpers/graph-hotspots.js deleted file mode 100644 --- a/debug-helpers/graph-hotspots.js +++ /dev/null @@ -1,26 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// Load your exported graph -const nodes = JSON.parse( - fs.readFileSync(path.resolve(__dirname, 'graph.json'), 'utf-8'), -); - -// Annotate nodes with children count -const annotatedNodes = nodes.map((n) => ({ - childrenCount: n.childrenPointers?.length ?? 0, - pointer: n.pointer, -})); - -// Sort by childrenCount descending -annotatedNodes.sort((a, b) => b.childrenCount - a.childrenCount); - -// Print top 20 hotspots -console.log('Top 20 potential bottleneck nodes:\n'); -annotatedNodes.slice(0, 20).forEach((n) => { - console.log(`${n.pointer} — children: ${n.childrenCount}`); -}); diff --git a/debug-helpers/json-to-dot.js b/debug-helpers/json-to-dot.js deleted file mode 100644 --- a/debug-helpers/json-to-dot.js +++ /dev/null @@ -1,60 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const nodes = JSON.parse( - fs.readFileSync(path.resolve(__dirname, 'graph.json'), 'utf-8'), -); - -// --- Filter nodes for readability --- -const threshold = 10; // high-fanout threshold -const filteredSet = new Set(); - -// Include nodes with children > threshold and their immediate children -for (const n of nodes) { - const childCount = n.childrenPointers?.length ?? 0; - if (childCount > threshold) { - filteredSet.add(n.pointer); - for (const child of n.childrenPointers || []) { - filteredSet.add(child); - } - } -} - -// Filtered nodes list -const filteredNodes = nodes.filter((n) => filteredSet.has(n.pointer)); - -// Start the .dot file -let dot = 'digraph OpenAPIGraph {\nrankdir=LR;\nnode [style=filled];\n'; - -// Add nodes with color based on fanout -for (const n of filteredNodes) { - const childCount = n.childrenPointers?.length ?? 0; - const color = - childCount > 50 ? 'red' : childCount > 20 ? 'orange' : 'lightgray'; - dot += `"${n.pointer}" [label="${n.pointer}\\n${childCount} children", fillcolor=${color}];\n`; -} - -// Add edges: node -> its children -for (const n of filteredNodes) { - for (const child of n.childrenPointers || []) { - if (filteredSet.has(child)) { - dot += `"${n.pointer}" -> "${child}";\n`; - } - } -} - -dot += '}\n'; - -// Write to a file -fs.writeFileSync(path.resolve(__dirname, 'graph.dot'), dot); -console.log('graph.dot created!'); - -// Instructions: -// Render with Graphviz: -// dot -Tpng graph.dot -o graph.png -// or -// dot -Tsvg graph.dot -o graph.svg diff --git a/dev/graph-hotspots.js b/dev/graph-hotspots.js new file mode 100644 --- /dev/null +++ b/dev/graph-hotspots.js @@ -0,0 +1,26 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Load your exported graph +const nodes = JSON.parse( + fs.readFileSync(path.resolve(__dirname, 'graph.json'), 'utf-8'), +); + +// Annotate nodes with children count +const annotatedNodes = nodes.map((n) => ({ + childrenCount: n.childrenPointers?.length ?? 0, + pointer: n.pointer, +})); + +// Sort by childrenCount descending +annotatedNodes.sort((a, b) => b.childrenCount - a.childrenCount); + +// Print top 20 hotspots +console.log('Top 20 potential bottleneck nodes:\n'); +annotatedNodes.slice(0, 20).forEach((n) => { + console.log(`${n.pointer} — children: ${n.childrenCount}`); +}); diff --git a/dev/json-to-dot.js b/dev/json-to-dot.js new file mode 100644 --- /dev/null +++ b/dev/json-to-dot.js @@ -0,0 +1,60 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const nodes = JSON.parse( + fs.readFileSync(path.resolve(__dirname, 'graph.json'), 'utf-8'), +); + +// --- Filter nodes for readability --- +const threshold = 10; // high-fanout threshold +const filteredSet = new Set(); + +// Include nodes with children > threshold and their immediate children +for (const n of nodes) { + const childCount = n.childrenPointers?.length ?? 0; + if (childCount > threshold) { + filteredSet.add(n.pointer); + for (const child of n.childrenPointers || []) { + filteredSet.add(child); + } + } +} + +// Filtered nodes list +const filteredNodes = nodes.filter((n) => filteredSet.has(n.pointer)); + +// Start the .dot file +let dot = 'digraph OpenAPIGraph {\nrankdir=LR;\nnode [style=filled];\n'; + +// Add nodes with color based on fanout +for (const n of filteredNodes) { + const childCount = n.childrenPointers?.length ?? 0; + const color = + childCount > 50 ? 'red' : childCount > 20 ? 'orange' : 'lightgray'; + dot += `"${n.pointer}" [label="${n.pointer}\\n${childCount} children", fillcolor=${color}];\n`; +} + +// Add edges: node -> its children +for (const n of filteredNodes) { + for (const child of n.childrenPointers || []) { + if (filteredSet.has(child)) { + dot += `"${n.pointer}" -> "${child}";\n`; + } + } +} + +dot += '}\n'; + +// Write to a file +fs.writeFileSync(path.resolve(__dirname, 'graph.dot'), dot); +console.log('graph.dot created!'); + +// Instructions: +// Render with Graphviz: +// dot -Tpng graph.dot -o graph.png +// or +// dot -Tsvg graph.dot -o graph.svg diff --git a/dev/openapi-ts.config.ts b/dev/openapi-ts.config.ts new file mode 100644 --- /dev/null +++ b/dev/openapi-ts.config.ts @@ -0,0 +1,484 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import path from 'node:path'; + +// @ts-ignore +import { customClientPlugin } from '@hey-api/custom-client/plugin'; +// @ts-ignore +import { defineConfig, utils } from '@hey-api/openapi-ts'; + +// @ts-ignore +import { myClientPlugin } from '../packages/openapi-ts-tests/main/test/custom/client/plugin'; +import { getSpecsPath } from '../packages/openapi-ts-tests/utils'; + +// @ts-ignore +// eslint-disable-next-line arrow-body-style +export default defineConfig(() => { + // ... + 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', + '3.1.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.json', + // '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, + // }, + }, + // 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', + }, + // 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, '.gen'), + // tsConfigPath: path.resolve( + // __dirname, + // 'tsconfig', + // 'tsconfig.nodenext.json', + // ), + // }, + path.resolve(__dirname, '.gen'), + ], + parser: { + filters: { + // deprecated: false, + operations: { + include: [ + // 'GET /event', + // '/^[A-Z]+ /v1//', + ], + }, + // orphans: true, + // preserveOrder: true, + // schemas: { + // include: ['Foo'], + // }, + // tags: { + // exclude: ['bar'], + // }, + }, + hooks: { + events: { + // 'plugin:handler:after': ({ plugin }) => { + // console.log(`(${plugin.name}): handler finished`); + // }, + // 'plugin:handler:before': ({ plugin }) => { + // console.log(`(${plugin.name}): handler starting`); + // }, + // 'symbol:register:after': ({ plugin, symbol }) => { + // console.log(`(global, ${plugin.name}) registered:`, symbol.id); + // }, + 'symbol:register:before': ({ plugin, symbol }) => { + // if (!symbol.external && !symbol.meta?.resourceType) { + // console.log(`[${plugin.name}]:`, symbol.name); + // } + if (!symbol.external && !symbol.meta?.path) { + console.log(`[${plugin.name}]:`, symbol.name, symbol.meta); + } + // if (symbol.meta?.tags && symbol.meta?.tags.size > 0) { + // console.log( + // `[${plugin.name}]:`, + // symbol.name, + // symbol.meta.path, + // symbol.meta.tags, + // ); + // } + }, + // 'symbol:setValue:after': ({ plugin, symbol }) => { + // console.log(`(${plugin.name}) set value:`, symbol.id); + // }, + // 'symbol:setValue:before': ({ plugin, symbol }) => { + // console.log(`(${plugin.name}) setting value:`, symbol.id); + // }, + }, + 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; + // }, + }, + }, + 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}}', + // }, + }, + // 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, + // getSignature: ({ fields, signature, operation }) => { + // // ... + // fields.unwrap('path') + // }, + // include... + // instance: true, + name: '@hey-api/sdk', + // operationId: false, + // params_EXPERIMENTAL: 'experiment', + // responseStyle: 'data', + // signature: 'auto', + // signature: 'client', + // signature: 'object', + // transformer: '@hey-api/transformers', + // transformer: true, + validator: { + request: 'zod', + 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', + }, + { + // 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 + }, + }, + }, + }, + { + // name: 'arktype', + // types: { + // infer: true, + // }, + }, + { + // 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': { + events: { + // 'symbol:register:after': ({ plugin, symbol }) => { + // console.log(`(${plugin.name}) registered:`, symbol.id); + // }, + // 'symbol:register:before': ({ plugin, symbol }) => { + // console.log(`(${plugin.name}):`, symbol.name); + // }, + }, + symbols: { + // getFilePath: (symbol) => { + // if (symbol.name) { + // return utils.stringCase({ + // case: 'camelCase', + // value: symbol.name, + // }); + // } + // return; + // }, + }, + }, + }, + { + // case: 'snake_case', + // comments: false, + compatibilityVersion: 4, + 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 '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, + }, + // name: '@angular/common', + }, + { + 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, + }, + // { + // input: 'scalar:@scalar/access-service', + // logs: { + // // level: 'debug', + // path: './logs', + // }, + // output: path.resolve(__dirname, '.gen'), + // }, + ]; +}); diff --git a/packages/openapi-ts/package.json b/packages/openapi-ts/package.json --- a/packages/openapi-ts/package.json +++ b/packages/openapi-ts/package.json @@ -66,7 +66,7 @@ "./package.json": "./package.json" }, "bin": { - "openapi-ts": "bin/index.cjs" + "openapi-ts": "./dist/cli.cjs" }, "files": [ "bin", diff --git a/packages/openapi-ts/tsdown.config.ts b/packages/openapi-ts/tsdown.config.ts --- a/packages/openapi-ts/tsdown.config.ts +++ b/packages/openapi-ts/tsdown.config.ts @@ -29,7 +29,7 @@ }, clean: true, dts: true, - entry: ['src/index.ts', 'src/internal.ts'], + entry: ['./src/{index,internal,cli}.ts'], format: ['cjs', 'esm'], minify: !options.watch, onSuccess: async () => { diff --git a/packages/openapi-ts-tests/main/.gitignore b/packages/openapi-ts-tests/main/.gitignore --- a/packages/openapi-ts-tests/main/.gitignore +++ b/packages/openapi-ts-tests/main/.gitignore @@ -16,5 +16,6 @@ .env # test files +.gen/ test/generated generated/ diff --git a/packages/openapi-ts/bin/index.cjs b/packages/openapi-ts/bin/index.cjs deleted file mode 100644 --- a/packages/openapi-ts/bin/index.cjs +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -const path = require('path'); - -const { program } = require('commander'); -const pkg = require('../package.json'); - -const params = program - .name(Object.keys(pkg.bin)[0]) - .usage('[options]') - .version(pkg.version) - .option( - '-c, --client ', - 'HTTP client to generate [@hey-api/client-axios, @hey-api/client-fetch, @hey-api/client-next, @hey-api/client-nuxt, legacy/angular, legacy/axios, legacy/fetch, legacy/node, legacy/xhr]', - ) - .option('-d, --debug', 'Set log level to debug') - .option('--dry-run [value]', 'Skip writing files to disk?') - .option( - '-e, --experimental-parser [value]', - 'Opt-in to the experimental parser?', - ) - .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( - '--base [value]', - 'DEPRECATED. Manually set base in OpenAPI config instead of inferring from server value', - ) - .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?', - ) - .option('--exportCore [value]', 'DEPRECATED. Write core files to disk') - .option('--name ', 'DEPRECATED. Custom client class name') - .option('--request ', 'DEPRECATED. Path to custom request file') - .option( - '--useOptions [value]', - 'DEPRECATED. Use options instead of arguments?', - ) - .parse(process.argv) - .opts(); - -const stringToBoolean = (value) => { - if (value === 'true') { - return true; - } - if (value === 'false') { - return false; - } - return value; -}; - -const processParams = (obj, booleanKeys) => { - for (const key of booleanKeys) { - const value = obj[key]; - if (typeof value === 'string') { - const parsedValue = stringToBoolean(value); - delete obj[key]; - obj[key] = parsedValue; - } - } - if (obj.file) { - obj.configFile = obj.file; - } - return obj; -}; - -async function start() { - let userConfig; - - try { - const { createClient } = require( - path.resolve(__dirname, '../dist/index.cjs'), - ); - - userConfig = processParams(params, [ - 'dryRun', - 'logFile', - 'experimentalParser', - 'exportCore', - 'useOptions', - ]); - - 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.push(userConfig.client); - delete userConfig.client; - } - - userConfig.logs = userConfig.logs - ? { - path: userConfig.logs, - } - : {}; - - if (userConfig.debug || stringToBoolean(process.env.DEBUG)) { - userConfig.logs.level = 'debug'; - } else if (userConfig.silent) { - userConfig.logs.level = 'silent'; - } - - userConfig.logs.file = userConfig.logFile; - delete userConfig.logFile; - - if (typeof params.watch === 'string') { - userConfig.watch = Number.parseInt(params.watch, 10); - } - - if (!Object.keys(userConfig.logs).length) { - delete userConfig.logs; - } - - const context = await createClient(userConfig); - if ( - !context[0] || - !context[0].config || - !context[0].config.input || - !context[0].config.input.some( - (input) => input.watch && input.watch.enabled, - ) - ) { - process.exit(0); - } - } catch { - process.exit(1); - } -} - -start(); diff --git a/packages/openapi-ts/src/cli.ts b/packages/openapi-ts/src/cli.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/cli.ts @@ -0,0 +1,142 @@ +#!/usr/bin/env node + +'use strict'; + +import { program } from 'commander'; + +import { createClient } from '~/index'; + +import pkg from '../package.json' assert { type: 'json' }; + +const params = program + .name(Object.keys(pkg.bin)[0]!) + .usage('[options]') + .version(pkg.version) + .option( + '-c, --client ', + 'HTTP client to generate [@hey-api/client-axios, @hey-api/client-fetch, @hey-api/client-next, @hey-api/client-nuxt, legacy/angular, legacy/axios, legacy/fetch, legacy/node, legacy/xhr]', + ) + .option('-d, --debug', 'Set log level to debug') + .option('--dry-run [value]', 'Skip writing files to disk?') + .option( + '-e, --experimental-parser [value]', + 'Opt-in to the experimental parser?', + ) + .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( + '--base [value]', + 'DEPRECATED. Manually set base in OpenAPI config instead of inferring from server value', + ) + .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?', + ) + .option('--exportCore [value]', 'DEPRECATED. Write core files to disk') + .option('--name ', 'DEPRECATED. Custom client class name') + .option('--request ', 'DEPRECATED. Path to custom request file') + .option( + '--useOptions [value]', + 'DEPRECATED. Use options instead of arguments?', + ) + .parse(process.argv) + .opts(); + +const stringToBoolean = (value: any) => { + if (value === 'true') { + return true; + } + if (value === 'false') { + return false; + } + return value; +}; + +const processParams = (obj: any, booleanKeys: any) => { + for (const key of booleanKeys) { + const value = obj[key]; + if (typeof value === 'string') { + const parsedValue = stringToBoolean(value); + delete obj[key]; + obj[key] = parsedValue; + } + } + if (obj.file) { + obj.configFile = obj.file; + } + return obj; +}; + +async function start() { + let userConfig; + + try { + userConfig = processParams(params, [ + 'dryRun', + 'logFile', + 'experimentalParser', + 'exportCore', + 'useOptions', + ]); + + 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.push(userConfig.client); + delete userConfig.client; + } + + userConfig.logs = userConfig.logs + ? { + path: userConfig.logs, + } + : {}; + + if (userConfig.debug || stringToBoolean(process.env.DEBUG)) { + userConfig.logs.level = 'debug'; + } else if (userConfig.silent) { + userConfig.logs.level = 'silent'; + } + + userConfig.logs.file = userConfig.logFile; + delete userConfig.logFile; + + if (typeof params.watch === 'string') { + userConfig.watch = Number.parseInt(params.watch, 10); + } + + if (!Object.keys(userConfig.logs).length) { + delete userConfig.logs; + } + + const context = await createClient(userConfig); + if ( + !context[0]?.config.input.some( + (input) => input.watch && input.watch.enabled, + ) + ) { + process.exit(0); + } + } catch { + process.exit(1); + } +} + +start(); diff --git a/packages/openapi-ts-tests/main/test/openapi-ts.config.ts b/packages/openapi-ts-tests/main/test/openapi-ts.config.ts deleted file mode 100644 --- a/packages/openapi-ts-tests/main/test/openapi-ts.config.ts +++ /dev/null @@ -1,484 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import path from 'node:path'; - -// @ts-ignore -import { customClientPlugin } from '@hey-api/custom-client/plugin'; -// @ts-ignore -import { defineConfig, utils } from '@hey-api/openapi-ts'; - -import { getSpecsPath } from '../../utils'; -// @ts-ignore -import { myClientPlugin } from './custom/client/plugin'; - -// @ts-ignore -// eslint-disable-next-line arrow-body-style -export default defineConfig(() => { - // ... - 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', - '3.1.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.json', - // '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, - // }, - }, - // 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', - }, - // 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', - // ), - // }, - 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'], - // }, - }, - hooks: { - events: { - // 'plugin:handler:after': ({ plugin }) => { - // console.log(`(${plugin.name}): handler finished`); - // }, - // 'plugin:handler:before': ({ plugin }) => { - // console.log(`(${plugin.name}): handler starting`); - // }, - // 'symbol:register:after': ({ plugin, symbol }) => { - // console.log(`(global, ${plugin.name}) registered:`, symbol.id); - // }, - 'symbol:register:before': ({ plugin, symbol }) => { - // if (!symbol.external && !symbol.meta?.resourceType) { - // console.log(`[${plugin.name}]:`, symbol.name); - // } - if (!symbol.external && !symbol.meta?.path) { - console.log(`[${plugin.name}]:`, symbol.name, symbol.meta); - } - // if (symbol.meta?.tags && symbol.meta?.tags.size > 0) { - // console.log( - // `[${plugin.name}]:`, - // symbol.name, - // symbol.meta.path, - // symbol.meta.tags, - // ); - // } - }, - // 'symbol:setValue:after': ({ plugin, symbol }) => { - // console.log(`(${plugin.name}) set value:`, symbol.id); - // }, - // 'symbol:setValue:before': ({ plugin, symbol }) => { - // console.log(`(${plugin.name}) setting value:`, symbol.id); - // }, - }, - 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; - // }, - }, - }, - 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}}', - // }, - }, - // 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, - // getSignature: ({ fields, signature, operation }) => { - // // ... - // fields.unwrap('path') - // }, - // include... - // instance: true, - name: '@hey-api/sdk', - // operationId: false, - // params_EXPERIMENTAL: 'experiment', - // responseStyle: 'data', - // signature: 'auto', - // signature: 'client', - // signature: 'object', - // transformer: '@hey-api/transformers', - // transformer: true, - validator: { - request: 'zod', - 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', - }, - { - // 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 - }, - }, - }, - }, - { - // name: 'arktype', - // types: { - // infer: true, - // }, - }, - { - // 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': { - events: { - // 'symbol:register:after': ({ plugin, symbol }) => { - // console.log(`(${plugin.name}) registered:`, symbol.id); - // }, - // 'symbol:register:before': ({ plugin, symbol }) => { - // console.log(`(${plugin.name}):`, symbol.name); - // }, - }, - symbols: { - // getFilePath: (symbol) => { - // if (symbol.name) { - // return utils.stringCase({ - // case: 'camelCase', - // value: symbol.name, - // }); - // } - // return; - // }, - }, - }, - }, - { - // case: 'snake_case', - // comments: false, - compatibilityVersion: 4, - 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 '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, - }, - // name: '@angular/common', - }, - { - 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, - }, - // { - // input: 'scalar:@scalar/access-service', - // logs: { - // // level: 'debug', - // path: './logs', - // }, - // output: path.resolve(__dirname, 'generated', 'sample'), - // }, - ]; -}); diff --git a/packages/openapi-ts/src/ir/context.ts b/packages/openapi-ts/src/ir/context.ts --- a/packages/openapi-ts/src/ir/context.ts +++ b/packages/openapi-ts/src/ir/context.ts @@ -14,7 +14,7 @@ import type { IR } from './types'; -export class IRContext = any> { +export class Context = any> { /** * Configuration for parsing and generating the output. This * is a mix of user-provided and default values. diff --git a/packages/openapi-ts/src/ir/types.d.ts b/packages/openapi-ts/src/ir/types.d.ts --- a/packages/openapi-ts/src/ir/types.d.ts +++ b/packages/openapi-ts/src/ir/types.d.ts @@ -4,7 +4,7 @@ ServerObject, } from '~/openApi/3.1.x/types/spec'; -import type { IRContext } from './context'; +import type { Context as IRContext } from './context'; import type { IRMediaType } from './mediaType'; interface IRBodyObject { diff --git a/packages/openapi-ts/src/openApi/index.ts b/packages/openapi-ts/src/openApi/index.ts --- a/packages/openapi-ts/src/openApi/index.ts +++ b/packages/openapi-ts/src/openApi/index.ts @@ -1,5 +1,5 @@ import { satisfies } from '~/config/utils/package'; -import { IRContext } from '~/ir/context'; +import { Context } from '~/ir/context'; import type { IR } from '~/ir/types'; import { parseV2_0_X } from '~/openApi/2.0.x'; import { parseV3_0_X } from '~/openApi/3.0.x'; @@ -74,7 +74,7 @@ logger: Logger; spec: unknown; }): IR.Context | undefined => { - const context = new IRContext({ + const context = new Context({ config, dependencies, logger, diff --git a/packages/openapi-ts/src/openApi/shared/utils/graph.ts b/packages/openapi-ts/src/openApi/shared/utils/graph.ts --- a/packages/openapi-ts/src/openApi/shared/utils/graph.ts +++ b/packages/openapi-ts/src/openApi/shared/utils/graph.ts @@ -503,10 +503,10 @@ eventBuildGraph.timeEnd(); - // functions creating data for debug scripts located in `debug-helpers/` + // functions creating data for debug scripts located in `dev/` // const { maxChildren, maxDepth, totalNodes } = debugTools.graph.analyzeStructure(graph); // const nodesForViz = debugTools.graph.exportForVisualization(graph); - // fs.writeFileSync('debug-helpers/graph.json', JSON.stringify(nodesForViz, null, 2)); + // fs.writeFileSync('dev/graph.json', JSON.stringify(nodesForViz, null, 2)); return { graph }; };