Skip to content

Commit

Permalink
Merge branch 'feature/graphql-plugin' of github.com:thekip/graphql in…
Browse files Browse the repository at this point in the history
…to thekip-feature/graphql-plugin
  • Loading branch information
kamilmysliwiec committed Sep 1, 2022
2 parents 2a5b51e + 1afa497 commit 6f2860c
Show file tree
Hide file tree
Showing 21 changed files with 1,558 additions and 368 deletions.
5 changes: 4 additions & 1 deletion .prettierignore
@@ -1,2 +1,5 @@
packages/**/tests/generated-definitions/*.ts
packages/**/tests/**/*.fixture.ts
packages/**/tests/**/*.fixture.ts

packages/**/tests/cases/**/*.ts
packages/**/tests/cases/**/*.ts
2 changes: 2 additions & 0 deletions packages/graphql/lib/plugin/merge-options.ts
Expand Up @@ -3,11 +3,13 @@ import { isString } from '@nestjs/common/utils/shared.utils';
export interface PluginOptions {
typeFileNameSuffix?: string | string[];
introspectComments?: boolean;
autoRegisterEnums?: boolean;
}

const defaultOptions: PluginOptions = {
typeFileNameSuffix: ['.input.ts', '.args.ts', '.entity.ts', '.model.ts'],
introspectComments: false,
autoRegisterEnums: false,
};

export const mergePluginOptions = (
Expand Down
262 changes: 226 additions & 36 deletions packages/graphql/lib/plugin/utils/ast-utils.ts
Expand Up @@ -12,14 +12,17 @@ import {
TypeChecker,
TypeFlags,
TypeFormatFlags,
SourceFile,
CommentRange,
getLeadingCommentRanges,
getTrailingCommentRanges,
UnionTypeNode,
TypeNode,
JSDoc,
getTextOfJSDocComment,
getJSDocDeprecatedTag,
ModifiersArray,
NodeArray,
getJSDocTags,
} from 'typescript';
import { isDynamicallyAdded } from './plugin-utils';
import * as ts from 'typescript';

export function isArray(type: Type) {
const symbol = type.getSymbol();
Expand Down Expand Up @@ -144,7 +147,7 @@ export function getDecoratorName(decorator: Decorator) {

function getIdentifierFromName(expression: LeftHandSideExpression) {
const identifier = getNameFromExpression(expression);
if (identifier && identifier.kind !== SyntaxKind.Identifier) {
if (expression && expression.kind !== SyntaxKind.Identifier) {
throw new Error();
}
return identifier;
Expand All @@ -157,39 +160,28 @@ function getNameFromExpression(expression: LeftHandSideExpression) {
return expression;
}

export function getDescriptionOfNode(
node: Node,
sourceFile: SourceFile,
): string {
const sourceText = sourceFile.getFullText();
// in case we decide to include "// comments"
const replaceRegex =
/^ *\** *@.*$|^ *\/\*+ *|^ *\/\/+.*|^ *\/+ *|^ *\*+ *| +$| *\**\/ *$/gim;
//const replaceRegex = /^ *\** *@.*$|^ *\/\*+ *|^ *\/+ *|^ *\*+ *| +$| *\**\/ *$/gim;

const description = [];
const introspectCommentsAndExamples = (comments?: CommentRange[]) =>
comments?.forEach((comment) => {
const commentSource = sourceText.substring(comment.pos, comment.end);
const oneComment = commentSource.replace(replaceRegex, '').trim();
if (oneComment) {
description.push(oneComment);
}
});
export function getJSDocDescription(node: Node): string {
const jsDoc: JSDoc[] = (node as any).jsDoc;

const leadingCommentRanges = getLeadingCommentRanges(
sourceText,
node.getFullStart(),
);
introspectCommentsAndExamples(leadingCommentRanges);
if (!description.length) {
const trailingCommentRanges = getTrailingCommentRanges(
sourceText,
node.getFullStart(),
);
introspectCommentsAndExamples(trailingCommentRanges);
if (!jsDoc) {
return undefined;
}

return getTextOfJSDocComment(jsDoc[0].comment);
}

export function hasJSDocTags(node: Node, tagName: string[]): boolean {
const tags = getJSDocTags(node);
return tags.some((tag) => tagName.includes(tag.tagName.text));
// return jsDoc;
}

export function getJsDocDeprecation(node: Node): string {
const deprecatedTag = getJSDocDeprecatedTag(node);
if (!deprecatedTag) {
return undefined;
}
return description.join('\n');
return getTextOfJSDocComment(deprecatedTag.comment) || 'deprecated';
}

export function findNullableTypeFromUnion(
Expand All @@ -200,3 +192,201 @@ export function findNullableTypeFromUnion(
hasFlag(typeChecker.getTypeAtLocation(tNode), TypeFlags.Null),
);
}

export function hasModifiers(
modifiers: ModifiersArray,
toCheck: SyntaxKind[],
): boolean {
if (!modifiers) {
return false;
}
return modifiers.some((modifier) => toCheck.includes(modifier.kind));
}

export function hasDecorators(
decorators: NodeArray<Decorator>,
toCheck: string[],
): boolean {
if (!decorators) {
return false;
}

return decorators.some((decorator) => {
return toCheck.includes(getDecoratorName(decorator));
});
}

export function hasImport(sf: ts.SourceFile, what: string): boolean {
for (const statement of sf.statements) {
if (
ts.isImportDeclaration(statement) &&
ts.isNamedImports(statement.importClause.namedBindings)
) {
const bindings = statement.importClause.namedBindings.elements;

for (const namedBinding of bindings) {
if (namedBinding.name.text === what) {
return true;
}
}
}
}
return false;
}

export function createImportEquals(
f: ts.NodeFactory,
identifier: ts.Identifier | string,
from: string,
): ts.ImportEqualsDeclaration {
const [major, minor] = ts.versionMajorMinor?.split('.').map((x) => +x);

if (major == 4 && minor >= 2) {
// support TS v4.2+
return f.createImportEqualsDeclaration(
undefined,
undefined,
false,
identifier,
f.createExternalModuleReference(f.createStringLiteral(from)),
);
}
return (f.createImportEqualsDeclaration as any)(
undefined,
undefined,
identifier,
f.createExternalModuleReference(f.createStringLiteral(from)),
);
}

export function createNamedImport(
f: ts.NodeFactory,
what: string[],
from: string,
) {
return f.createImportDeclaration(
undefined,
undefined,
f.createImportClause(
false,
undefined,
f.createNamedImports(
what.map((name) =>
f.createImportSpecifier(false, undefined, f.createIdentifier(name)),
),
),
),
f.createStringLiteral(from),
);
}

export function isCallExpressionOf(name: string, node: ts.CallExpression) {
return ts.isIdentifier(node.expression) && node.expression.text === name;
}

export type PrimitiveObject = {
[key: string]: string | boolean | ts.Node | PrimitiveObject;
};

function isNode(value: any): value is ts.Node {
return typeof value === 'object' && value.constructor.name === 'NodeObject';
}

export function serializePrimitiveObjectToAst(
f: ts.NodeFactory,
object: PrimitiveObject,
): ts.ObjectLiteralExpression {
const properties = [];

Object.keys(object).forEach((key) => {
const value = object[key];

if (value === undefined) {
return;
}

let initializer: ts.Expression;
if (isNode(value)) {
initializer = value as ts.Expression;
} else if (typeof value === 'string') {
initializer = f.createStringLiteral(value);
} else if (typeof value === 'boolean') {
initializer = value ? f.createTrue() : f.createFalse();
} else if (typeof value === 'object') {
initializer = serializePrimitiveObjectToAst(f, value);
}

properties.push(f.createPropertyAssignment(key, initializer));
});

return f.createObjectLiteralExpression(properties);
}

export function safelyMergeObjects(
f: ts.NodeFactory,
a: ts.Expression,
b: ts.Expression,
) {
// if both of objects are ObjectLiterals, so merge property by property in compile time
// if one or both of expressions not an object literal, produce rest spread and merge in runtime
if (ts.isObjectLiteralExpression(a) && ts.isObjectLiteralExpression(b)) {
const aMap = a.properties.reduce((acc, prop) => {
acc[(prop.name as ts.Identifier).text] = prop;
return acc;
}, {} as { [propName: string]: ts.ObjectLiteralElementLike });

b.properties.forEach((prop) => {
aMap[(prop.name as ts.Identifier).text] = prop;
}, {});

return f.createObjectLiteralExpression(Object.values(aMap));
} else {
return f.createObjectLiteralExpression([
f.createSpreadAssignment(a),
f.createSpreadAssignment(b),
]);
}
}

export function updateDecoratorArguments<T extends ts.ClassDeclaration | ts.PropertyDeclaration | ts.GetAccessorDeclaration>(
f: ts.NodeFactory,
node: T,
decoratorName: string,
replaceFn: (decoratorArguments: ts.NodeArray<ts.Expression>) => ts.Expression[]
): T {
let updated = false;

const decorators = node.decorators.map((decorator) => {
if (getDecoratorName(decorator) !== decoratorName) {
return decorator;
}

const decoratorExpression = decorator.expression as ts.CallExpression;
updated = true;
return f.updateDecorator(
decorator,
f.updateCallExpression(
decoratorExpression,
decoratorExpression.expression,
decoratorExpression.typeArguments,
replaceFn(decoratorExpression.arguments),
),
);
});

if (!updated) {
return node;
}

if (ts.isClassDeclaration(node)) {
return f.updateClassDeclaration(node, decorators, node.modifiers, node.name, node.typeParameters, node.heritageClauses, node.members) as T;
}

if (ts.isPropertyDeclaration(node)) {
return f.updatePropertyDeclaration(node, decorators, node.modifiers, node.name, node.questionToken, node.type, node.initializer) as T;
}

if (ts.isGetAccessorDeclaration(node)) {
return f.updateGetAccessorDeclaration(node, decorators, node.modifiers, node.name, node.parameters, node.type, node.body) as T;
}
}
46 changes: 6 additions & 40 deletions packages/graphql/lib/plugin/utils/plugin-utils.ts
Expand Up @@ -2,7 +2,6 @@ import { head } from 'lodash';
import { posix } from 'path';
import * as ts from 'typescript';
import {
getDecoratorName,
getText,
getTypeArguments,
isArray,
Expand All @@ -13,15 +12,6 @@ import {
isString,
} from './ast-utils';

export function getDecoratorOrUndefinedByNames(
names: string[],
decorators: ts.NodeArray<ts.Decorator>,
): ts.Decorator | undefined {
return (decorators || ts.createNodeArray()).find((item) =>
names.includes(getDecoratorName(item)),
);
}

export function getTypeReferenceAsString(
type: ts.Type,
typeChecker: ts.TypeChecker,
Expand Down Expand Up @@ -107,22 +97,13 @@ export function isPromiseOrObservable(type: string) {
return type.includes('Promise') || type.includes('Observable');
}

export function hasPropertyKey(
key: string,
properties: ts.NodeArray<ts.PropertyAssignment>,
): boolean {
return properties
.filter((item) => !isDynamicallyAdded(item))
.some((item) => item.name.getText() === key);
}

export function replaceImportPath(typeReference: string, fileName: string) {
if (!typeReference.includes('import')) {
return typeReference;
return { typeReference, importPath: null };
}
let importPath = /\(\"([^)]).+(\")/.exec(typeReference)[0];
let importPath = /\("([^)]).+(")/.exec(typeReference)[0];
if (!importPath) {
return undefined;
return { typeReference: undefined, importPath: null };
}
importPath = convertPath(importPath);
importPath = importPath.slice(2, importPath.length - 1);
Expand Down Expand Up @@ -153,7 +134,9 @@ export function replaceImportPath(typeReference: string, fileName: string) {
}

typeReference = typeReference.replace(importPath, relativePath);
return typeReference.replace('import', 'require');
typeReference = typeReference.replace('import', 'require');

return { typeReference, importPath: relativePath };
}

export function isDynamicallyAdded(identifier: ts.Node) {
Expand Down Expand Up @@ -234,23 +217,6 @@ export function isAutoGeneratedTypeUnion(type: ts.Type): boolean {
return false;
}

export function extractTypeArgumentIfArray(type: ts.Type) {
if (isArray(type)) {
type = getTypeArguments(type)[0];
if (!type) {
return undefined;
}
return {
type,
isArray: true,
};
}
return {
type,
isArray: false,
};
}

/**
* when "strict" mode enabled, TypeScript transform optional boolean properties to "boolean | undefined"
* @param text
Expand Down

0 comments on commit 6f2860c

Please sign in to comment.