diff --git a/packages/http-server/src/HttpServerModule.ts b/packages/http-server/src/HttpServerModule.ts index e01bd819..06c51883 100644 --- a/packages/http-server/src/HttpServerModule.ts +++ b/packages/http-server/src/HttpServerModule.ts @@ -14,7 +14,6 @@ import { Module, omit } from '@davinci/core'; -import pathUtils from 'path'; import pino from 'pino'; import { ClassReflection, ClassType, DecoratorId, MethodReflection } from '@davinci/reflector'; import type { InjectOptions } from 'light-my-request'; @@ -130,7 +129,7 @@ export abstract class HttpServerModule< options: { path } } = methodDecoratorMetadata; - let fullPath = pathUtils.join(basePath, path); + let fullPath = this.joinPaths(basePath, path); if (fullPath.length > 1 && fullPath[fullPath.length - 1] === '/') { fullPath = fullPath.slice(0, -1); } @@ -565,6 +564,12 @@ export abstract class HttpServerModule< } } + private joinPaths(...args: Array) { + const path = args.join('/').replace(/\/+/g, '/'); // replace multiple slashes with one + + return path === '/' ? '/' : path.replace(/\/$/, ''); // remove trailing slash only if it's not '/' + } + /* abstract render(response, view: string, options: unknown); abstract useStaticAssets(...args: unknown[]); abstract setViewEngine(engine: string); diff --git a/packages/openapi/.mocharc.js b/packages/openapi/.mocharc.js index ec47c105..43d3d36b 100644 --- a/packages/openapi/.mocharc.js +++ b/packages/openapi/.mocharc.js @@ -14,5 +14,6 @@ module.exports = { bail: true, checkLeaks: true, require: ['ts-node/register', 'source-map-support/register'], - spec: 'test/**/*.{js,ts}' + spec: 'test/**/*.{js,ts}', + globals: ['EdgeRuntime'] }; diff --git a/packages/openapi/src/OpenAPIModule.ts b/packages/openapi/src/OpenAPIModule.ts index 17189f4f..4524b40b 100644 --- a/packages/openapi/src/OpenAPIModule.ts +++ b/packages/openapi/src/OpenAPIModule.ts @@ -20,9 +20,6 @@ import pino, { Level } from 'pino'; import { OpenAPIV3 } from 'openapi-types'; import createDeepMerge from '@fastify/deepmerge'; import { ClassType, PartialDeep, TypeValue } from '@davinci/reflector'; -import { promises as fs } from 'fs'; -import { join as joinPaths } from 'path'; -import * as process from 'process'; import { generateSwaggerUiHtml } from './swaggerUi'; const deepMerge = createDeepMerge(); @@ -133,19 +130,40 @@ export class OpenAPIModule extends Module { const relativeOutputPath = this.moduleOptions.document?.output?.path; if (relativeOutputPath) { + await this.writeOpenAPIDocument(relativeOutputPath); + } + } + + public getOpenAPIDocument(): PartialDeep { + return this.openAPIDoc; + } + + public async writeOpenAPIDocument(relativeOutputPath: string) { + /** + This block will be dead-code-eliminated on Edge + @see https://vercel.com/docs/functions/edge-functions/edge-runtime#check-if-you're-running-on-the-edge-runtime + */ + // @ts-expect-error + if (typeof EdgeRuntime === 'string') { + throw new Error('Write operation not available in Edge Runtime'); + } else { const stringifyOptions = this.moduleOptions.document?.output.stringifyOptions; const stringifiedDocument = JSON.stringify( this.openAPIDoc, stringifyOptions?.replacer, stringifyOptions?.space ); - const outputPath = joinPaths(process.cwd(), relativeOutputPath); + + const fs = await import('fs').then(m => m.promises); + const process = await import('process'); + + const outputPath = this.joinPaths(process.cwd(), relativeOutputPath); await fs.writeFile(outputPath, stringifiedDocument); this.logger.debug(`The OpenAPI document has been written to the local file system at path: ${outputPath}`); } } - async createPathAndSchema(route: Route): Promise { + private async createPathAndSchema(route: Route): Promise { const { path: origPath, verb, @@ -359,7 +377,7 @@ export class OpenAPIModule extends Module { }); } - async registerOpenapiRoutes() { + private async registerOpenapiRoutes() { const documentEnabled = this.moduleOptions.document?.enabled; const explorerEnabled = this.moduleOptions.explorer?.enabled; @@ -384,10 +402,6 @@ export class OpenAPIModule extends Module { } } - getOpenAPIDocument(): PartialDeep { - return this.openAPIDoc; - } - private createJsonSchema(entityJsonSchema: EntityDefinitionJSONSchema): Partial { return transformEntityDefinitionSchema(entityJsonSchema, args => { if (args.pointerPath === '') { @@ -458,4 +472,10 @@ export class OpenAPIModule extends Module { return jsonSchema; } + + private joinPaths(...args: Array) { + const path = args.join('/').replace(/\/+/g, '/'); // replace multiple slashes with one + + return path === '/' ? '/' : path.replace(/\/$/, ''); // remove trailing slash only if it's not '/' + } } diff --git a/packages/openapi/test/unit/OpenAPIModule.test.ts b/packages/openapi/test/unit/OpenAPIModule.test.ts index 77079572..a27f9476 100644 --- a/packages/openapi/test/unit/OpenAPIModule.test.ts +++ b/packages/openapi/test/unit/OpenAPIModule.test.ts @@ -127,7 +127,7 @@ describe('OpenAPIModule', () => { } } const openApiModule = new OpenAPIModule(openapiModuleOpts); - const app = new App({ logger: { level: 'error' } }); + const app = new App({ logger: { level: 'silent' } }); await app.registerController(CustomerController).registerModule(new FastifyHttpServer(), openApiModule); await app.init(); @@ -1149,5 +1149,26 @@ describe('OpenAPIModule', () => { expect(writeFileStub.args[0][0]).to.match(/path\/to\/local\/file\.json$/); expect(writeFileStub.args[0][1]).to.contain('"openapi": "3.0.0"'); }); + + it('should throw an error when trying to output in a Edge environment', async () => { + const origValue = globalThis.EdgeRuntime; + globalThis.EdgeRuntime = 'edge'; + + await expect( + initApp({ + document: { + output: { path: 'path/to/local/file.json', stringifyOptions: { space: 2 } }, + spec: { info: { title: '', version: '' } } + } + }) + ).to.rejectedWith('Write operation not available in Edge Runtime'); + + // eslint-disable-next-line require-atomic-updates + globalThis.EdgeRuntime = origValue; + + /* expect(writeFileStub.called).to.be.true; + expect(writeFileStub.args[0][0]).to.match(/path\/to\/local\/file\.json$/); + expect(writeFileStub.args[0][1]).to.contain('"openapi": "3.0.0"'); */ + }); }); }); diff --git a/packages/openapi/tsconfig.cjs.release.json b/packages/openapi/tsconfig.cjs.release.json index 4667d7a5..d5f1be4a 100644 --- a/packages/openapi/tsconfig.cjs.release.json +++ b/packages/openapi/tsconfig.cjs.release.json @@ -2,7 +2,6 @@ "extends": "../../tsconfig.json", "compilerOptions": { "rootDir": "./src", - "module": "commonjs", "outDir": "./build-cjs", "strictNullChecks": false }, diff --git a/packages/openapi/tsconfig.esm.release.json b/packages/openapi/tsconfig.esm.release.json index bc9670c3..bae0b10c 100644 --- a/packages/openapi/tsconfig.esm.release.json +++ b/packages/openapi/tsconfig.esm.release.json @@ -2,7 +2,7 @@ "extends": "../../tsconfig.json", "compilerOptions": { "rootDir": "./src", - "module": "ES6", + "module": "ES2020", "outDir": "./build-esm", "strictNullChecks": false },