@angular-cli/ast-tools
Version:
CLI tool for Angular
501 lines • 22.6 kB
JavaScript
;
var ts = require('typescript');
var fs = require('fs');
var path = require('path');
var change_1 = require('./change');
var node_1 = require('./node');
var ast_utils_1 = require('./ast-utils');
var change_2 = require('./change');
/**
* Adds imports to mainFile and adds toBootstrap to the array of providers
* in bootstrap, if not present
* @param mainFile main.ts
* @param imports Object { importedClass: ['path/to/import/from', defaultStyleImport?] }
* @param toBootstrap
*/
function bootstrapItem(mainFile, imports, toBootstrap) {
var changes = Object.keys(imports).map(function (importedClass) {
var defaultStyleImport = imports[importedClass].length === 2 && !!imports[importedClass][1];
return insertImport(mainFile, importedClass, imports[importedClass][0].toString(), defaultStyleImport);
});
var rootNode = getRootNode(mainFile);
// get ExpressionStatements from the top level syntaxList of the sourceFile
var bootstrapNodes = rootNode.getChildAt(0).getChildren().filter(function (node) {
// get bootstrap expressions
return node.kind === ts.SyntaxKind.ExpressionStatement &&
node.getChildAt(0).getChildAt(0).text.toLowerCase() === 'bootstrap';
});
if (bootstrapNodes.length !== 1) {
throw new Error(("Did not bootstrap provideRouter in " + mainFile) +
' because of multiple or no bootstrap calls');
}
var bootstrapNode = bootstrapNodes[0].getChildAt(0);
var isBootstraped = node_1.findNodes(bootstrapNode, ts.SyntaxKind.SyntaxList) // get bootstrapped items
.reduce(function (a, b) { return a.concat(b.getChildren().map(function (n) { return n.getText(); })); }, [])
.filter(function (n) { return n !== ','; })
.indexOf(toBootstrap) !== -1;
if (isBootstraped) {
return changes;
}
// if bracket exitst already, add configuration template,
// otherwise, insert into bootstrap parens
var fallBackPos, configurePathsTemplate, separator;
var syntaxListNodes;
var bootstrapProviders = bootstrapNode.getChildAt(2).getChildAt(2); // array of providers
if (bootstrapProviders) {
syntaxListNodes = bootstrapProviders.getChildAt(1).getChildren();
fallBackPos = bootstrapProviders.getChildAt(2).pos; // closeBracketLiteral
separator = syntaxListNodes.length === 0 ? '' : ', ';
configurePathsTemplate = "" + separator + toBootstrap;
}
else {
fallBackPos = bootstrapNode.getChildAt(3).pos; // closeParenLiteral
syntaxListNodes = bootstrapNode.getChildAt(2).getChildren();
configurePathsTemplate = ", [ " + toBootstrap + " ]";
}
changes.push(ast_utils_1.insertAfterLastOccurrence(syntaxListNodes, configurePathsTemplate, mainFile, fallBackPos));
return changes;
}
exports.bootstrapItem = bootstrapItem;
/**
* Add Import `import { symbolName } from fileName` if the import doesn't exit
* already. Assumes fileToEdit can be resolved and accessed.
* @param fileToEdit (file we want to add import to)
* @param symbolName (item to import)
* @param fileName (path to the file)
* @param isDefault (if true, import follows style for importing default exports)
* @return Change
*/
function insertImport(fileToEdit, symbolName, fileName, isDefault) {
if (isDefault === void 0) { isDefault = false; }
if (process.platform.startsWith('win')) {
fileName = fileName.replace(/\\/g, '/'); // correction in windows
}
var rootNode = getRootNode(fileToEdit);
var allImports = node_1.findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
// get nodes that map to import statements from the file fileName
var relevantImports = allImports.filter(function (node) {
// StringLiteral of the ImportDeclaration is the import file (fileName in this case).
var importFiles = node.getChildren().filter(function (child) { return child.kind === ts.SyntaxKind.StringLiteral; })
.map(function (n) { return n.text; });
return importFiles.filter(function (file) { return file === fileName; }).length === 1;
});
if (relevantImports.length > 0) {
var importsAsterisk_1 = false;
// imports from import file
var imports_1 = [];
relevantImports.forEach(function (n) {
Array.prototype.push.apply(imports_1, node_1.findNodes(n, ts.SyntaxKind.Identifier));
if (node_1.findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) {
importsAsterisk_1 = true;
}
});
// if imports * from fileName, don't add symbolName
if (importsAsterisk_1) {
return;
}
var importTextNodes = imports_1.filter(function (n) { return n.text === symbolName; });
// insert import if it's not there
if (importTextNodes.length === 0) {
var fallbackPos_1 = node_1.findNodes(relevantImports[0], ts.SyntaxKind.CloseBraceToken)[0].pos ||
node_1.findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].pos;
return ast_utils_1.insertAfterLastOccurrence(imports_1, ", " + symbolName, fileToEdit, fallbackPos_1);
}
return new change_1.NoopChange();
}
// no such import declaration exists
var useStrict = node_1.findNodes(rootNode, ts.SyntaxKind.StringLiteral)
.filter(function (n) { return n.text === 'use strict'; });
var fallbackPos = 0;
if (useStrict.length > 0) {
fallbackPos = useStrict[0].end;
}
var open = isDefault ? '' : '{ ';
var close = isDefault ? '' : ' }';
// if there are no imports or 'use strict' statement, insert import at beginning of file
var insertAtBeginning = allImports.length === 0 && useStrict.length === 0;
var separator = insertAtBeginning ? '' : ';\n';
var toInsert = (separator + "import " + open + symbolName + close) +
(" from '" + fileName + "'" + (insertAtBeginning ? ';\n' : ''));
return ast_utils_1.insertAfterLastOccurrence(allImports, toInsert, fileToEdit, fallbackPos, ts.SyntaxKind.StringLiteral);
}
exports.insertImport = insertImport;
;
/**
* Inserts a path to the new route into src/routes.ts if it doesn't exist
* @param routesFile
* @param pathOptions
* @return Change[]
* @throws Error if routesFile has multiple export default or none.
*/
function addPathToRoutes(routesFile, pathOptions) {
var route = pathOptions.route.split('/')
.filter(function (n) { return n !== ''; }).join('/'); // change say `/about/:id/` to `about/:id`
var isDefault = pathOptions.isDefault ? ', useAsDefault: true' : '';
var outlet = pathOptions.outlet ? ", outlet: '" + pathOptions.outlet + "'" : '';
// create route path and resolve component import
var positionalRoutes = /\/:[^/]*/g;
var routePath = route.replace(positionalRoutes, '');
routePath = "./app/" + routePath + "/" + pathOptions.dasherizedName + ".component";
var originalComponent = pathOptions.component;
pathOptions.component = resolveImportName(pathOptions.component, routePath, pathOptions.routesFile);
var content = "{ path: '" + route + "', component: " + pathOptions.component + isDefault + outlet + " }";
var rootNode = getRootNode(routesFile);
var routesNode = rootNode.getChildAt(0).getChildren().filter(function (n) {
// get export statement
return n.kind === ts.SyntaxKind.ExportAssignment &&
n.getFullText().indexOf('export default') !== -1;
});
if (routesNode.length !== 1) {
throw new Error('Did not insert path in routes.ts because ' +
"there were multiple or no 'export default' statements");
}
var pos = routesNode[0].getChildAt(2).getChildAt(0).end; // openBracketLiteral
// all routes in export route array
var routesArray = routesNode[0].getChildAt(2).getChildAt(1)
.getChildren()
.filter(function (n) { return n.kind === ts.SyntaxKind.ObjectLiteralExpression; });
if (pathExists(routesArray, route, pathOptions.component)) {
// don't duplicate routes
throw new Error('Route was not added since it is a duplicate');
}
var isChild = false;
// get parent to insert under
var parent;
if (pathOptions.parent) {
// append '_' to route to find the actual parent (not parent of the parent)
parent = getParent(routesArray, pathOptions.parent + "/_");
if (!parent) {
throw new Error("You specified parent '" + pathOptions.parent + "'' which was not found in routes.ts");
}
if (route.indexOf(pathOptions.parent) === 0) {
route = route.substring(pathOptions.parent.length);
}
}
else {
parent = getParent(routesArray, route);
}
if (parent) {
var childrenInfo = addChildPath(parent, pathOptions, route);
if (!childrenInfo) {
// path exists already
throw new Error('Route was not added since it is a duplicate');
}
content = childrenInfo.newContent;
pos = childrenInfo.pos;
isChild = true;
}
var isFirstElement = routesArray.length === 0;
if (!isChild) {
var separator = isFirstElement ? '\n' : ',';
content = "\n " + content + separator;
}
var changes = [new change_1.InsertChange(routesFile, pos, content)];
var component = originalComponent === pathOptions.component ? originalComponent :
originalComponent + " as " + pathOptions.component;
routePath = routePath.replace(/\\/, '/'); // correction in windows
changes.push(insertImport(routesFile, component, routePath));
return changes;
}
exports.addPathToRoutes = addPathToRoutes;
/**
* Add more properties to the route object in routes.ts
* @param routesFile routes.ts
* @param routes Object {route: [key, value]}
*/
function addItemsToRouteProperties(routesFile, routes) {
var rootNode = getRootNode(routesFile);
var routesNode = rootNode.getChildAt(0).getChildren().filter(function (n) {
// get export statement
return n.kind === ts.SyntaxKind.ExportAssignment &&
n.getFullText().indexOf('export default') !== -1;
});
if (routesNode.length !== 1) {
throw new Error('Did not insert path in routes.ts because ' +
"there were multiple or no 'export default' statements");
}
var routesArray = routesNode[0].getChildAt(2).getChildAt(1)
.getChildren()
.filter(function (n) { return n.kind === ts.SyntaxKind.ObjectLiteralExpression; });
var changes = Object.keys(routes).reduce(function (result, route) {
// let route = routes[guardName][0];
var itemKey = routes[route][0];
var itemValue = routes[route][1];
var currRouteNode = getParent(routesArray, route + "/_");
if (!currRouteNode) {
throw new Error("Could not find '" + route + "' in routes.ts");
}
var fallBackPos = node_1.findNodes(currRouteNode, ts.SyntaxKind.CloseBraceToken).pop().pos;
var pathPropertiesNodes = currRouteNode.getChildAt(1).getChildren()
.filter(function (n) { return n.kind === ts.SyntaxKind.PropertyAssignment; });
return result.concat([ast_utils_1.insertAfterLastOccurrence(pathPropertiesNodes, ", " + itemKey + ": " + itemValue, routesFile, fallBackPos)]);
}, []);
return changes;
}
exports.addItemsToRouteProperties = addItemsToRouteProperties;
/**
* Verifies that a component file exports a class of the component
* @param file
* @param componentName
* @return whether file exports componentName
*/
function confirmComponentExport(file, componentName) {
var rootNode = getRootNode(file);
var exportNodes = rootNode.getChildAt(0).getChildren().filter(function (n) {
return n.kind === ts.SyntaxKind.ClassDeclaration &&
(n.getChildren().filter(function (p) { return p.text === componentName; }).length !== 0);
});
return exportNodes.length > 0;
}
exports.confirmComponentExport = confirmComponentExport;
/**
* Ensures there is no collision between import names. If a collision occurs, resolve by adding
* underscore number to the name
* @param importName
* @param importPath path to import component from
* @param fileName (file to add import to)
* @return resolved importName
*/
function resolveImportName(importName, importPath, fileName) {
var rootNode = getRootNode(fileName);
// get all the import names
var importNodes = rootNode.getChildAt(0).getChildren()
.filter(function (n) { return n.kind === ts.SyntaxKind.ImportDeclaration; });
// check if imported file is same as current one before updating component name
var importNames = importNodes
.reduce(function (a, b) {
var importFrom = node_1.findNodes(b, ts.SyntaxKind.StringLiteral); // there's only one
if (importFrom.pop().text !== importPath) {
// importing from different file, add to imported components to inspect
// if only one identifier { FooComponent }, if two { FooComponent as FooComponent_1 }
// choose last element of identifier array in both cases
return a.concat([node_1.findNodes(b, ts.SyntaxKind.Identifier).pop()]);
}
return a;
}, [])
.map(function (n) { return n.text; });
var index = importNames.indexOf(importName);
if (index === -1) {
return importName;
}
var baseName = importNames[index].split('_')[0];
var newName = baseName;
var resolutionNumber = 1;
while (importNames.indexOf(newName) !== -1) {
newName = baseName + "_" + resolutionNumber;
resolutionNumber++;
}
return newName;
}
/**
* Resolve a path to a component file. If the path begins with path.sep, it is treated to be
* absolute from the app/ directory. Otherwise, it is relative to currDir
* @param projectRoot
* @param currentDir
* @param filePath componentName or path to componentName
* @return component file name
* @throw Error if component file referenced by path is not found
*/
function resolveComponentPath(projectRoot, currentDir, filePath) {
var parsedPath = path.parse(filePath);
var componentName = parsedPath.base.split('.')[0];
var componentDir = path.parse(parsedPath.dir).base;
// correction for a case where path is /**/componentName/componentName(.component.ts)
if (componentName === componentDir) {
filePath = parsedPath.dir;
}
if (parsedPath.dir === '') {
// only component file name is given
filePath = componentName;
}
var directory = filePath[0] === path.sep ?
path.resolve(path.join(projectRoot, 'src', 'app', filePath)) :
path.resolve(currentDir, filePath);
if (!fs.existsSync(directory)) {
throw new Error(("path '" + filePath + "' must be relative to current directory") +
" or absolute from project root");
}
if (directory.indexOf('src' + path.sep + 'app') === -1) {
throw new Error('Route must be within app');
}
var componentFile = path.join(directory, componentName + ".component.ts");
if (!fs.existsSync(componentFile)) {
throw new Error("could not find component file referenced by " + filePath);
}
return componentFile;
}
exports.resolveComponentPath = resolveComponentPath;
/**
* Sort changes in decreasing order and apply them.
* @param changes
* @param host
* @return Promise
*/
function applyChanges(changes, host) {
if (host === void 0) { host = change_2.NodeHost; }
return changes
.filter(function (change) { return !!change; })
.sort(function (curr, next) { return next.order - curr.order; })
.reduce(function (newChange, change) { return newChange.then(function () { return change.apply(host); }); }, Promise.resolve());
}
exports.applyChanges = applyChanges;
/**
* Helper for addPathToRoutes. Adds child array to the appropriate position in the routes.ts file
* @return Object (pos, newContent)
*/
function addChildPath(parentObject, pathOptions, route) {
if (!parentObject) {
return;
}
var pos;
var newContent;
// get object with 'children' property
var childrenNode = parentObject.getChildAt(1).getChildren()
.filter(function (n) {
return n.kind === ts.SyntaxKind.PropertyAssignment
&& n.name.text === 'children';
});
// find number of spaces to pad nested paths
var nestingLevel = 1; // for indenting route object in the `children` array
var n = parentObject;
while (n.parent) {
if (n.kind === ts.SyntaxKind.ObjectLiteralExpression
|| n.kind === ts.SyntaxKind.ArrayLiteralExpression) {
nestingLevel++;
}
n = n.parent;
}
// strip parent route
var parentRoute = parentObject.getChildAt(1).getChildAt(0).getChildAt(2).text;
var childRoute = route.substring(route.indexOf(parentRoute) + parentRoute.length + 1);
var isDefault = pathOptions.isDefault ? ', useAsDefault: true' : '';
var outlet = pathOptions.outlet ? ", outlet: '" + pathOptions.outlet + "'" : '';
var content = ("{ path: '" + childRoute + "', component: " + pathOptions.component) +
("" + isDefault + outlet + " }");
var spaces = Array(2 * nestingLevel + 1).join(' ');
if (childrenNode.length !== 0) {
// add to beginning of children array
pos = childrenNode[0].getChildAt(2).getChildAt(1).pos; // open bracket
newContent = "\n" + spaces + content + ",";
}
else {
// no children array, add one
pos = parentObject.getChildAt(2).pos; // close brace
newContent = (",\n" + spaces.substring(2) + "children: [\n" + spaces + content) +
("\n" + spaces.substring(2) + "]\n" + spaces.substring(5));
}
return { newContent: newContent, pos: pos };
}
/**
* Helper for addPathToRoutes.
* @return parentNode which contains the children array to add a new path to or
* undefined if none or the entire route was matched.
*/
function getParent(routesArray, route, parent) {
if (routesArray.length === 0 && !parent) {
return; // no children array and no parent found
}
if (route.length === 0) {
return; // route has been completely matched
}
var splitRoute = route.split('/');
// don't treat positional parameters separately
if (splitRoute.length > 1 && splitRoute[1].indexOf(':') !== -1) {
var actualRoute = splitRoute.shift();
splitRoute[0] = actualRoute + "/" + splitRoute[0];
}
var potentialParents = routesArray // route nodes with same path as current route
.filter(function (n) { return getValueForKey(n, 'path') === splitRoute[0]; });
if (potentialParents.length !== 0) {
splitRoute.shift(); // matched current parent, move on
route = splitRoute.join('/');
}
// get all children paths
var newRouteArray = getChildrenArray(routesArray);
if (route && parent && potentialParents.length === 0) {
return parent; // final route is not matched. assign parent from here
}
parent = potentialParents.sort(function (a, b) { return a.pos - b.pos; }).shift();
return getParent(newRouteArray, route, parent);
}
/**
* Helper for addPathToRoutes.
* @return whether path with same route and component exists
*/
function pathExists(routesArray, route, component, fullRoute) {
if (routesArray.length === 0) {
return false;
}
fullRoute = fullRoute ? fullRoute : route;
var sameRoute = false;
var splitRoute = route.split('/');
// don't treat positional parameters separately
if (splitRoute.length > 1 && splitRoute[1].indexOf(':') !== -1) {
var actualRoute = splitRoute.shift();
splitRoute[0] = actualRoute + "/" + splitRoute[0];
}
var repeatedRoutes = routesArray.filter(function (n) {
var currentRoute = getValueForKey(n, 'path');
var sameComponent = getValueForKey(n, 'component') === component;
sameRoute = currentRoute === splitRoute[0];
// Confirm that it's parents are the same
if (sameRoute && sameComponent) {
var path_1 = currentRoute;
var objExp = n.parent;
while (objExp) {
if (objExp.kind === ts.SyntaxKind.ObjectLiteralExpression) {
var currentParentPath = getValueForKey(objExp, 'path');
path_1 = currentParentPath ? currentParentPath + "/" + path_1 : path_1;
}
objExp = objExp.parent;
}
return path_1 === fullRoute;
}
return false;
});
if (sameRoute) {
splitRoute.shift(); // matched current parent, move on
route = splitRoute.join('/');
}
if (repeatedRoutes.length !== 0) {
return true; // new path will be repeating if inserted. report that path already exists
}
// all children paths
var newRouteArray = getChildrenArray(routesArray);
return pathExists(newRouteArray, route, component, fullRoute);
}
/**
* Helper for getParent and pathExists
* @return array with all nodes holding children array under routes
* in routesArray
*/
function getChildrenArray(routesArray) {
return routesArray.reduce(function (allRoutes, currRoute) { return allRoutes.concat(currRoute.getChildAt(1).getChildren()
.filter(function (n) { return n.kind === ts.SyntaxKind.PropertyAssignment
&& n.name.text === 'children'; })
.map(function (n) { return n.getChildAt(2).getChildAt(1); }) // syntaxList containing chilren paths
.reduce(function (childrenArray, currChild) { return childrenArray.concat(currChild.getChildren()
.filter(function (p) { return p.kind === ts.SyntaxKind.ObjectLiteralExpression; })); }, [])); }, []);
}
/**
* Helper method to get the path text or component
* @param objectLiteralNode
* @param key 'path' or 'component'
*/
function getValueForKey(objectLiteralNode, key) {
var currentNode = key === 'component' ? objectLiteralNode.getChildAt(1).getChildAt(2) :
objectLiteralNode.getChildAt(1).getChildAt(0);
return currentNode
&& currentNode.getChildAt(0)
&& currentNode.getChildAt(0).text === key
&& currentNode.getChildAt(2)
&& currentNode.getChildAt(2).text;
}
/**
* Helper method to get AST from file
* @param file
*/
function getRootNode(file) {
return ts.createSourceFile(file, fs.readFileSync(file).toString(), ts.ScriptTarget.Latest, true);
}
//# sourceMappingURL=/Users/hans/Sources/angular-cli/packages/@angular-cli/ast-tools/src/route-utils.js.map