Repository: nestjs/swagger
Pull request #3235 (2025-01-12 21:52:42)

feat: make generated require() ESM compatible
## PR Checklist
Please check if your PR fulfills the following requirements:

- [x] The commit message follows our guidelines: [NestJS Contribution Guide](https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md)
- [x] Tests have been added (for bug fixes and/or features)
- [x] Documentation has been added or updated (for bug fixes and/or 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 or API changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Other... Please describe:

## What is the current behavior?
NestJS does not officially support ESM. I have a project that successfully uses ESM with a custom fork, but in this Swagger plugin, the only incompatibility I found is that the generated code uses `require` without file extensions.

- Prior to Node.js 22, `require` could not import ES modules.
- As of Node.js 22, `require` can import ES modules, but missing file extensions continue to cause issues.

Issue Number: N/A

## What is the new behavior?
This PR adds the appropriate file extension in the generated code based on the input extension (.cts, .mts, or .ts), thereby improving compatibility with ESM.

## 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
I am a new NestJS user who has integrated it into an existing ESM-based project. Currently, I rely on a monkey-patch workaround, but I would prefer to avoid such hacky solutions. This PR addresses that concern by providing a more robust and maintainable approach.


Patch:
diff --git a/lib/plugin/utils/plugin-utils.ts b/lib/plugin/utils/plugin-utils.ts
index 7d1bd208..5465032a 100644
--- a/lib/plugin/utils/plugin-utils.ts
+++ b/lib/plugin/utils/plugin-utils.ts
@@ -137,6 +137,16 @@ export function hasPropertyKey(
     .some((item) => item.name.getText() === key);
 }
 
+function getOutputExtension(fileName: string): string {  
+  if (fileName.endsWith('.mts')) {
+    return '.mjs';
+  } else if (fileName.endsWith('.cts')) {
+    return '.cjs';
+  } else{
+    return ".js";
+  }
+}
+
 export function replaceImportPath(
   typeReference: string,
   fileName: string,
@@ -190,6 +200,10 @@ export function replaceImportPath(
       if (indexPos >= 0) {
         relativePath = relativePath.slice(0, indexPos);
       }
+    } else {
+      // Add appropriate extension for non-node_modules imports
+      const extension = getOutputExtension(fileName);
+      relativePath += extension;
     }
 
     typeReference = typeReference.replace(importPath, relativePath);
@@ -203,6 +217,7 @@ export function replaceImportPath(
         importPath: relativePath
       };
     }
+
     return {
       typeReference: typeReference.replace('import', 'require'),
       importPath: relativePath
diff --git a/package-lock.json b/package-lock.json
index 50528886..2bb96996 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,13 +27,13 @@
         "@types/jest": "29.5.14",
         "@types/js-yaml": "4.0.9",
         "@types/lodash": "4.17.14",
-        "@types/node": "22.10.6",
-        "@typescript-eslint/eslint-plugin": "8.20.0",
-        "@typescript-eslint/parser": "8.20.0",
+        "@types/node": "22.10.5",
+        "@typescript-eslint/eslint-plugin": "8.19.1",
+        "@typescript-eslint/parser": "8.19.1",
         "class-transformer": "0.5.1",
         "class-validator": "0.14.1",
         "eslint": "9.18.0",
-        "eslint-config-prettier": "10.0.1",
+        "eslint-config-prettier": "9.1.0",
         "eslint-plugin-import": "2.31.0",
         "express": "4.21.2",
         "husky": "9.1.7",
@@ -3235,9 +3235,9 @@
       "license": "MIT"
     },
     "node_modules/@types/node": {
-      "version": "22.10.6",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.6.tgz",
-      "integrity": "sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==",
+      "version": "22.10.5",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
+      "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -3279,17 +3279,17 @@
       "dev": true
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "8.20.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz",
-      "integrity": "sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==",
+      "version": "8.19.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.1.tgz",
+      "integrity": "sha512-tJzcVyvvb9h/PB96g30MpxACd9IrunT7GF9wfA9/0TJ1LxGOJx1TdPzSbBBnNED7K9Ka8ybJsnEpiXPktolTLg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@eslint-community/regexpp": "^4.10.0",
-        "@typescript-eslint/scope-manager": "8.20.0",
-        "@typescript-eslint/type-utils": "8.20.0",
-        "@typescript-eslint/utils": "8.20.0",
-        "@typescript-eslint/visitor-keys": "8.20.0",
+        "@typescript-eslint/scope-manager": "8.19.1",
+        "@typescript-eslint/type-utils": "8.19.1",
+        "@typescript-eslint/utils": "8.19.1",
+        "@typescript-eslint/visitor-keys": "8.19.1",
         "graphemer": "^1.4.0",
         "ignore": "^5.3.1",
         "natural-compare": "^1.4.0",
@@ -3309,16 +3309,16 @@
       }
     },
     "node_modules/@typescript-eslint/parser": {
-      "version": "8.20.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.20.0.tgz",
-      "integrity": "sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==",
+      "version": "8.19.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.1.tgz",
+      "integrity": "sha512-67gbfv8rAwawjYx3fYArwldTQKoYfezNUT4D5ioWetr/xCrxXxvleo3uuiFuKfejipvq+og7mjz3b0G2bVyUCw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/scope-manager": "8.20.0",
-        "@typescript-eslint/types": "8.20.0",
-        "@typescript-eslint/typescript-estree": "8.20.0",
-        "@typescript-eslint/visitor-keys": "8.20.0",
+        "@typescript-eslint/scope-manager": "8.19.1",
+        "@typescript-eslint/types": "8.19.1",
+        "@typescript-eslint/typescript-estree": "8.19.1",
+        "@typescript-eslint/visitor-keys": "8.19.1",
         "debug": "^4.3.4"
       },
       "engines": {
@@ -3334,14 +3334,14 @@
       }
     },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "8.20.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.20.0.tgz",
-      "integrity": "sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==",
+      "version": "8.19.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.1.tgz",
+      "integrity": "sha512-60L9KIuN/xgmsINzonOcMDSB8p82h95hoBfSBtXuO4jlR1R9L1xSkmVZKgCPVfavDlXihh4ARNjXhh1gGnLC7Q==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.20.0",
-        "@typescript-eslint/visitor-keys": "8.20.0"
+        "@typescript-eslint/types": "8.19.1",
+        "@typescript-eslint/visitor-keys": "8.19.1"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3352,14 +3352,14 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "8.20.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.20.0.tgz",
-      "integrity": "sha512-bPC+j71GGvA7rVNAHAtOjbVXbLN5PkwqMvy1cwGeaxUoRQXVuKCebRoLzm+IPW/NtFFpstn1ummSIasD5t60GA==",
+      "version": "8.19.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.1.tgz",
+      "integrity": "sha512-Rp7k9lhDKBMRJB/nM9Ksp1zs4796wVNyihG9/TU9R6KCJDNkQbc2EOKjrBtLYh3396ZdpXLtr/MkaSEmNMtykw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "8.20.0",
-        "@typescript-eslint/utils": "8.20.0",
+        "@typescript-eslint/typescript-estree": "8.19.1",
+        "@typescript-eslint/utils": "8.19.1",
         "debug": "^4.3.4",
         "ts-api-utils": "^2.0.0"
       },
@@ -3376,9 +3376,9 @@
       }
     },
     "node_modules/@typescript-eslint/types": {
-      "version": "8.20.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.20.0.tgz",
-      "integrity": "sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==",
+      "version": "8.19.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.1.tgz",
+      "integrity": "sha512-JBVHMLj7B1K1v1051ZaMMgLW4Q/jre5qGK0Ew6UgXz1Rqh+/xPzV1aW581OM00X6iOfyr1be+QyW8LOUf19BbA==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -3390,14 +3390,14 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "8.20.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.20.0.tgz",
-      "integrity": "sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==",
+      "version": "8.19.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.1.tgz",
+      "integrity": "sha512-jk/TZwSMJlxlNnqhy0Eod1PNEvCkpY6MXOXE/WLlblZ6ibb32i2We4uByoKPv1d0OD2xebDv4hbs3fm11SMw8Q==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.20.0",
-        "@typescript-eslint/visitor-keys": "8.20.0",
+        "@typescript-eslint/types": "8.19.1",
+        "@typescript-eslint/visitor-keys": "8.19.1",
         "debug": "^4.3.4",
         "fast-glob": "^3.3.2",
         "is-glob": "^4.0.3",
@@ -3417,16 +3417,16 @@
       }
     },
     "node_modules/@typescript-eslint/utils": {
-      "version": "8.20.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.20.0.tgz",
-      "integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==",
+      "version": "8.19.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.1.tgz",
+      "integrity": "sha512-IxG5gLO0Ne+KaUc8iW1A+XuKLd63o4wlbI1Zp692n1xojCl/THvgIKXJXBZixTh5dd5+yTJ/VXH7GJaaw21qXA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
-        "@typescript-eslint/scope-manager": "8.20.0",
-        "@typescript-eslint/types": "8.20.0",
-        "@typescript-eslint/typescript-estree": "8.20.0"
+        "@typescript-eslint/scope-manager": "8.19.1",
+        "@typescript-eslint/types": "8.19.1",
+        "@typescript-eslint/typescript-estree": "8.19.1"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3441,13 +3441,13 @@
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "8.20.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.20.0.tgz",
-      "integrity": "sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==",
+      "version": "8.19.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.1.tgz",
+      "integrity": "sha512-fzmjU8CHK853V/avYZAvuVut3ZTfwN5YtMaoi+X9Y9MA9keaWNHC3zEQ9zvyX/7Hj+5JkNyK1l7TOR2hevHB6Q==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.20.0",
+        "@typescript-eslint/types": "8.19.1",
         "eslint-visitor-keys": "^4.2.0"
       },
       "engines": {
@@ -5966,13 +5966,12 @@
       }
     },
     "node_modules/eslint-config-prettier": {
-      "version": "10.0.1",
-      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz",
-      "integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==",
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
+      "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
       "dev": true,
-      "license": "MIT",
       "bin": {
-        "eslint-config-prettier": "build/bin/cli.js"
+        "eslint-config-prettier": "bin/cli.js"
       },
       "peerDependencies": {
         "eslint": ">=7.0.0"
diff --git a/package.json b/package.json
index 758cd14d..8fd2fa9c 100644
--- a/package.json
+++ b/package.json
@@ -42,13 +42,13 @@
     "@types/jest": "29.5.14",
     "@types/js-yaml": "4.0.9",
     "@types/lodash": "4.17.14",
-    "@types/node": "22.10.6",
-    "@typescript-eslint/eslint-plugin": "8.20.0",
-    "@typescript-eslint/parser": "8.20.0",
+    "@types/node": "22.10.5",
+    "@typescript-eslint/eslint-plugin": "8.19.1",
+    "@typescript-eslint/parser": "8.19.1",
     "class-transformer": "0.5.1",
     "class-validator": "0.14.1",
     "eslint": "9.18.0",
-    "eslint-config-prettier": "10.0.1",
+    "eslint-config-prettier": "9.1.0",
     "eslint-plugin-import": "2.31.0",
     "express": "4.21.2",
     "husky": "9.1.7",


Test patch:
diff --git a/test/plugin/fixtures/parameter-property.dto.ts b/test/plugin/fixtures/parameter-property.dto.ts
index 3d8ef3f5..ade2c977 100644
--- a/test/plugin/fixtures/parameter-property.dto.ts
+++ b/test/plugin/fixtures/parameter-property.dto.ts
@@ -29,7 +29,7 @@ export class ParameterPropertyDto {
         this.protectedValue = protectedValue;
     }
     static _OPENAPI_METADATA_FACTORY() {
-        return { readonlyValue: { required: false, type: () => String }, privateValue: { required: true, type: () => String, nullable: true }, publicValue: { required: true, type: () => [require("./parameter-property.dto").ItemDto] }, protectedValue: { required: true, type: () => String, default: "1234" } };
+        return { readonlyValue: { required: false, type: () => String }, privateValue: { required: true, type: () => String, nullable: true }, publicValue: { required: true, type: () => [require("./parameter-property.dto.js").ItemDto] }, protectedValue: { required: true, type: () => String, default: "1234" } };
     }
 }
 export var LettersEnum;
@@ -43,7 +43,7 @@ export class ItemDto {
         this.enumValue = enumValue;
     }
     static _OPENAPI_METADATA_FACTORY() {
-        return { enumValue: { required: true, enum: require("./parameter-property.dto").LettersEnum } };
+        return { enumValue: { required: true, enum: require("./parameter-property.dto.js").LettersEnum } };
     }
 }
 `;
diff --git a/test/plugin/fixtures/serialized-meta.fixture.ts b/test/plugin/fixtures/serialized-meta.fixture.ts
index acb24e41..4943f798 100644
--- a/test/plugin/fixtures/serialized-meta.fixture.ts
+++ b/test/plugin/fixtures/serialized-meta.fixture.ts
@@ -1,12 +1,14 @@
 // @ts-nocheck
 export default async () => {
   const t = {
-    ['./cats/dto/pagination-query.dto']: await import(
-      './cats/dto/pagination-query.dto'
+    ['./cats/dto/pagination-query.dto.js']: await import(
+      './cats/dto/pagination-query.dto.js'
     ),
-    ['./cats/dto/create-cat.dto']: await import('./cats/dto/create-cat.dto'),
-    ['./cats/dto/tag.dto']: await import('./cats/dto/tag.dto'),
-    ['./cats/classes/cat.class']: await import('./cats/classes/cat.class')
+    ['./cats/dto/create-cat.dto.js']: await import(
+      './cats/dto/create-cat.dto.js'
+    ),
+    ['./cats/dto/tag.dto.js']: await import('./cats/dto/tag.dto.js'),
+    ['./cats/classes/cat.class.js']: await import('./cats/classes/cat.class.js')
   };
   return {
     '@nestjs/swagger': {
@@ -21,16 +23,16 @@ export default async () => {
               constrainedLimit: { required: false, type: () => Number },
               enum: {
                 required: true,
-                enum: t['./cats/dto/pagination-query.dto'].LettersEnum
+                enum: t['./cats/dto/pagination-query.dto.js'].LettersEnum
               },
               enumArr: {
                 required: true,
-                enum: t['./cats/dto/pagination-query.dto'].LettersEnum,
+                enum: t['./cats/dto/pagination-query.dto.js'].LettersEnum,
                 isArray: true
               },
               letters: {
                 required: true,
-                enum: t['./cats/dto/pagination-query.dto'].LettersEnum,
+                enum: t['./cats/dto/pagination-query.dto.js'].LettersEnum,
                 isArray: true
               },
               beforeDate: { required: true, type: () => Date },
@@ -60,11 +62,11 @@ export default async () => {
               options: { required: false, type: () => [Object] },
               enum: {
                 required: true,
-                enum: t['./cats/dto/pagination-query.dto'].LettersEnum
+                enum: t['./cats/dto/pagination-query.dto.js'].LettersEnum
               },
               enumArr: {
                 required: true,
-                enum: t['./cats/dto/pagination-query.dto'].LettersEnum
+                enum: t['./cats/dto/pagination-query.dto.js'].LettersEnum
               },
               uppercaseString: { required: true, type: () => String },
               lowercaseString: { required: true, type: () => String },
@@ -137,30 +139,30 @@ export default async () => {
               options: { required: false, type: () => [Object] },
               enum: {
                 required: true,
-                enum: t['./cats/dto/pagination-query.dto'].LettersEnum
+                enum: t['./cats/dto/pagination-query.dto.js'].LettersEnum
               },
               state: {
                 required: false,
                 description: 'Available language in the application',
                 example: 'FR',
-                enum: t['./cats/dto/create-cat.dto'].CategoryState
+                enum: t['./cats/dto/create-cat.dto.js'].CategoryState
               },
               enumArr: {
                 required: true,
-                enum: t['./cats/dto/pagination-query.dto'].LettersEnum
+                enum: t['./cats/dto/pagination-query.dto.js'].LettersEnum
               },
               enumArr2: {
                 required: true,
-                enum: t['./cats/dto/pagination-query.dto'].LettersEnum,
+                enum: t['./cats/dto/pagination-query.dto.js'].LettersEnum,
                 isArray: true
               },
               tag: {
                 required: true,
-                type: () => t['./cats/dto/tag.dto'].TagDto
+                type: () => t['./cats/dto/tag.dto.js'].TagDto
               },
               multipleTags: {
                 required: true,
-                type: () => [t['./cats/dto/tag.dto'].TagDto]
+                type: () => [t['./cats/dto/tag.dto.js'].TagDto]
               },
               nested: {
                 required: true,
@@ -197,11 +199,11 @@ export default async () => {
           import('./cats/cats.controller'),
           {
             CatsController: {
-              create: { type: t['./cats/classes/cat.class'].Cat },
-              findOne: { type: t['./cats/classes/cat.class'].Cat },
+              create: { type: t['./cats/classes/cat.class.js'].Cat },
+              findOne: { type: t['./cats/classes/cat.class.js'].Cat },
               findAll: {},
-              createBulk: { type: t['./cats/classes/cat.class'].Cat },
-              createAsFormData: { type: t['./cats/classes/cat.class'].Cat },
+              createBulk: { type: t['./cats/classes/cat.class.js'].Cat },
+              createAsFormData: { type: t['./cats/classes/cat.class.js'].Cat },
               getWithEnumParam: {},
               getWithRandomQuery: {}
             }