@yolkai/nx-workspace
Version:
506 lines (505 loc) • 18.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const ts = require("typescript");
const stripJsonComments = require("strip-json-comments");
const fileutils_1 = require("./fileutils");
const tasks_1 = require("@angular-devkit/schematics/tasks");
const cli_config_utils_1 = require("./cli-config-utils");
const project_graph_1 = require("../core/project-graph");
const core_1 = require("@angular-devkit/core");
function nodesByPosition(first, second) {
return first.getStart() - second.getStart();
}
function insertAfterLastOccurrence(nodes, toInsert, file, fallbackPos, syntaxKind) {
// sort() has a side effect, so make a copy so that we won't overwrite the parent's object.
let lastItem = [...nodes].sort(nodesByPosition).pop();
if (!lastItem) {
throw new Error();
}
if (syntaxKind) {
lastItem = findNodes(lastItem, syntaxKind)
.sort(nodesByPosition)
.pop();
}
if (!lastItem && fallbackPos == undefined) {
throw new Error(`tried to insert ${toInsert} as first occurrence with no fallback position`);
}
const lastItemPosition = lastItem ? lastItem.getEnd() : fallbackPos;
return new InsertChange(file, lastItemPosition, toInsert);
}
function findNodes(node, kind, max = Infinity) {
if (!node || max == 0) {
return [];
}
const arr = [];
const hasMatch = Array.isArray(kind)
? kind.includes(node.kind)
: node.kind === kind;
if (hasMatch) {
arr.push(node);
max--;
}
if (max > 0) {
for (const child of node.getChildren()) {
findNodes(child, kind, max).forEach(node => {
if (max > 0) {
arr.push(node);
}
max--;
});
if (max <= 0) {
break;
}
}
}
return arr;
}
exports.findNodes = findNodes;
function getSourceNodes(sourceFile) {
const nodes = [sourceFile];
const result = [];
while (nodes.length > 0) {
const node = nodes.shift();
if (node) {
result.push(node);
if (node.getChildCount(sourceFile) >= 0) {
nodes.unshift(...node.getChildren());
}
}
}
return result;
}
exports.getSourceNodes = getSourceNodes;
class NoopChange {
constructor() {
this.type = 'noop';
this.description = 'No operation.';
this.order = Infinity;
this.path = null;
}
apply() {
return Promise.resolve();
}
}
exports.NoopChange = NoopChange;
class InsertChange {
constructor(path, pos, toAdd) {
this.path = path;
this.pos = pos;
this.toAdd = toAdd;
this.type = 'insert';
if (pos < 0) {
throw new Error('Negative positions are invalid');
}
this.description = `Inserted ${toAdd} into position ${pos} of ${path}`;
this.order = pos;
}
apply(host) {
return host.read(this.path).then(content => {
const prefix = content.substring(0, this.pos);
const suffix = content.substring(this.pos);
return host.write(this.path, `${prefix}${this.toAdd}${suffix}`);
});
}
}
exports.InsertChange = InsertChange;
class RemoveChange {
constructor(path, pos, toRemove) {
this.path = path;
this.pos = pos;
this.toRemove = toRemove;
this.type = 'remove';
if (pos < 0) {
throw new Error('Negative positions are invalid');
}
this.description = `Removed ${toRemove} into position ${pos} of ${path}`;
this.order = pos;
}
apply(host) {
return host.read(this.path).then(content => {
const prefix = content.substring(0, this.pos);
const suffix = content.substring(this.pos + this.toRemove.length);
return host.write(this.path, `${prefix}${suffix}`);
});
}
}
exports.RemoveChange = RemoveChange;
class ReplaceChange {
constructor(path, pos, oldText, newText) {
this.path = path;
this.pos = pos;
this.oldText = oldText;
this.newText = newText;
this.type = 'replace';
if (pos < 0) {
throw new Error('Negative positions are invalid');
}
this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`;
this.order = pos;
}
apply(host) {
return host.read(this.path).then(content => {
const prefix = content.substring(0, this.pos);
const suffix = content.substring(this.pos + this.oldText.length);
const text = content.substring(this.pos, this.pos + this.oldText.length);
if (text !== this.oldText) {
return Promise.reject(new Error(`Invalid replace: "${text}" != "${this.oldText}".`));
}
return host.write(this.path, `${prefix}${this.newText}${suffix}`);
});
}
}
exports.ReplaceChange = ReplaceChange;
function addParameterToConstructor(source, modulePath, opts) {
const clazz = findClass(source, opts.className);
const constructor = clazz.members.filter(m => m.kind === ts.SyntaxKind.Constructor)[0];
if (constructor) {
throw new Error('Should be tested');
}
else {
const methodHeader = `constructor(${opts.param})`;
return addMethod(source, modulePath, {
className: opts.className,
methodHeader,
body: null
});
}
}
exports.addParameterToConstructor = addParameterToConstructor;
function addMethod(source, modulePath, opts) {
const clazz = findClass(source, opts.className);
const body = opts.body
? `
${opts.methodHeader} {
${offset(opts.body, 1, false)}
}
`
: `
${opts.methodHeader} {}
`;
return [new InsertChange(modulePath, clazz.end - 1, offset(body, 1, true))];
}
exports.addMethod = addMethod;
function findClass(source, className, silent = false) {
const nodes = getSourceNodes(source);
const clazz = (nodes.filter(n => n.kind === ts.SyntaxKind.ClassDeclaration &&
n.name.text === className)[0]);
if (!clazz && !silent) {
throw new Error(`Cannot find class '${className}'`);
}
return clazz;
}
exports.findClass = findClass;
function offset(text, numberOfTabs, wrap) {
const lines = text
.trim()
.split('\n')
.map(line => {
let tabs = '';
for (let c = 0; c < numberOfTabs; ++c) {
tabs += ' ';
}
return `${tabs}${line}`;
})
.join('\n');
return wrap ? `\n${lines}\n` : lines;
}
exports.offset = offset;
function addIncludeToTsConfig(tsConfigPath, source, include) {
const includeKeywordPos = source.text.indexOf('"include":');
if (includeKeywordPos > -1) {
const includeArrayEndPos = source.text.indexOf(']', includeKeywordPos);
return [new InsertChange(tsConfigPath, includeArrayEndPos, include)];
}
else {
return [];
}
}
exports.addIncludeToTsConfig = addIncludeToTsConfig;
function getImport(source, predicate) {
const allImports = findNodes(source, ts.SyntaxKind.ImportDeclaration);
const matching = allImports.filter((i) => predicate(i.moduleSpecifier.getText()));
return matching.map((i) => {
const moduleSpec = i.moduleSpecifier
.getText()
.substring(1, i.moduleSpecifier.getText().length - 1);
const t = i.importClause.namedBindings.getText();
const bindings = t
.replace('{', '')
.replace('}', '')
.split(',')
.map(q => q.trim());
return { moduleSpec, bindings };
});
}
exports.getImport = getImport;
function addGlobal(source, modulePath, statement) {
const allImports = findNodes(source, ts.SyntaxKind.ImportDeclaration);
if (allImports.length > 0) {
const lastImport = allImports[allImports.length - 1];
return [
new InsertChange(modulePath, lastImport.end + 1, `\n${statement}\n`)
];
}
else {
return [new InsertChange(modulePath, 0, `${statement}\n`)];
}
}
exports.addGlobal = addGlobal;
function insert(host, modulePath, changes) {
if (changes.length < 1) {
return;
}
const recorder = host.beginUpdate(modulePath);
for (const change of changes) {
if (change.type === 'insert') {
recorder.insertLeft(change.pos, change.toAdd);
}
else if (change.type === 'remove') {
recorder.remove(change.pos - 1, change.toRemove.length + 1);
}
else if (change.type === 'noop') {
// do nothing
}
else if (change.type === 'replace') {
const action = change;
recorder.remove(action.pos, action.oldText.length);
recorder.insertLeft(action.pos, action.newText);
}
else {
throw new Error(`Unexpected Change '${change.constructor.name}'`);
}
}
host.commitUpdate(recorder);
}
exports.insert = insert;
/**
* This method is specifically for reading JSON files in a Tree
* @param host The host tree
* @param path The path to the JSON file
* @returns The JSON data in the file.
*/
function readJsonInTree(host, path) {
if (!host.exists(path)) {
throw new Error(`Cannot find ${path}`);
}
const contents = stripJsonComments(host.read(path).toString('utf-8'));
try {
return JSON.parse(contents);
}
catch (e) {
throw new Error(`Cannot parse ${path}: ${e.message}`);
}
}
exports.readJsonInTree = readJsonInTree;
/**
* Method for utilizing the project graph in schematics
*/
function getProjectGraphFromHost(host) {
const workspaceJson = readJsonInTree(host, cli_config_utils_1.getWorkspacePath(host));
const nxJson = readJsonInTree(host, '/nx.json');
const fileRead = (f) => host.read(f).toString();
const workspaceFiles = [];
const mtime = +Date.now();
workspaceFiles.push(...allFilesInDirInHost(host, core_1.normalize(''), { recursive: false }).map(f => getFileDataInHost(host, f, mtime)));
workspaceFiles.push(...allFilesInDirInHost(host, core_1.normalize('tools')).map(f => getFileDataInHost(host, f, mtime)));
// Add files for workspace projects
Object.keys(workspaceJson.projects).forEach(projectName => {
const project = workspaceJson.projects[projectName];
workspaceFiles.push(...allFilesInDirInHost(host, core_1.normalize(project.root)).map(f => getFileDataInHost(host, f, mtime)));
});
return project_graph_1.createProjectGraph(workspaceJson, nxJson, workspaceFiles, fileRead, false);
}
exports.getProjectGraphFromHost = getProjectGraphFromHost;
function getFileDataInHost(host, path, mtime) {
return {
file: path,
ext: core_1.extname(core_1.normalize(path)),
mtime
};
}
exports.getFileDataInHost = getFileDataInHost;
function allFilesInDirInHost(host, path, options = { recursive: true }) {
const dir = host.getDir(path);
const res = [];
dir.subfiles.forEach(p => {
res.push(core_1.join(path, p));
});
if (!options.recursive) {
return res;
}
dir.subdirs.forEach(p => {
res.push(...allFilesInDirInHost(host, core_1.join(path, p)));
});
return res;
}
exports.allFilesInDirInHost = allFilesInDirInHost;
/**
* This method is specifically for updating JSON in a Tree
* @param path Path of JSON file in the Tree
* @param callback Manipulation of the JSON data
* @returns A rule which updates a JSON file file in a Tree
*/
function updateJsonInTree(path, callback) {
return (host, context) => {
if (!host.exists(path)) {
host.create(path, fileutils_1.serializeJson(callback({}, context)));
return host;
}
host.overwrite(path, fileutils_1.serializeJson(callback(readJsonInTree(host, path), context)));
return host;
};
}
exports.updateJsonInTree = updateJsonInTree;
function updateWorkspaceInTree(callback) {
return (host, context) => {
const path = cli_config_utils_1.getWorkspacePath(host);
host.overwrite(path, fileutils_1.serializeJson(callback(readJsonInTree(host, path), context)));
return host;
};
}
exports.updateWorkspaceInTree = updateWorkspaceInTree;
function readWorkspace(host) {
const path = cli_config_utils_1.getWorkspacePath(host);
return readJsonInTree(host, path);
}
exports.readWorkspace = readWorkspace;
let installAdded = false;
function addDepsToPackageJson(deps, devDeps, addInstall = true) {
return updateJsonInTree('package.json', (json, context) => {
json.dependencies = Object.assign({}, (json.dependencies || {}), deps, (json.dependencies || {}));
json.devDependencies = Object.assign({}, (json.devDependencies || {}), devDeps, (json.devDependencies || {}));
if (addInstall && !installAdded) {
context.addTask(new tasks_1.NodePackageInstallTask());
installAdded = true;
}
return json;
});
}
exports.addDepsToPackageJson = addDepsToPackageJson;
function updatePackageJsonDependencies(deps, devDeps, addInstall = true) {
return updateJsonInTree('package.json', (json, context) => {
json.dependencies = Object.assign({}, (json.dependencies || {}), deps);
json.devDependencies = Object.assign({}, (json.devDependencies || {}), devDeps);
if (addInstall && !installAdded) {
context.addTask(new tasks_1.NodePackageInstallTask());
installAdded = true;
}
return json;
});
}
exports.updatePackageJsonDependencies = updatePackageJsonDependencies;
function getProjectConfig(host, name) {
const workspaceJson = readJsonInTree(host, cli_config_utils_1.getWorkspacePath(host));
const projectConfig = workspaceJson.projects[name];
if (!projectConfig) {
throw new Error(`Cannot find project '${name}'`);
}
else {
return projectConfig;
}
}
exports.getProjectConfig = getProjectConfig;
function createOrUpdate(host, path, content) {
if (host.exists(path)) {
host.overwrite(path, content);
}
else {
host.create(path, content);
}
}
exports.createOrUpdate = createOrUpdate;
function insertImport(source, fileToEdit, symbolName, fileName, isDefault = false) {
const rootNode = source;
const allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
// get nodes that map to import statements from the file fileName
const relevantImports = allImports.filter(node => {
// StringLiteral of the ImportDeclaration is the import file (fileName in this case).
const importFiles = node
.getChildren()
.filter(child => child.kind === ts.SyntaxKind.StringLiteral)
.map(n => n.text);
return importFiles.filter(file => file === fileName).length === 1;
});
if (relevantImports.length > 0) {
let importsAsterisk = false;
// imports from import file
const imports = [];
relevantImports.forEach(n => {
Array.prototype.push.apply(imports, findNodes(n, ts.SyntaxKind.Identifier));
if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) {
importsAsterisk = true;
}
});
// if imports * from fileName, don't add symbolName
if (importsAsterisk) {
return new NoopChange();
}
const importTextNodes = imports.filter(n => n.text === symbolName);
// insert import if it's not there
if (importTextNodes.length === 0) {
const fallbackPos = findNodes(relevantImports[0], ts.SyntaxKind.CloseBraceToken)[0].getStart() ||
findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart();
return insertAfterLastOccurrence(imports, `, ${symbolName}`, fileToEdit, fallbackPos);
}
return new NoopChange();
}
// no such import declaration exists
const useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral).filter((n) => n.text === 'use strict');
let fallbackPos = 0;
if (useStrict.length > 0) {
fallbackPos = useStrict[0].end;
}
const open = isDefault ? '' : '{ ';
const close = isDefault ? '' : ' }';
// if there are no imports or 'use strict' statement, insert import at beginning of file
const insertAtBeginning = allImports.length === 0 && useStrict.length === 0;
const separator = insertAtBeginning ? '' : ';\n';
const toInsert = `${separator}import ${open}${symbolName}${close}` +
` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`;
return insertAfterLastOccurrence(allImports, toInsert, fileToEdit, fallbackPos, ts.SyntaxKind.StringLiteral);
}
exports.insertImport = insertImport;
function replaceNodeValue(host, modulePath, node, content) {
insert(host, modulePath, [
new ReplaceChange(modulePath, node.getStart(node.getSourceFile()), node.getFullText(), content)
]);
}
exports.replaceNodeValue = replaceNodeValue;
function renameSyncInTree(tree, from, to, cb) {
if (!tree.exists(from)) {
cb(`Path: ${from} does not exist`);
}
else if (tree.exists(to)) {
cb(`Path: ${to} already exists`);
}
else {
renameFile(tree, from, to);
cb(null);
}
}
exports.renameSyncInTree = renameSyncInTree;
function renameDirSyncInTree(tree, from, to, cb) {
const dir = tree.getDir(from);
if (!dirExists(dir)) {
cb(`Path: ${from} does not exist`);
return;
}
dir.visit(path => {
const destination = path.replace(from, to);
renameFile(tree, path, destination);
});
cb(null);
}
exports.renameDirSyncInTree = renameDirSyncInTree;
function dirExists(dir) {
return dir.subdirs.length + dir.subfiles.length !== 0;
}
function renameFile(tree, from, to) {
const buffer = tree.read(from);
if (!buffer) {
return;
}
tree.create(to, buffer.toString());
tree.delete(from);
}