UNPKG

@yolkai/nx-workspace

Version:

Extensible Dev Tools for Monorepos

506 lines (505 loc) 18.1 kB
"use strict"; 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); }