Repository: nestjs/swagger
Pull request #2544 (2023-07-31 10:26:48)

feat: add `customSwaggerUiPath`
## PR Checklist
Please check if your PR fulfills the following requirements:

- [x] The commit message follows our guidelines: https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md
- [x] Tests for the changes have been added (for bug fixes / features)
- [x] Docs have been added / updated (for bug fixes / features)


## PR Type
What kind of change does this PR introduce?

<!-- Please check the one that applies to this PR using "x". -->
- [ ] Bugfix
- [x] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Other... Please describe:

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->

Issue Number: https://github.com/nestjs/swagger/issues/2543


## What is the new behavior?
Adds the ability to override the default location `swagger-ui-dist`. This is useful when using, for example, `webpack`.

## Does this PR introduce a breaking change?
- [ ] Yes
- [x] No

<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->


## Other information
https://github.com/nestjs/docs.nestjs.com/pull/2813


Patch:
diff --git a/lib/interfaces/swagger-custom-options.interface.ts b/lib/interfaces/swagger-custom-options.interface.ts
index 584a96bb..a28e6d3a 100644
--- a/lib/interfaces/swagger-custom-options.interface.ts
+++ b/lib/interfaces/swagger-custom-options.interface.ts
@@ -11,6 +11,7 @@ export interface SwaggerCustomOptions {
   customJs?: string | string[];
   customJsStr?: string | string[];
   customfavIcon?: string;
+  customSwaggerUiPath?: string;
   swaggerUrl?: string;
   customSiteTitle?: string;
   validatorUrl?: string;
diff --git a/lib/swagger-module.ts b/lib/swagger-module.ts
index a04c9bfd..1d9b6dfb 100644
--- a/lib/swagger-module.ts
+++ b/lib/swagger-module.ts
@@ -20,6 +20,7 @@ import { getGlobalPrefix } from './utils/get-global-prefix';
 import { normalizeRelPath } from './utils/normalize-rel-path';
 import { validateGlobalPrefix } from './utils/validate-global-prefix.util';
 import { validatePath } from './utils/validate-path.util';
+import { resolvePath } from './utils/resolve-path.util';
 
 export class SwaggerModule {
   private static readonly metadataLoader = new MetadataLoader();
@@ -53,19 +54,23 @@ export class SwaggerModule {
     return this.metadataLoader.load(metadata);
   }
 
-  private static serveStatic(finalPath: string, app: INestApplication) {
+  private static serveStatic(finalPath: string, app: INestApplication, customStaticPath?: string) {
     const httpAdapter = app.getHttpAdapter();
-    const swaggerAssetsAbsoluteFSPath = getSwaggerAssetsAbsoluteFSPath();
+
+    // See <https://github.com/nestjs/swagger/issues/2543>
+    const swaggerAssetsPath = customStaticPath
+      ? resolvePath(customStaticPath)
+      : getSwaggerAssetsAbsoluteFSPath();
 
     if (httpAdapter && httpAdapter.getType() === 'fastify') {
       (app as NestFastifyApplication).useStaticAssets({
-        root: swaggerAssetsAbsoluteFSPath,
+        root: swaggerAssetsPath,
         prefix: finalPath,
         decorateReply: false
       });
     } else {
       (app as NestExpressApplication).useStaticAssets(
-        swaggerAssetsAbsoluteFSPath,
+        swaggerAssetsPath,
         { prefix: finalPath }
       );
     }
@@ -264,7 +269,6 @@ export class SwaggerModule {
         : path
     );
     const urlLastSubdirectory = finalPath.split('/').slice(-1).pop() || '';
-
     const validatedGlobalPrefix =
       options?.useGlobalPrefix && validateGlobalPrefix(globalPrefix)
         ? validatePath(globalPrefix)
@@ -292,7 +296,7 @@ export class SwaggerModule {
       }
     );
 
-    SwaggerModule.serveStatic(finalPath, app);
+    SwaggerModule.serveStatic(finalPath, app, options?.customSwaggerUiPath);
     /**
      * Covers assets fetched through a relative path when Swagger url ends with a slash '/'.
      * @see https://github.com/nestjs/swagger/issues/1976
diff --git a/lib/utils/resolve-path.util.ts b/lib/utils/resolve-path.util.ts
new file mode 100644
index 00000000..c8815a9d
--- /dev/null
+++ b/lib/utils/resolve-path.util.ts
@@ -0,0 +1,5 @@
+import * as pathLib from 'path';
+
+export function resolvePath(path: string): string {
+  return path ? pathLib.resolve(path) : path;
+}
\ No newline at end of file


Test patch:
diff --git a/e2e/express.e2e-spec.ts b/e2e/express.e2e-spec.ts
index 7194de81..e2c6c1c1 100644
--- a/e2e/express.e2e-spec.ts
+++ b/e2e/express.e2e-spec.ts
@@ -7,6 +7,7 @@ import * as request from 'supertest';
 import * as SwaggerParser from 'swagger-parser';
 import { DocumentBuilder, SwaggerModule } from '../lib';
 import { ApplicationModule } from './src/app.module';
+import * as path from 'path';
 
 describe('Express Swagger', () => {
   let app: NestExpressApplication;
@@ -83,7 +84,10 @@ describe('Express Swagger', () => {
         app,
         builder.build()
       );
-      SwaggerModule.setup(SWAGGER_RELATIVE_URL, app, swaggerDocument);
+      SwaggerModule.setup(SWAGGER_RELATIVE_URL, app, swaggerDocument, {
+        // to showcase that in new implementation u can use custom swagger-ui path. Useful when using e.g. webpack
+        customSwaggerUiPath: path.resolve(`./node_modules/swagger-ui-dist`),
+      });
 
       await app.init();
     });
@@ -100,6 +104,14 @@ describe('Express Swagger', () => {
       expect(response.status).toEqual(200);
       expect(Object.keys(response.body).length).toBeGreaterThan(0);
     });
+
+    it('content type of served static should be available', async () => {
+      const response = await request(app.getHttpServer()).get(
+        `${SWAGGER_RELATIVE_URL}/swagger-ui-bundle.js`
+      );
+
+      expect(response.status).toEqual(200);
+    });
   });
 
   describe('custom documents endpoints', () => {
diff --git a/e2e/fastify.e2e-spec.ts b/e2e/fastify.e2e-spec.ts
index 432d3eb0..237eb458 100644
--- a/e2e/fastify.e2e-spec.ts
+++ b/e2e/fastify.e2e-spec.ts
@@ -7,6 +7,7 @@ import * as request from 'supertest';
 import * as SwaggerParser from 'swagger-parser';
 import { DocumentBuilder, SwaggerModule } from '../lib';
 import { ApplicationModule } from './src/app.module';
+import * as path from 'path';
 
 describe('Fastify Swagger', () => {
   let app: NestFastifyApplication;
@@ -83,9 +84,12 @@ describe('Fastify Swagger', () => {
     beforeEach(async () => {
       const swaggerDocument = SwaggerModule.createDocument(
         app,
-        builder.build()
+        builder.build(),
       );
-      SwaggerModule.setup(SWAGGER_RELATIVE_URL, app, swaggerDocument);
+      SwaggerModule.setup(SWAGGER_RELATIVE_URL, app, swaggerDocument, {
+        // to showcase that in new implementation u can use custom swagger-ui path. Useful when using e.g. webpack
+        customSwaggerUiPath: path.resolve(`./node_modules/swagger-ui-dist`),
+      });
 
       await app.init();
       await app.getHttpAdapter().getInstance().ready();
@@ -103,6 +107,14 @@ describe('Fastify Swagger', () => {
       expect(response.status).toEqual(200);
       expect(Object.keys(response.body).length).toBeGreaterThan(0);
     });
+
+    it('content type of served static should be available', async () => {
+      const response = await request(app.getHttpServer()).get(
+        `${SWAGGER_RELATIVE_URL}/swagger-ui-bundle.js`
+      );
+
+      expect(response.status).toEqual(200);
+    });
   });
 
   describe('custom documents endpoints', () => {