@o3r/schematics
Version:
Schematics module of the Otter framework
310 lines • 14.6 kB
JavaScript
;
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