From f2a804b83675123e351718ef40e3b29981fe2112 Mon Sep 17 00:00:00 2001 From: Mac Mini Date: Fri, 12 Sep 2025 10:10:15 +0200 Subject: [PATCH 1/4] feat: add built-in hey-openapi generator plugin and core loader mapping to support @hey-api/openapi-ts --- packages/core/src/lib/plugin-loader.ts | 51 +++++---- packages/plugin-hey-openapi/README.md | 11 ++ packages/plugin-hey-openapi/eslint.config.js | 19 ++++ packages/plugin-hey-openapi/jest.config.ts | 10 ++ packages/plugin-hey-openapi/package.json | 15 +++ packages/plugin-hey-openapi/project.json | 42 ++++++++ packages/plugin-hey-openapi/src/index.ts | 3 + .../src/lib/hey-openapi-generator.spec.ts | 78 ++++++++++++++ .../src/lib/hey-openapi-generator.ts | 102 ++++++++++++++++++ .../src/types/openapi-ts.d.ts | 1 + packages/plugin-hey-openapi/tsconfig.json | 22 ++++ packages/plugin-hey-openapi/tsconfig.lib.json | 10 ++ .../plugin-hey-openapi/tsconfig.spec.json | 14 +++ 13 files changed, 360 insertions(+), 18 deletions(-) create mode 100644 packages/plugin-hey-openapi/README.md create mode 100644 packages/plugin-hey-openapi/eslint.config.js create mode 100644 packages/plugin-hey-openapi/jest.config.ts create mode 100644 packages/plugin-hey-openapi/package.json create mode 100644 packages/plugin-hey-openapi/project.json create mode 100644 packages/plugin-hey-openapi/src/index.ts create mode 100644 packages/plugin-hey-openapi/src/lib/hey-openapi-generator.spec.ts create mode 100644 packages/plugin-hey-openapi/src/lib/hey-openapi-generator.ts create mode 100644 packages/plugin-hey-openapi/src/types/openapi-ts.d.ts create mode 100644 packages/plugin-hey-openapi/tsconfig.json create mode 100644 packages/plugin-hey-openapi/tsconfig.lib.json create mode 100644 packages/plugin-hey-openapi/tsconfig.spec.json diff --git a/packages/core/src/lib/plugin-loader.ts b/packages/core/src/lib/plugin-loader.ts index 1e1164f..2ce5311 100644 --- a/packages/core/src/lib/plugin-loader.ts +++ b/packages/core/src/lib/plugin-loader.ts @@ -6,6 +6,7 @@ import { logger } from '@nx/devkit'; const BUILTIN_PLUGIN_MAP: Record = { 'openapi-tools': '@nx-plugin-openapi/plugin-openapi', + 'hey-openapi': '@nx-plugin-openapi/plugin-hey-openapi', }; const cache = new Map(); @@ -15,13 +16,13 @@ export async function loadPlugin( opts?: { root?: string } ): Promise { logger.debug(`Loading plugin: ${name}`); - + // Check registry first if (GeneratorRegistry.instance().has(name)) { logger.debug(`Plugin ${name} found in registry`); return GeneratorRegistry.instance().get(name); } - + // Check cache const cached = cache.get(name); if (cached) { @@ -31,9 +32,9 @@ export async function loadPlugin( const pkg = BUILTIN_PLUGIN_MAP[name] ?? name; const searchPaths: string[] = [pkg]; - + logger.debug(`Attempting to load plugin from package: ${pkg}`); - + try { const mod = (await import(pkg)) as { default?: unknown; @@ -45,7 +46,7 @@ export async function loadPlugin( logger.debug(`Successfully imported module: ${pkg}`); let candidate: unknown = undefined; - + // Try different export patterns if (isGeneratorPlugin(mod.default)) { logger.debug(`Found plugin as default export`); @@ -62,10 +63,16 @@ export async function loadPlugin( } if (!isGeneratorPlugin(candidate)) { - const availableExports = Object.keys(mod).filter(k => k !== '__esModule'); + const availableExports = Object.keys(mod).filter( + (k) => k !== '__esModule' + ); throw new PluginLoadError( name, - new Error(`Module does not export a valid plugin. Available exports: ${availableExports.join(', ')}`) + new Error( + `Module does not export a valid plugin. Available exports: ${availableExports.join( + ', ' + )}` + ) ); } @@ -74,25 +81,29 @@ export async function loadPlugin( return candidate; } catch (e) { logger.debug(`Failed to load plugin from primary path: ${e}`); - + // Fallback to workspace-relative resolution if provided const root = opts?.root ?? process.cwd(); - - if (pkg === '@nx-plugin-openapi/plugin-openapi') { + + if ( + pkg === '@nx-plugin-openapi/plugin-openapi' || + pkg === '@nx-plugin-openapi/plugin-hey-openapi' + ) { + const pkgName = pkg.split('/').pop()!; // e.g., 'plugin-openapi' or 'plugin-hey-openapi' const fallbackPaths = [ - `${root}/dist/packages/plugin-openapi/src/index.js`, - `${root}/packages/plugin-openapi/src/index.js` + `${root}/dist/packages/${pkgName}/src/index.js`, + `${root}/packages/${pkgName}/src/index.js`, ]; searchPaths.push(...fallbackPaths); - + logger.debug(`Trying fallback paths for built-in plugin`); - + for (const p of fallbackPaths) { try { logger.debug(`Attempting to load from: ${p}`); const url = `file://${p}`; const mod2 = (await import(url)) as { default?: unknown }; - + if (isGeneratorPlugin(mod2?.default)) { const plugin2 = mod2.default; logger.info(`Successfully loaded plugin from fallback path: ${p}`); @@ -108,12 +119,16 @@ export async function loadPlugin( // Determine appropriate error type const msg = String(e); const code = (e as Record)?.['code']; - + if (code === 'ERR_MODULE_NOT_FOUND' || /Cannot find module/.test(msg)) { - logger.error(`Plugin not found: ${name}. Searched paths: ${JSON.stringify(searchPaths)}`); + logger.error( + `Plugin not found: ${name}. Searched paths: ${JSON.stringify( + searchPaths + )}` + ); throw new PluginNotFoundError(name, searchPaths); } - + logger.error(`Failed to load plugin: ${name}. Error: ${e}`); throw new PluginLoadError(name, e); } diff --git a/packages/plugin-hey-openapi/README.md b/packages/plugin-hey-openapi/README.md new file mode 100644 index 0000000..35a66e3 --- /dev/null +++ b/packages/plugin-hey-openapi/README.md @@ -0,0 +1,11 @@ +# plugin-hey-openapi + +This library provides a generator plugin that uses `@hey-api/openapi-ts` to generate TypeScript clients from OpenAPI specs. + +## Building + +Run `nx build plugin-hey-openapi` to build the library. + +## Running unit tests + +Run `nx test plugin-hey-openapi` to execute the unit tests via Jest. diff --git a/packages/plugin-hey-openapi/eslint.config.js b/packages/plugin-hey-openapi/eslint.config.js new file mode 100644 index 0000000..9d2af7a --- /dev/null +++ b/packages/plugin-hey-openapi/eslint.config.js @@ -0,0 +1,19 @@ +const baseConfig = require('../../eslint.config.js'); + +module.exports = [ + ...baseConfig, + { + files: ['**/*.json'], + rules: { + '@nx/dependency-checks': [ + 'error', + { + ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs}'], + }, + ], + }, + languageOptions: { + parser: require('jsonc-eslint-parser'), + }, + }, +]; diff --git a/packages/plugin-hey-openapi/jest.config.ts b/packages/plugin-hey-openapi/jest.config.ts new file mode 100644 index 0000000..13248a6 --- /dev/null +++ b/packages/plugin-hey-openapi/jest.config.ts @@ -0,0 +1,10 @@ +export default { + displayName: 'plugin-hey-openapi', + preset: '../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/packages/plugin-hey-openapi', +}; diff --git a/packages/plugin-hey-openapi/package.json b/packages/plugin-hey-openapi/package.json new file mode 100644 index 0000000..5d68fe6 --- /dev/null +++ b/packages/plugin-hey-openapi/package.json @@ -0,0 +1,15 @@ +{ + "name": "@nx-plugin-openapi/plugin-hey-openapi", + "version": "0.0.1", + "dependencies": { + "@nx-plugin-openapi/core": "0.0.1", + "tslib": "^2.3.0", + "@nx/devkit": "19.8.14" + }, + "peerDependencies": { + "@nx-plugin-openapi/core": ">=0.0.1" + }, + "type": "commonjs", + "main": "./src/index.js", + "typings": "./src/index.d.ts" +} diff --git a/packages/plugin-hey-openapi/project.json b/packages/plugin-hey-openapi/project.json new file mode 100644 index 0000000..3b43fed --- /dev/null +++ b/packages/plugin-hey-openapi/project.json @@ -0,0 +1,42 @@ +{ + "name": "plugin-hey-openapi", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/plugin-hey-openapi/src", + "projectType": "library", + "release": { + "version": { + "generatorOptions": { + "packageRoot": "dist/{projectRoot}", + "currentVersionResolver": "git-tag" + } + } + }, + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/plugin-hey-openapi", + "main": "packages/plugin-hey-openapi/src/index.ts", + "tsConfig": "packages/plugin-hey-openapi/tsconfig.lib.json", + "assets": ["packages/plugin-hey-openapi/*.md"] + } + }, + "nx-release-publish": { + "options": { + "packageRoot": "dist/{projectRoot}" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "packages/plugin-hey-openapi/jest.config.ts" + } + } + } +} diff --git a/packages/plugin-hey-openapi/src/index.ts b/packages/plugin-hey-openapi/src/index.ts new file mode 100644 index 0000000..ddc8c87 --- /dev/null +++ b/packages/plugin-hey-openapi/src/index.ts @@ -0,0 +1,3 @@ +export { default as HeyOpenApiPlugin } from './lib/hey-openapi-generator'; +export { default } from './lib/hey-openapi-generator'; +export { HeyOpenApiGenerator } from './lib/hey-openapi-generator'; diff --git a/packages/plugin-hey-openapi/src/lib/hey-openapi-generator.spec.ts b/packages/plugin-hey-openapi/src/lib/hey-openapi-generator.spec.ts new file mode 100644 index 0000000..d33c82a --- /dev/null +++ b/packages/plugin-hey-openapi/src/lib/hey-openapi-generator.spec.ts @@ -0,0 +1,78 @@ +import { HeyOpenApiGenerator } from './hey-openapi-generator'; +import { GeneratorContext } from '@nx-plugin-openapi/core'; + +jest.mock('@hey-api/openapi-ts', () => ({ + generate: jest.fn(async () => undefined), +})); + +describe('HeyOpenApiGenerator', () => { + let generator: HeyOpenApiGenerator; + let mockContext: GeneratorContext; + let cleanOutputSpy: jest.SpyInstance; + + beforeEach(() => { + jest.clearAllMocks(); + generator = new HeyOpenApiGenerator(); + mockContext = { root: '/workspace', workspaceName: 'test' }; + // Spy on cleanOutput inherited from BaseGenerator + cleanOutputSpy = jest + .spyOn( + generator as unknown as { cleanOutput: (...a: unknown[]) => void }, + 'cleanOutput' + ) + .mockImplementation(() => {}); + }); + + it('should have correct plugin name', () => { + expect(generator.name).toBe('hey-openapi'); + }); + + it('should call openapi-ts generate for single spec', async () => { + const { generate } = (await import('@hey-api/openapi-ts')) as any; + + await generator.generate( + { + inputSpec: 'api.yaml', + outputPath: 'src/generated', + generatorOptions: { client: 'fetch' }, + } as any, + mockContext + ); + + expect(cleanOutputSpy).toHaveBeenCalledWith(mockContext, 'src/generated'); + expect(generate).toHaveBeenCalledWith( + expect.objectContaining({ + input: 'api.yaml', + output: '/workspace/src/generated', + client: 'fetch', + }) + ); + }); + + it('should call openapi-ts generate for multiple specs', async () => { + const { generate } = (await import('@hey-api/openapi-ts')) as any; + + await generator.generate( + { + inputSpec: { users: 'users.yaml', products: 'products.yaml' }, + outputPath: 'src/api', + } as any, + mockContext + ); + + expect(cleanOutputSpy).toHaveBeenCalledTimes(2); + expect(generate).toHaveBeenCalledTimes(2); + expect(generate).toHaveBeenCalledWith( + expect.objectContaining({ + input: 'users.yaml', + output: '/workspace/src/api/users', + }) + ); + expect(generate).toHaveBeenCalledWith( + expect.objectContaining({ + input: 'products.yaml', + output: '/workspace/src/api/products', + }) + ); + }); +}); diff --git a/packages/plugin-hey-openapi/src/lib/hey-openapi-generator.ts b/packages/plugin-hey-openapi/src/lib/hey-openapi-generator.ts new file mode 100644 index 0000000..306d3fe --- /dev/null +++ b/packages/plugin-hey-openapi/src/lib/hey-openapi-generator.ts @@ -0,0 +1,102 @@ +import { join } from 'node:path'; +import { logger } from '@nx/devkit'; +import { + BaseGenerator, + GeneratorContext, + GeneratorPlugin, + GenerateOptionsBase, +} from '@nx-plugin-openapi/core'; + +export interface HeyOpenApiOptions { + [key: string]: unknown; +} + +export class HeyOpenApiGenerator + extends BaseGenerator + implements GeneratorPlugin +{ + readonly name = 'hey-openapi'; + + async generate( + options: HeyOpenApiOptions & GenerateOptionsBase, + ctx: GeneratorContext + ): Promise { + const { inputSpec, outputPath } = options; + const generatorOptions = (options.generatorOptions || + {}) as Partial; + + logger.info(`Starting hey-openapi code generation`); + logger.debug(`Input spec: ${JSON.stringify(inputSpec)}`); + logger.debug(`Output path: ${outputPath}`); + + if (typeof inputSpec === 'string') { + this.cleanOutput(ctx, outputPath); + await this.invokeOpenApiTs( + { + input: inputSpec, + output: join(ctx.root, outputPath), + ...generatorOptions, + }, + ctx + ); + } else { + const entries = Object.entries(inputSpec as Record) as [ + string, + string + ][]; + + logger.info(`Generating code for ${entries.length} services`); + + for (const [serviceName, specPath] of entries) { + logger.info(`Generating service: ${serviceName}`); + const serviceOutputPath = join(outputPath, serviceName); + this.cleanOutput(ctx, serviceOutputPath); + await this.invokeOpenApiTs( + { + input: specPath, + output: join(ctx.root, serviceOutputPath), + ...generatorOptions, + }, + ctx + ); + } + } + + logger.info(`hey-openapi code generation completed successfully`); + } + + private async invokeOpenApiTs( + config: { input: string; output: string } & Record, + _ctx: GeneratorContext + ): Promise { + let mod: Record; + try { + mod = (await import('@hey-api/openapi-ts')) as Record; + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + throw new Error( + `@hey-api/openapi-ts is required but not installed. Install it in your workspace devDependencies. Original error: ${msg}` + ); + } + + const fn = + typeof (mod as any).generate === 'function' + ? ((mod as any).generate as (cfg: unknown) => Promise) + : typeof (mod as any).createClient === 'function' + ? ((mod as any).createClient as (cfg: unknown) => Promise) + : undefined; + + if (!fn) { + const keys = Object.keys(mod).filter((k) => k !== '__esModule'); + throw new Error( + `@hey-api/openapi-ts does not export a supported API. Expected 'generate' or 'createClient'. Available: ${keys.join( + ', ' + )}` + ); + } + + await fn(config); + } +} + +export default new HeyOpenApiGenerator(); diff --git a/packages/plugin-hey-openapi/src/types/openapi-ts.d.ts b/packages/plugin-hey-openapi/src/types/openapi-ts.d.ts new file mode 100644 index 0000000..907ab56 --- /dev/null +++ b/packages/plugin-hey-openapi/src/types/openapi-ts.d.ts @@ -0,0 +1 @@ +declare module '@hey-api/openapi-ts'; diff --git a/packages/plugin-hey-openapi/tsconfig.json b/packages/plugin-hey-openapi/tsconfig.json new file mode 100644 index 0000000..f5b8565 --- /dev/null +++ b/packages/plugin-hey-openapi/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/plugin-hey-openapi/tsconfig.lib.json b/packages/plugin-hey-openapi/tsconfig.lib.json new file mode 100644 index 0000000..33eca2c --- /dev/null +++ b/packages/plugin-hey-openapi/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/packages/plugin-hey-openapi/tsconfig.spec.json b/packages/plugin-hey-openapi/tsconfig.spec.json new file mode 100644 index 0000000..9b2a121 --- /dev/null +++ b/packages/plugin-hey-openapi/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} From c639cb0f161d9c09df942ba715fd02ee13006db0 Mon Sep 17 00:00:00 2001 From: Mac Mini Date: Fri, 12 Sep 2025 10:13:48 +0200 Subject: [PATCH 2/4] chore(hey-openapi): make generator and tests lint-compliant and rely on virtual mock for @hey-api/openapi-ts --- .../src/lib/hey-openapi-generator.spec.ts | 30 ++++++++----- .../src/lib/hey-openapi-generator.ts | 44 +++++++++---------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/packages/plugin-hey-openapi/src/lib/hey-openapi-generator.spec.ts b/packages/plugin-hey-openapi/src/lib/hey-openapi-generator.spec.ts index d33c82a..1308764 100644 --- a/packages/plugin-hey-openapi/src/lib/hey-openapi-generator.spec.ts +++ b/packages/plugin-hey-openapi/src/lib/hey-openapi-generator.spec.ts @@ -1,9 +1,13 @@ import { HeyOpenApiGenerator } from './hey-openapi-generator'; import { GeneratorContext } from '@nx-plugin-openapi/core'; -jest.mock('@hey-api/openapi-ts', () => ({ - generate: jest.fn(async () => undefined), -})); +jest.mock( + '@hey-api/openapi-ts', + () => ({ + generate: jest.fn(async () => undefined), + }), + { virtual: true } +); describe('HeyOpenApiGenerator', () => { let generator: HeyOpenApiGenerator; @@ -28,19 +32,21 @@ describe('HeyOpenApiGenerator', () => { }); it('should call openapi-ts generate for single spec', async () => { - const { generate } = (await import('@hey-api/openapi-ts')) as any; + const mod = (await import('@hey-api/openapi-ts')) as unknown as { + generate: jest.Mock; + }; await generator.generate( { inputSpec: 'api.yaml', outputPath: 'src/generated', generatorOptions: { client: 'fetch' }, - } as any, + } as unknown as Parameters[0], mockContext ); expect(cleanOutputSpy).toHaveBeenCalledWith(mockContext, 'src/generated'); - expect(generate).toHaveBeenCalledWith( + expect(mod.generate).toHaveBeenCalledWith( expect.objectContaining({ input: 'api.yaml', output: '/workspace/src/generated', @@ -50,25 +56,27 @@ describe('HeyOpenApiGenerator', () => { }); it('should call openapi-ts generate for multiple specs', async () => { - const { generate } = (await import('@hey-api/openapi-ts')) as any; + const mod = (await import('@hey-api/openapi-ts')) as unknown as { + generate: jest.Mock; + }; await generator.generate( { inputSpec: { users: 'users.yaml', products: 'products.yaml' }, outputPath: 'src/api', - } as any, + } as unknown as Parameters[0], mockContext ); expect(cleanOutputSpy).toHaveBeenCalledTimes(2); - expect(generate).toHaveBeenCalledTimes(2); - expect(generate).toHaveBeenCalledWith( + expect(mod.generate).toHaveBeenCalledTimes(2); + expect(mod.generate).toHaveBeenCalledWith( expect.objectContaining({ input: 'users.yaml', output: '/workspace/src/api/users', }) ); - expect(generate).toHaveBeenCalledWith( + expect(mod.generate).toHaveBeenCalledWith( expect.objectContaining({ input: 'products.yaml', output: '/workspace/src/api/products', diff --git a/packages/plugin-hey-openapi/src/lib/hey-openapi-generator.ts b/packages/plugin-hey-openapi/src/lib/hey-openapi-generator.ts index 306d3fe..04ff3d0 100644 --- a/packages/plugin-hey-openapi/src/lib/hey-openapi-generator.ts +++ b/packages/plugin-hey-openapi/src/lib/hey-openapi-generator.ts @@ -31,14 +31,11 @@ export class HeyOpenApiGenerator if (typeof inputSpec === 'string') { this.cleanOutput(ctx, outputPath); - await this.invokeOpenApiTs( - { - input: inputSpec, - output: join(ctx.root, outputPath), - ...generatorOptions, - }, - ctx - ); + await this.invokeOpenApiTs({ + input: inputSpec, + output: join(ctx.root, outputPath), + ...generatorOptions, + }); } else { const entries = Object.entries(inputSpec as Record) as [ string, @@ -51,14 +48,11 @@ export class HeyOpenApiGenerator logger.info(`Generating service: ${serviceName}`); const serviceOutputPath = join(outputPath, serviceName); this.cleanOutput(ctx, serviceOutputPath); - await this.invokeOpenApiTs( - { - input: specPath, - output: join(ctx.root, serviceOutputPath), - ...generatorOptions, - }, - ctx - ); + await this.invokeOpenApiTs({ + input: specPath, + output: join(ctx.root, serviceOutputPath), + ...generatorOptions, + }); } } @@ -66,8 +60,7 @@ export class HeyOpenApiGenerator } private async invokeOpenApiTs( - config: { input: string; output: string } & Record, - _ctx: GeneratorContext + config: { input: string; output: string } & Record ): Promise { let mod: Record; try { @@ -79,11 +72,16 @@ export class HeyOpenApiGenerator ); } + const generateExport = (mod as Record)['generate']; + const createClientExport = (mod as Record)['createClient']; + const fn = - typeof (mod as any).generate === 'function' - ? ((mod as any).generate as (cfg: unknown) => Promise) - : typeof (mod as any).createClient === 'function' - ? ((mod as any).createClient as (cfg: unknown) => Promise) + typeof generateExport === 'function' + ? (generateExport as (cfg: Record) => Promise) + : typeof createClientExport === 'function' + ? (createClientExport as ( + cfg: Record + ) => Promise) : undefined; if (!fn) { @@ -95,7 +93,7 @@ export class HeyOpenApiGenerator ); } - await fn(config); + await fn(config as Record); } } From d1e4b1d5567b2d2c5aaa09fc11cb866f9dc14a19 Mon Sep 17 00:00:00 2001 From: Mac Mini Date: Fri, 12 Sep 2025 10:57:27 +0200 Subject: [PATCH 3/4] fix(core): make built-in plugin fallback robust and use it for demo generation via hey-openapi\n\n- Correct fallback path to dist src/index.js and support CJS require + file URL import\n- Switch demo target to 'hey-openapi' with a valid client option\n- Add @hey-api/openapi-ts to workspace devDependencies to enable runtime codegen --- apps/demo/project.json | 3 +- package-lock.json | 336 +++++++++++++++++++++++++ package.json | 1 + packages/core/src/lib/plugin-loader.ts | 12 +- 4 files changed, 349 insertions(+), 3 deletions(-) diff --git a/apps/demo/project.json b/apps/demo/project.json index f03474b..34085d8 100644 --- a/apps/demo/project.json +++ b/apps/demo/project.json @@ -30,10 +30,11 @@ "generate-api-with-core": { "executor": "@nx-plugin-openapi/core:generate-api", "options": { + "generator": "hey-openapi", "inputSpec": "apps/demo/swagger.json", "outputPath": "apps/demo/src/app/api-core", "generatorOptions": { - "skipValidateSpec": true + "client": "fetch" } }, "outputs": ["{options.outputPath}"] diff --git a/package-lock.json b/package-lock.json index 359c533..1fd1304 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "@angular/language-service": "~18.2.0", "@astrojs/starlight": "^0.34.4", "@eslint/js": "^9.8.0", + "@hey-api/openapi-ts": "^0.83.1", "@nx/angular": "19.8.14", "@nx/eslint": "19.8.14", "@nx/eslint-plugin": "19.8.14", @@ -3726,6 +3727,113 @@ "@expressive-code/core": "^0.41.3" } }, + "node_modules/@hey-api/codegen-core": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.1.0.tgz", + "integrity": "sha512-7B6B9Zuw4uQWoG5YwAwNC+UTz6ErAujqaQrJuMu/OgPkUHq+hl6zdKKi2MX/399vPYilo0IGBxQHmY1UfmTHsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=22.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + }, + "peerDependencies": { + "typescript": ">=5.5.3" + } + }, + "node_modules/@hey-api/json-schema-ref-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.1.0.tgz", + "integrity": "sha512-+5eg9pgAAM9oSqJQuUtfTKbLz8yieFKN91myyXiLnprqFj8ROfxUKJLr9DKq/hGKyybKT1WxFSetDqCFm80pCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + } + }, + "node_modules/@hey-api/openapi-ts": { + "version": "0.83.1", + "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.83.1.tgz", + "integrity": "sha512-AJIXDsXBLXS4uzhU+yVeNmD43uN6FYPcWpcid7Q0bA4JRXpI+6livOzUUMiuszt3NlgHy1gcf6x15W2xHNSoag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hey-api/codegen-core": "^0.1.0", + "@hey-api/json-schema-ref-parser": "1.1.0", + "ansi-colors": "4.1.3", + "c12": "2.0.1", + "color-support": "1.1.3", + "commander": "13.0.0", + "handlebars": "4.7.8", + "open": "10.1.2", + "semver": "7.7.2" + }, + "bin": { + "openapi-ts": "bin/index.cjs" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=22.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + }, + "peerDependencies": { + "typescript": ">=5.5.3" + } + }, + "node_modules/@hey-api/openapi-ts/node_modules/commander": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.0.0.tgz", + "integrity": "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@hey-api/openapi-ts/node_modules/open": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", + "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@hey-api/openapi-ts/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -4966,6 +5074,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true, + "license": "MIT" + }, "node_modules/@jsonjoy.com/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", @@ -12393,6 +12508,82 @@ "node": ">= 0.8" } }, + "node_modules/c12": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/c12/-/c12-2.0.1.tgz", + "integrity": "sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.1", + "confbox": "^0.1.7", + "defu": "^6.1.4", + "dotenv": "^16.4.5", + "giget": "^1.2.3", + "jiti": "^2.3.0", + "mlly": "^1.7.1", + "ohash": "^1.1.4", + "pathe": "^1.1.2", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/c12/node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/c12/node_modules/ohash": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.6.tgz", + "integrity": "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg==", + "dev": true, + "license": "MIT" + }, + "node_modules/c12/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/cacache": { "version": "18.0.4", "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", @@ -12733,6 +12924,16 @@ "node": ">=8" } }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/cjs-module-lexer": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", @@ -12998,6 +13199,16 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/colord": { "version": "2.9.3", "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", @@ -13176,6 +13387,13 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -16361,6 +16579,32 @@ "assert-plus": "^1.0.0" } }, + "node_modules/giget": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.5.tgz", + "integrity": "sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.5.4", + "pathe": "^2.0.3", + "tar": "^6.2.1" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/giget/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -21937,6 +22181,26 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", @@ -22748,6 +23012,34 @@ "node": ">=8" } }, + "node_modules/nypm": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.5.4.tgz", + "integrity": "sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "tinyexec": "^0.3.2", + "ufo": "^1.5.4" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/nypm/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -23493,6 +23785,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, "node_modules/peek-readable": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.4.2.tgz", @@ -23517,6 +23816,13 @@ "through2": "^2.0.3" } }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -23663,6 +23969,25 @@ "node": ">=8" } }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pkginfo": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.4.1.tgz", @@ -24798,6 +25123,17 @@ "node": ">=0.10.0" } }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/react": { "version": "19.1.1", "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", diff --git a/package.json b/package.json index 17d6161..999812c 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@angular/language-service": "~18.2.0", "@astrojs/starlight": "^0.34.4", "@eslint/js": "^9.8.0", + "@hey-api/openapi-ts": "^0.83.1", "@nx/angular": "19.8.14", "@nx/eslint": "19.8.14", "@nx/eslint-plugin": "19.8.14", diff --git a/packages/core/src/lib/plugin-loader.ts b/packages/core/src/lib/plugin-loader.ts index 2ce5311..a1affe3 100644 --- a/packages/core/src/lib/plugin-loader.ts +++ b/packages/core/src/lib/plugin-loader.ts @@ -101,8 +101,16 @@ export async function loadPlugin( for (const p of fallbackPaths) { try { logger.debug(`Attempting to load from: ${p}`); - const url = `file://${p}`; - const mod2 = (await import(url)) as { default?: unknown }; + let mod2: { default?: unknown } | undefined; + try { + // Prefer require for CJS outputs + // eslint-disable-next-line @typescript-eslint/no-var-requires + mod2 = require(p); + } catch { + const { pathToFileURL } = await import('node:url'); + const url = pathToFileURL(p).href; + mod2 = (await import(url)) as { default?: unknown }; + } if (isGeneratorPlugin(mod2?.default)) { const plugin2 = mod2.default; From ab1dab3c149808e55c5a35998f28acbe4d52d7ae Mon Sep 17 00:00:00 2001 From: Mac Mini Date: Fri, 12 Sep 2025 11:07:11 +0200 Subject: [PATCH 4/4] chore(core): satisfy lint by using file URL dynamic import for built-in plugin fallback (remove require) --- packages/core/src/lib/plugin-loader.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/core/src/lib/plugin-loader.ts b/packages/core/src/lib/plugin-loader.ts index a1affe3..6c81298 100644 --- a/packages/core/src/lib/plugin-loader.ts +++ b/packages/core/src/lib/plugin-loader.ts @@ -101,16 +101,9 @@ export async function loadPlugin( for (const p of fallbackPaths) { try { logger.debug(`Attempting to load from: ${p}`); - let mod2: { default?: unknown } | undefined; - try { - // Prefer require for CJS outputs - // eslint-disable-next-line @typescript-eslint/no-var-requires - mod2 = require(p); - } catch { - const { pathToFileURL } = await import('node:url'); - const url = pathToFileURL(p).href; - mod2 = (await import(url)) as { default?: unknown }; - } + const { pathToFileURL } = await import('node:url'); + const url = pathToFileURL(p).href; + const mod2 = (await import(url)) as { default?: unknown }; if (isGeneratorPlugin(mod2?.default)) { const plugin2 = mod2.default;