Skip to content

Commit

Permalink
feat(plugin): introduce readonly visitors
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilmysliwiec committed Jun 5, 2023
1 parent 0693fba commit 2a7abda
Show file tree
Hide file tree
Showing 8 changed files with 321 additions and 92 deletions.
3 changes: 1 addition & 2 deletions lib/plugin/compiler-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import * as ts from 'typescript';
import { mergePluginOptions } from './merge-options';
import { isFilenameMatched } from './utils/is-filename-matched.util';
import { ControllerClassVisitor } from './visitors/controller-class.visitor';
import { ModelClassVisitor } from './visitors/model-class.visitor';

const modelClassVisitor = new ModelClassVisitor();
const controllerClassVisitor = new ControllerClassVisitor();
const isFilenameMatched = (patterns: string[], filename: string) =>
patterns.some((path) => filename.includes(path));

export const before = (options?: Record<string, any>, program?: ts.Program) => {
options = mergePluginOptions(options);
Expand Down
1 change: 1 addition & 0 deletions lib/plugin/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './compiler-plugin';
export * from './visitors/readonly.visitor';
5 changes: 4 additions & 1 deletion lib/plugin/merge-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export interface PluginOptions {
dtoKeyOfComment?: string;
controllerKeyOfComment?: string;
introspectComments?: boolean;
readonly?: boolean;
pathToSource?: string;
}

const defaultOptions: PluginOptions = {
Expand All @@ -15,7 +17,8 @@ const defaultOptions: PluginOptions = {
classValidatorShim: true,
dtoKeyOfComment: 'description',
controllerKeyOfComment: 'description',
introspectComments: false
introspectComments: false,
readonly: false
};

export const mergePluginOptions = (
Expand Down
2 changes: 2 additions & 0 deletions lib/plugin/utils/is-filename-matched.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const isFilenameMatched = (patterns: string[], filename: string) =>
patterns.some((path) => filename.includes(path));
12 changes: 10 additions & 2 deletions lib/plugin/utils/plugin-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { head } from 'lodash';
import { isAbsolute, posix } from 'path';
import * as ts from 'typescript';
import { PluginOptions } from '../merge-options';
import {
getDecoratorName,
getText,
Expand Down Expand Up @@ -117,7 +118,11 @@ export function hasPropertyKey(
.some((item) => item.name.getText() === key);
}

export function replaceImportPath(typeReference: string, fileName: string) {
export function replaceImportPath(
typeReference: string,
fileName: string,
options: PluginOptions
) {
if (!typeReference.includes('import')) {
return typeReference;
}
Expand All @@ -135,7 +140,10 @@ export function replaceImportPath(typeReference: string, fileName: string) {
require.resolve(importPath);
return typeReference.replace('import', 'require');
} catch (_error) {
let relativePath = posix.relative(posix.dirname(fileName), importPath);
const from = options?.readonly
? options.pathToSource
: posix.dirname(fileName);
let relativePath = posix.relative(from, importPath);
relativePath = relativePath[0] !== '.' ? './' + relativePath : relativePath;

const nodeModulesText = 'node_modules';
Expand Down
174 changes: 132 additions & 42 deletions lib/plugin/visitors/controller-class.visitor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { compact, head } from 'lodash';
import { posix } from 'path';
import * as ts from 'typescript';
import { ApiOperation, ApiResponse } from '../../decorators';
import { PluginOptions } from '../merge-options';
Expand All @@ -18,32 +19,78 @@ import {
} from '../utils/plugin-utils';
import { AbstractFileVisitor } from './abstract.visitor';

type ClassMetadata = Record<string, ts.ObjectLiteralExpression>;

export class ControllerClassVisitor extends AbstractFileVisitor {
private readonly _collectedMetadata: Record<
string,
Record<string, ClassMetadata>
> = {};

get collectedMetadata(): Record<string, Record<string, ClassMetadata>> {
return this._collectedMetadata;
}

visit(
sourceFile: ts.SourceFile,
ctx: ts.TransformationContext,
program: ts.Program,
options: PluginOptions
) {
const typeChecker = program.getTypeChecker();
sourceFile = this.updateImports(sourceFile, ctx.factory, program);
if (!options.readonly) {
sourceFile = this.updateImports(sourceFile, ctx.factory, program);
}

const visitNode = (node: ts.Node): ts.Node => {
if (ts.isMethodDeclaration(node)) {
try {
return this.addDecoratorToNode(
const metadata: ClassMetadata = {};
const updatedNode = this.addDecoratorToNode(
ctx.factory,
node,
typeChecker,
options,
sourceFile.fileName,
sourceFile
sourceFile,
metadata
);
if (!options.readonly) {
return updatedNode;
} else {
const filePath = this.normalizeImportPath(
options.pathToSource,
sourceFile.fileName
);

if (!this.collectedMetadata[filePath]) {
this.collectedMetadata[filePath] = {};
}

const parent = node.parent as ts.ClassDeclaration;
const clsName = parent.name?.getText();

if (clsName) {
if (!this.collectedMetadata[filePath][clsName]) {
this.collectedMetadata[filePath][clsName] = {};
}
Object.assign(
this.collectedMetadata[filePath][clsName],
metadata
);
}
}
} catch {
return node;
if (!options.readonly) {
return node;
}
}
}
return ts.visitEachChild(node, visitNode, ctx);

if (options.readonly) {
ts.forEachChild(node, visitNode);
} else {
return ts.visitEachChild(node, visitNode, ctx);
}
};
return ts.visitNode(sourceFile, visitNode);
}
Expand All @@ -53,11 +100,13 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
compilerNode: ts.MethodDeclaration,
typeChecker: ts.TypeChecker,
options: PluginOptions,
hostFilename: string,
sourceFile: ts.SourceFile
sourceFile: ts.SourceFile,
metadata: ClassMetadata
): ts.MethodDeclaration {
const hostFilename = sourceFile.fileName;
const decorators =
ts.canHaveDecorators(compilerNode) && ts.getDecorators(compilerNode);

if (!decorators) {
return compilerNode;
}
Expand All @@ -68,7 +117,8 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
decorators,
options,
sourceFile,
typeChecker
typeChecker,
metadata
);
const removeExistingApiOperationDecorator =
apiOperationDecoratorsArray.length > 0;
Expand All @@ -80,37 +130,42 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
: decorators;

const modifiers = ts.getModifiers(compilerNode) ?? [];
const objectLiteralExpr = this.createDecoratorObjectLiteralExpr(
factory,
compilerNode,
typeChecker,
factory.createNodeArray(),
hostFilename,
metadata,
options
);
const updatedDecorators = [
...apiOperationDecoratorsArray,
...existingDecorators,
factory.createDecorator(
factory.createCallExpression(
factory.createIdentifier(`${OPENAPI_NAMESPACE}.${ApiResponse.name}`),
undefined,
[
this.createDecoratorObjectLiteralExpr(
factory,
compilerNode,
typeChecker,
factory.createNodeArray(),
hostFilename
)
]
[factory.createObjectLiteralExpression(objectLiteralExpr.properties)]
)
)
];

return factory.updateMethodDeclaration(
compilerNode,
[...updatedDecorators, ...modifiers],
compilerNode.asteriskToken,
compilerNode.name,
compilerNode.questionToken,
compilerNode.typeParameters,
compilerNode.parameters,
compilerNode.type,
compilerNode.body
);
if (!options.readonly) {
return factory.updateMethodDeclaration(
compilerNode,
[...updatedDecorators, ...modifiers],
compilerNode.asteriskToken,
compilerNode.name,
compilerNode.questionToken,
compilerNode.typeParameters,
compilerNode.parameters,
compilerNode.type,
compilerNode.body
);
} else {
return compilerNode;
}
}

createApiOperationDecorator(
Expand All @@ -119,7 +174,8 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
decorators: readonly ts.Decorator[],
options: PluginOptions,
sourceFile: ts.SourceFile,
typeChecker: ts.TypeChecker
typeChecker: ts.TypeChecker,
metadata: ClassMetadata
) {
if (!options.introspectComments) {
return [];
Expand Down Expand Up @@ -163,10 +219,15 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
properties.push(deprecatedPropertyAssignment);
}

const objectLiteralExpr = factory.createObjectLiteralExpression(
compact(properties)
);
const apiOperationDecoratorArguments: ts.NodeArray<ts.Expression> =
factory.createNodeArray([
factory.createObjectLiteralExpression(compact(properties))
]);
factory.createNodeArray([objectLiteralExpr]);

const methodKey = node.name.getText();
metadata[methodKey] = objectLiteralExpr;

if (apiOperationDecorator) {
const expr = apiOperationDecorator.expression as any as ts.CallExpression;
const updatedCallExpr = factory.updateCallExpression(
Expand Down Expand Up @@ -196,28 +257,51 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
node: ts.MethodDeclaration,
typeChecker: ts.TypeChecker,
existingProperties: ts.NodeArray<ts.PropertyAssignment> = factory.createNodeArray(),
hostFilename: string
hostFilename: string,
metadata: ClassMetadata,
options: PluginOptions
): ts.ObjectLiteralExpression {
const properties = [
...existingProperties,
this.createStatusPropertyAssignment(factory, node, existingProperties),
let properties = [];
if (!options.readonly) {
properties = properties.concat(
existingProperties,
this.createStatusPropertyAssignment(factory, node, existingProperties)
);
}
properties = properties.concat([
this.createTypePropertyAssignment(
factory,
node,
typeChecker,
existingProperties,
hostFilename
hostFilename,
options
)
];
return factory.createObjectLiteralExpression(compact(properties));
]);
const objectLiteralExpr = factory.createObjectLiteralExpression(
compact(properties)
);

const methodKey = node.name.getText();
const existingExprOrUndefined = metadata[methodKey];
if (existingExprOrUndefined) {
factory.updateObjectLiteralExpression(objectLiteralExpr, [
...existingExprOrUndefined.properties,
...objectLiteralExpr.properties
]);
} else {
metadata[methodKey] = objectLiteralExpr;
}
return objectLiteralExpr;
}

createTypePropertyAssignment(
factory: ts.NodeFactory,
node: ts.MethodDeclaration,
typeChecker: ts.TypeChecker,
existingProperties: ts.NodeArray<ts.PropertyAssignment>,
hostFilename: string
hostFilename: string,
options: PluginOptions
) {
if (hasPropertyKey('type', existingProperties)) {
return undefined;
Expand All @@ -234,7 +318,7 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
if (typeReference.includes('node_modules')) {
return undefined;
}
typeReference = replaceImportPath(typeReference, hostFilename);
typeReference = replaceImportPath(typeReference, hostFilename, options);
return factory.createPropertyAssignment(
'type',
factory.createIdentifier(typeReference)
Expand Down Expand Up @@ -276,4 +360,10 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
}
return factory.createIdentifier('200');
}

private normalizeImportPath(pathToSource: string, path: string) {
let relativePath = posix.relative(pathToSource, path);
relativePath = relativePath[0] !== '.' ? './' + relativePath : relativePath;
return relativePath;
}
}
Loading

0 comments on commit 2a7abda

Please sign in to comment.