UNPKG

@o3r/schematics

Version:

Schematics module of the Otter framework

310 lines • 14.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.addImportsAndCodeBlockStatementAtSpecInitializationTransformerFactory = exports.isTestBedConfiguration = exports.getSimpleUpdatedMethod = exports.findMethodByName = exports.fixStringLiterals = exports.addCommentsOnClassProperties = exports.addInterfaceToClassTransformerFactory = exports.sortClassElement = exports.generateParametersDeclarationFromString = exports.generateBlockStatementsFromString = exports.generateClassElementsFromString = exports.generateImplementsExpressionWithTypeArguments = exports.getPropertyFromDecoratorFirstArgument = exports.isDecoratorWithArg = void 0; exports.findFirstNodeOfKind = findFirstNodeOfKind; exports.findLastNodeOfKind = findLastNodeOfKind; exports.parseImportsFromFile = parseImportsFromFile; exports.getExportedSymbolsFromFile = getExportedSymbolsFromFile; const ast_utils_1 = require("@schematics/angular/utility/ast-utils"); const ts = require("typescript"); /** * Find the first node with the specific syntax kind * @param sourceFile Typescript file * @param searchKind Kind of syntax to look up * @param node current node */ function findFirstNodeOfKind(sourceFile, searchKind, node) { const currentNode = node || sourceFile; let ret = null; currentNode.forEachChild((n) => { if (!ret) { ret = n.kind === searchKind ? n : findFirstNodeOfKind(sourceFile, searchKind, n); } }); return ret; } /** * Find the last node with the specific syntax kind * @param sourceFile Typescript file * @param searchKind Kind of syntax to look up * @param node current node */ function findLastNodeOfKind(sourceFile, searchKind, node) { const currentNode = node || sourceFile; let ret = null; currentNode.forEachChild((n) => { if (n.kind === searchKind) { ret = ret && ret.pos > n.pos ? ret : n; } else { const found = findLastNodeOfKind(sourceFile, searchKind, n); if (found) { ret = ret ? (found.pos > ret.pos ? found : ret) : found; } } }); return ret; } /** * Reads all the imports of a given SourceFile and returns a parsed list that's easy to consume. * @param sourceFile */ function parseImportsFromFile(sourceFile) { // First we look for all imports lines targeting an Otter package for which we know a mapping return (0, ast_utils_1.findNodes)(sourceFile, ts.SyntaxKind.ImportDeclaration).map((nodeImp) => { const imp = nodeImp; const importFrom = imp.moduleSpecifier.getText().replace(/["']/g, ''); // We retrieve all the symbols listed in the import statement const namedImport = imp.importClause && imp.importClause.getChildAt(0); const imports = namedImport && ts.isNamedImports(namedImport) ? namedImport.elements.map((element) => element.getText()) : []; return { node: imp, symbols: imports, module: importFrom }; }); } /** * Given a program and a path to a source file, returns all the Symbols exported by the file. * @param program * @param sourcePath */ function getExportedSymbolsFromFile(program, sourcePath) { const typeChecker = program.getTypeChecker(); const sourceFile = program.getSourceFiles().find((file) => { return file.fileName === sourcePath; }); if (!sourceFile) { return []; } const symbol = typeChecker.getSymbolAtLocation(sourceFile); return typeChecker.getExportsOfModule(symbol); } /** * Returns true if it is a decorator with arguments * @param node decorator node */ const isDecoratorWithArg = (node) => ts.isDecorator(node) && ts.isCallExpression(node.expression) && ts.isIdentifier(node.expression.expression); exports.isDecoratorWithArg = isDecoratorWithArg; /** * Returns the value of {@link argName} in the first argument * @param decorator * @param argName */ const getPropertyFromDecoratorFirstArgument = (decorator, argName) => { const ngClassDecoratorArgument = decorator.expression.arguments[0]; if (ts.isObjectLiteralExpression(ngClassDecoratorArgument)) { return ngClassDecoratorArgument.properties.find((prop) => ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.escapedText.toString() === argName)?.initializer; } }; exports.getPropertyFromDecoratorFirstArgument = getPropertyFromDecoratorFirstArgument; /** * Returns `ExpressionWithTypeArguments[]` of a class that implements {@link str} * @param str */ const generateImplementsExpressionWithTypeArguments = (str) => { const sourceFile = ts.createSourceFile('index.ts', `class A implements ${str} {}`, ts.ScriptTarget.ES2020, true); return [...sourceFile.statements[0].heritageClauses[0].types]; }; exports.generateImplementsExpressionWithTypeArguments = generateImplementsExpressionWithTypeArguments; /** * Returns `ClassElement[]` of a class that have {@link str} has body * @param str */ const generateClassElementsFromString = (str) => { const sourceFile = ts.createSourceFile('index.ts', `class A { ${str} }`, ts.ScriptTarget.ES2020, true); return [...sourceFile.statements[0].members]; }; exports.generateClassElementsFromString = generateClassElementsFromString; /** * Returns `Statement[]` of a function that have {@link str} has body * @param str */ const generateBlockStatementsFromString = (str) => { const sourceFile = ts.createSourceFile('index.ts', `function func() { ${str.replace(/^\s*/g, '')} }`, ts.ScriptTarget.ES2020, true); return [...sourceFile.statements[0].body.statements]; }; exports.generateBlockStatementsFromString = generateBlockStatementsFromString; /** * Returns `ParameterDeclaration[]` of a function that have {@link str} has parameters * @param str */ const generateParametersDeclarationFromString = (str) => { const sourceFile = ts.createSourceFile('index.ts', `function func(${str}) {}`, ts.ScriptTarget.ES2020, true); return [...sourceFile.statements[0].parameters]; }; exports.generateParametersDeclarationFromString = generateParametersDeclarationFromString; /** * Method to sort ClassElement based on the kind of it * order will be PropertyDeclaration, Constructor then MethodDeclaration * @param classElement1 * @param classElement2 */ const sortClassElement = (classElement1, classElement2) => { switch (classElement1.kind) { case ts.SyntaxKind.PropertyDeclaration: { return -1; } case ts.SyntaxKind.Constructor: { switch (classElement2.kind) { case ts.SyntaxKind.PropertyDeclaration: { return 1; } case ts.SyntaxKind.MethodDeclaration: { return -1; } default: { return -1; } } } case ts.SyntaxKind.MethodDeclaration: { return 1; } default: { return 1; } } }; exports.sortClassElement = sortClassElement; /** * Returns a TransformerFactory to add an interface to a class * @param interfaceToAdd * @param classIdentifier * @param interfacesToRemove */ const addInterfaceToClassTransformerFactory = (interfaceToAdd, classIdentifier = () => true, interfacesToRemove = new Set()) => { return (ctx) => (rootNode) => { const { factory } = ctx; const visit = (node) => { if (ts.isClassDeclaration(node) && classIdentifier(node)) { const implementsClauses = node.heritageClauses?.find((heritageClause) => heritageClause.token === ts.SyntaxKind.ImplementsKeyword); const interfaceToImplements = (0, exports.generateImplementsExpressionWithTypeArguments)(interfaceToAdd); const newImplementsClauses = implementsClauses ? factory.updateHeritageClause(implementsClauses, [ ...implementsClauses.types.filter((interfaceNode) => interfacesToRemove.size === 0 || (ts.isIdentifier(interfaceNode.expression) && !interfacesToRemove.has(interfaceNode.expression.escapedText.toString()))), ...interfaceToImplements ]) : factory.createHeritageClause(ts.SyntaxKind.ImplementsKeyword, [...interfaceToImplements]); const heritageClauses = [...(node.heritageClauses || [])] .filter((h) => h.token !== ts.SyntaxKind.ImplementsKeyword) .concat(newImplementsClauses); const newModifiers = [] .concat(ts.getDecorators(node) || []) .concat(ts.getModifiers(node) || []); return factory.updateClassDeclaration(node, newModifiers, node.name, node.typeParameters, heritageClauses, node.members); } return ts.visitEachChild(node, visit, ctx); }; return ts.visitNode(rootNode, visit); }; }; exports.addInterfaceToClassTransformerFactory = addInterfaceToClassTransformerFactory; /** * Add comment on class properties * @param classElements * @param comments Dictionary of comment indexed by properties' name */ const addCommentsOnClassProperties = (classElements, comments) => { classElements.filter((classElement) => ts.isPropertyDeclaration(classElement) && ts.isIdentifier(classElement.name)).forEach((classElement) => { const comment = comments[classElement.name.escapedText.toString()]; if (!comment) { return; } ts.addSyntheticLeadingComment(classElement, ts.SyntaxKind.MultiLineCommentTrivia, `* ${comment}`, true); }); }; exports.addCommentsOnClassProperties = addCommentsOnClassProperties; /** * Transformer to be used to fix the string literals generated by creating a new SourceFile (for instance, using #generateClassElementsFromString) * @param ctx */ const fixStringLiterals = (ctx) => (rootNode) => { const { factory } = ctx; const visit = (node) => { if (node.kind === ts.SyntaxKind.StringLiteral) { return factory.createStringLiteral(node.getText().replace(/^["'](.*)["']$/, '$1'), true); } return ts.visitEachChild(node, visit, ctx); }; return ts.visitNode(rootNode, visit); }; exports.fixStringLiterals = fixStringLiterals; /** * Returns a function to match classElement by method name * @param methodName */ const findMethodByName = (methodName) => (classElement) => ts.isMethodDeclaration(classElement) && ts.isIdentifier(classElement.name) && classElement.name.escapedText.toString() === methodName; exports.findMethodByName = findMethodByName; /** * Add block statements to a method * @param node * @param factory * @param methodName * @param blockStatements */ const getSimpleUpdatedMethod = (node, factory, methodName, blockStatements) => { const originalMethod = node.members.find((0, exports.findMethodByName)(methodName)); return originalMethod ? factory.updateMethodDeclaration(originalMethod, ts.getModifiers(originalMethod), originalMethod.asteriskToken, originalMethod.name, originalMethod.questionToken, originalMethod.typeParameters, originalMethod.parameters, originalMethod.type, originalMethod.body ? factory.updateBlock(originalMethod.body, originalMethod.body.statements.concat(blockStatements)) : factory.createBlock(blockStatements, true)) : factory.createMethodDeclaration([factory.createToken(ts.SyntaxKind.PublicKeyword)], undefined, factory.createIdentifier(methodName), undefined, undefined, [], undefined, factory.createBlock(blockStatements, true)); }; exports.getSimpleUpdatedMethod = getSimpleUpdatedMethod; /** * Return true is the node is the ExpressionStatement of the TestBedConfiguration * @param node */ const isTestBedConfiguration = (node) => (ts.isExpressionStatement(node) && ts.isCallExpression(node.expression) && ts.isPropertyAccessExpression(node.expression.expression) && ts.isIdentifier(node.expression.expression.expression) && node.expression.expression.expression.escapedText.toString() === 'TestBed' && node.expression.expression.name.escapedText.toString() === 'configureTestingModule') || (ts.isAwaitExpression(node) && (0, exports.isTestBedConfiguration)(node.expression)); exports.isTestBedConfiguration = isTestBedConfiguration; /** * TransformerFactory to add imports at spec initialization and code to be run just after * @param imports * @param code */ const addImportsAndCodeBlockStatementAtSpecInitializationTransformerFactory = (imports, code) => (ctx) => (rootNode) => { const { factory } = ctx; const visit = (node) => { if (ts.isBlock(node) && node.statements.some((statement) => (0, exports.isTestBedConfiguration)(statement))) { return factory.updateBlock(node, node.statements .reduce((acc, statement) => { if ((0, exports.isTestBedConfiguration)(statement)) { const firstArgProps = statement.expression.arguments[0].properties; const importsProp = firstArgProps.find((prop) => prop.name?.getText() === 'imports'); return acc.concat(factory.updateExpressionStatement(statement, factory.updateCallExpression(statement.expression, statement.expression.expression, statement.expression.typeArguments, [ factory.createObjectLiteralExpression([ ...firstArgProps.filter((prop) => prop.name?.getText() !== 'imports'), factory.createPropertyAssignment('imports', factory.createArrayLiteralExpression((importsProp ? [...importsProp.initializer.elements] : []).concat(imports.map((importName) => typeof importName === 'string' ? factory.createIdentifier(importName) : importName)), true)) ], true) ]))); } return acc.concat(statement); }, []) .concat(code ? (0, exports.generateBlockStatementsFromString)(code) : [])); } return ts.visitEachChild(node, visit, ctx); }; return ts.visitNode(rootNode, visit); }; exports.addImportsAndCodeBlockStatementAtSpecInitializationTransformerFactory = addImportsAndCodeBlockStatementAtSpecInitializationTransformerFactory; //# sourceMappingURL=ast.js.map