Skip to content

Commit d9a9b19

Browse files
Merge pull request #3362 from CatsMiaow/fix/esm-compatible
feat: make generated require() ESM compatible
2 parents f7a68f1 + 050ff34 commit d9a9b19

11 files changed

+335
-18
lines changed

lib/plugin/merge-options.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface PluginOptions {
99
dtoKeyOfComment?: string;
1010
controllerKeyOfComment?: string;
1111
introspectComments?: boolean;
12+
esmCompatible?: boolean;
1213
readonly?: boolean;
1314
pathToSource?: string;
1415
debug?: boolean;
@@ -27,6 +28,7 @@ const defaultOptions: PluginOptions = {
2728
dtoKeyOfComment: 'description',
2829
controllerKeyOfComment: 'summary',
2930
introspectComments: false,
31+
esmCompatible: false,
3032
readonly: false,
3133
debug: false
3234
};

lib/plugin/utils/plugin-utils.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,16 @@ export function hasPropertyKey(
140140
.some((item) => item.name.getText() === key);
141141
}
142142

143+
export function getOutputExtension(fileName: string): string {
144+
if (fileName.endsWith('.mts')) {
145+
return '.mjs';
146+
} else if (fileName.endsWith('.cts')) {
147+
return '.cjs';
148+
} else {
149+
return '.js';
150+
}
151+
}
152+
143153
export function replaceImportPath(
144154
typeReference: string,
145155
fileName: string,
@@ -148,6 +158,14 @@ export function replaceImportPath(
148158
if (!typeReference.includes('import')) {
149159
return { typeReference, importPath: null };
150160
}
161+
162+
if (options.esmCompatible) {
163+
typeReference = typeReference.replace(
164+
', { with: { "resolution-mode": "import" } }',
165+
''
166+
);
167+
}
168+
151169
let importPath = /\(\"([^)]).+(\")/.exec(typeReference)[0];
152170
if (!importPath) {
153171
return { typeReference: undefined, importPath: null };
@@ -193,6 +211,10 @@ export function replaceImportPath(
193211
if (indexPos >= 0) {
194212
relativePath = relativePath.slice(0, indexPos);
195213
}
214+
} else if (options.esmCompatible) {
215+
// Add appropriate extension for non-node_modules imports
216+
const extension = getOutputExtension(fileName);
217+
relativePath += extension;
196218
}
197219

198220
typeReference = typeReference.replace(importPath, relativePath);
@@ -206,6 +228,7 @@ export function replaceImportPath(
206228
importPath: relativePath
207229
};
208230
}
231+
209232
return {
210233
typeReference: typeReference.replace('import', 'require'),
211234
importPath: relativePath

lib/plugin/visitors/controller-class.visitor.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import {
1616
convertPath,
1717
getDecoratorOrUndefinedByNames,
18+
getOutputExtension,
1819
getTypeReferenceAsString,
1920
hasPropertyKey
2021
} from '../utils/plugin-utils';
@@ -34,13 +35,14 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
3435
return this._typeImports;
3536
}
3637

37-
get collectedMetadata(): Array<
38-
[ts.CallExpression, Record<string, ClassMetadata>]
39-
> {
38+
collectedMetadata(
39+
options: PluginOptions
40+
): Array<[ts.CallExpression, Record<string, ClassMetadata>]> {
4041
const metadataWithImports = [];
4142
Object.keys(this._collectedMetadata).forEach((filePath) => {
4243
const metadata = this._collectedMetadata[filePath];
43-
const path = filePath.replace(/\.[jt]s$/, '');
44+
const fileExt = options.esmCompatible ? getOutputExtension(filePath) : '';
45+
const path = filePath.replace(/\.[jt]s$/, fileExt);
4446
const importExpr = ts.factory.createCallExpression(
4547
ts.factory.createToken(ts.SyntaxKind.ImportKeyword) as ts.Expression,
4648
undefined,

lib/plugin/visitors/model-class.visitor.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
convertPath,
2626
extractTypeArgumentIfArray,
2727
getDecoratorOrUndefinedByNames,
28+
getOutputExtension,
2829
getTypeReferenceAsString,
2930
hasPropertyKey,
3031
isAutoGeneratedEnumUnion,
@@ -43,13 +44,14 @@ export class ModelClassVisitor extends AbstractFileVisitor {
4344
return this._typeImports;
4445
}
4546

46-
get collectedMetadata(): Array<
47-
[ts.CallExpression, Record<string, ClassMetadata>]
48-
> {
47+
collectedMetadata(
48+
options: PluginOptions
49+
): Array<[ts.CallExpression, Record<string, ClassMetadata>]> {
4950
const metadataWithImports = [];
5051
Object.keys(this._collectedMetadata).forEach((filePath) => {
5152
const metadata = this._collectedMetadata[filePath];
52-
const path = filePath.replace(/\.[jt]s$/, '');
53+
const fileExt = options.esmCompatible ? getOutputExtension(filePath) : '';
54+
const path = filePath.replace(/\.[jt]s$/, fileExt);
5355
const importExpr = ts.factory.createCallExpression(
5456
ts.factory.createToken(ts.SyntaxKind.ImportKeyword) as ts.Expression,
5557
undefined,

lib/plugin/visitors/readonly.visitor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ export class ReadonlyVisitor {
5050

5151
collect() {
5252
return {
53-
models: this.modelClassVisitor.collectedMetadata,
54-
controllers: this.controllerClassVisitor.collectedMetadata
53+
models: this.modelClassVisitor.collectedMetadata(this.options),
54+
controllers: this.controllerClassVisitor.collectedMetadata(this.options)
5555
};
5656
}
5757
}

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"author": "Kamil Mysliwiec",
66
"license": "MIT",
77
"repository": "https://github.com/nestjs/swagger",
8+
"main": "dist/index.js",
9+
"types": "dist/index.d.ts",
810
"scripts": {
911
"build": "tsc -p tsconfig.build.json",
1012
"format": "prettier \"lib/**/*.ts\" --write",
@@ -13,6 +15,7 @@
1315
"publish:next": "npm publish --access public --tag next",
1416
"prepublish:npm": "npm run build",
1517
"publish:npm": "npm publish --access public",
18+
"prepare": "npm run build",
1619
"test": "jest",
1720
"test:dev": "jest --watch",
1821
"test:e2e": "jest --config e2e/jest-e2e.json",

test/plugin/fixtures/create-option.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const mergedCliPluginMultiOption = {
1616
dtoKeyOfComment: 'description',
1717
controllerKeyOfComment: 'summary',
1818
introspectComments: true,
19+
esmCompatible: false,
1920
readonly: false,
2021
debug: false
2122
};
@@ -28,6 +29,7 @@ export const mergedCliPluginSingleOption = {
2829
dtoKeyOfComment: 'description',
2930
controllerKeyOfComment: 'summary',
3031
introspectComments: true,
32+
esmCompatible: false,
3133
readonly: false,
3234
debug: false
3335
};

test/plugin/fixtures/parameter-property.dto.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import { getOutputExtension } from '../../../lib/plugin/utils/plugin-utils';
2+
13
export const parameterPropertyDtoText = `
24
export class ParameterPropertyDto {
35
constructor(
4-
readonly readonlyValue?: string,
5-
private privateValue: string | null,
6-
public publicValue: ItemDto[],
6+
readonly readonlyValue?: string,
7+
private privateValue: string | null,
8+
public publicValue: ItemDto[],
79
regularParameter: string
8-
protected protectedValue: string = '1234',
10+
protected protectedValue: string = '1234',
911
) {}
1012
}
1113
@@ -20,7 +22,13 @@ export class ItemDto {
2022
}
2123
`;
2224

23-
export const parameterPropertyDtoTextTranspiled = `import * as openapi from "@nestjs/swagger";
25+
export const parameterPropertyDtoTextTranspiled = (esmCompatible?: boolean) => {
26+
let fileName = 'parameter-property.dto';
27+
if (esmCompatible) {
28+
fileName += getOutputExtension(fileName);
29+
}
30+
31+
return `import * as openapi from "@nestjs/swagger";
2432
export class ParameterPropertyDto {
2533
constructor(readonlyValue, privateValue, publicValue, regularParameter, protectedValue = '1234') {
2634
this.readonlyValue = readonlyValue;
@@ -29,7 +37,7 @@ export class ParameterPropertyDto {
2937
this.protectedValue = protectedValue;
3038
}
3139
static _OPENAPI_METADATA_FACTORY() {
32-
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" } };
40+
return { readonlyValue: { required: false, type: () => String }, privateValue: { required: true, type: () => String, nullable: true }, publicValue: { required: true, type: () => [require("./${fileName}").ItemDto] }, protectedValue: { required: true, type: () => String, default: "1234" } };
3341
}
3442
}
3543
export var LettersEnum;
@@ -43,7 +51,8 @@ export class ItemDto {
4351
this.enumValue = enumValue;
4452
}
4553
static _OPENAPI_METADATA_FACTORY() {
46-
return { enumValue: { required: true, enum: require("./parameter-property.dto").LettersEnum } };
54+
return { enumValue: { required: true, enum: require("./${fileName}").LettersEnum } };
4755
}
4856
}
4957
`;
58+
};

0 commit comments

Comments
 (0)