Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New features for GraphQL typescript plugin #1757

Merged
merged 20 commits into from
Sep 1, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f33c2c6
refactor(plugin) get rid of all deprecated ts compiler methods
thekip Sep 22, 2021
3767300
feat(plugin) support for `@deprecated` tag in jsDoc
thekip Sep 22, 2021
ba872af
feat(plugin) process only classes with whitelisted decorators
thekip Oct 17, 2021
c040d5d
feat(plugin) support JsDoc description on the type level
thekip Oct 17, 2021
3534806
feat(plugin) support automatic generation for inline string enums
thekip Oct 17, 2021
7f5ed13
feat(plugin) registerEnumType - inherit enum name from type
thekip Oct 17, 2021
40d8aba
feat(plugin) createUnionType - infer type name from variable declaration
thekip Oct 17, 2021
537e5b7
feat(plugin) auto discover enums
thekip Oct 17, 2021
1946079
refactor(plugin) reuse serializePrimitiveObjectToAst for property fac…
thekip Oct 18, 2021
c8c8c88
fix(plugin) don't add import if there is one existing
thekip Oct 18, 2021
e8763d6
fix(plugin) downlevel implicit registerEnumType import in CommonJS
thekip Oct 18, 2021
d84e42d
tests(plugin) add tests for commonjs import changing that actually ch…
thekip Oct 19, 2021
5d59b38
refactor(plugin) remove nested types introspection, refactor
thekip Oct 19, 2021
3fe7cdb
feature(plugin) don't introspect type if user provide it's own
thekip Oct 19, 2021
9d61a01
feature(plugin) collect type data from getters in class
thekip Nov 4, 2021
17dc544
chore() put graphql plugin into new folder structure
thekip Feb 11, 2022
8c27c3d
Merge remote-tracking branch 'upstream/master' into feature/graphql-p…
thekip Feb 11, 2022
5c6e0c0
Merge remote-tracking branch 'upstream/master' into feature/graphql-p…
thekip Jul 4, 2022
4ab789a
feature(plugin) use type defined in decorator instead introspection
thekip Jul 8, 2022
1afa497
feature(plugin) add introspection data to the Field decorator if pres…
thekip Jul 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
tests/generated-definitions/*.ts
tests/generated-definitions/*.ts
tests/plugin/cases/**/actual
tests/plugin/cases/**/expected
2 changes: 2 additions & 0 deletions lib/plugin/merge-options.ts
Original file line number Diff line number Diff line change
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
223 changes: 186 additions & 37 deletions lib/plugin/utils/ast-utils.ts
Original file line number Diff line number Diff line change
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 @@ -157,43 +160,189 @@ 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;

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;
}

const leadingCommentRanges = getLeadingCommentRanges(
sourceText,
node.getFullStart(),
export function getJsDocDeprecation(node: Node): string {
const deprecatedTag = getJSDocDeprecatedTag(node);
if (!deprecatedTag) {
return undefined;
}
return getTextOfJSDocComment(deprecatedTag.comment) || 'deprecated';
}

export function findNullableTypeFromUnion(
typeNode: UnionTypeNode,
typeChecker: TypeChecker,
) {
return typeNode.types.find((tNode: TypeNode) =>
hasFlag(typeChecker.getTypeAtLocation(tNode), TypeFlags.Null),
);
introspectCommentsAndExamples(leadingCommentRanges);
if (!description.length) {
const trailingCommentRanges = getTrailingCommentRanges(
sourceText,
node.getFullStart(),
}

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)),
);
introspectCommentsAndExamples(trailingCommentRanges);
}
return description.join('\n');
return (f.createImportEqualsDeclaration as any)(
undefined,
undefined,
identifier,
f.createExternalModuleReference(f.createStringLiteral(from)),
);
}

export function findNullableTypeFromUnion(typeNode: UnionTypeNode, typeChecker: TypeChecker) {
return typeNode.types.find(
(tNode: TypeNode) =>
hasFlag(typeChecker.getTypeAtLocation(tNode), TypeFlags.Null)
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(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),
]);
}
}
46 changes: 6 additions & 40 deletions lib/plugin/utils/plugin-utils.ts
Original file line number Diff line number Diff line change
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