UNPKG

@nx/js

Version:

The JS plugin for Nx contains executors and generators that provide the best experience for developing JavaScript and TypeScript projects.

266 lines (265 loc) • 10.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.resolveModuleByImport = resolveModuleByImport; exports.insertChange = insertChange; exports.replaceChange = replaceChange; exports.removeChange = removeChange; exports.insertImport = insertImport; exports.addGlobal = addGlobal; exports.getImport = getImport; exports.replaceNodeValue = replaceNodeValue; exports.addParameterToConstructor = addParameterToConstructor; exports.addMethod = addMethod; exports.findClass = findClass; exports.findNodes = findNodes; const ensure_typescript_1 = require("./ensure-typescript"); const devkit_1 = require("@nx/devkit"); const path_1 = require("path"); const get_source_nodes_1 = require("./get-source-nodes"); const normalizedAppRoot = devkit_1.workspaceRoot.replace(/\\/g, '/'); let tsModule; let compilerHost; /** * Find a module based on its import * * @param importExpr Import used to resolve to a module * @param filePath * @param tsConfigPath */ function resolveModuleByImport(importExpr, filePath, tsConfigPath) { compilerHost = compilerHost || getCompilerHost(tsConfigPath); const { options, host, moduleResolutionCache } = compilerHost; const { resolvedModule } = tsModule.resolveModuleName(importExpr, filePath, options, host, moduleResolutionCache); if (!resolvedModule) { return; } else { return resolvedModule.resolvedFileName.replace(`${normalizedAppRoot}/`, ''); } } function getCompilerHost(tsConfigPath) { const options = readTsConfigOptions(tsConfigPath); const host = tsModule.createCompilerHost(options, true); const moduleResolutionCache = tsModule.createModuleResolutionCache(devkit_1.workspaceRoot, host.getCanonicalFileName); return { options, host, moduleResolutionCache }; } function readTsConfigOptions(tsConfigPath) { if (!tsModule) { tsModule = require('typescript'); } const readResult = tsModule.readConfigFile(tsConfigPath, tsModule.sys.readFile); // we don't need to scan the files, we only care about options const host = { readDirectory: () => [], readFile: () => '', fileExists: tsModule.sys.fileExists, }; return tsModule.parseJsonConfigFileContent(readResult.config, host, (0, path_1.dirname)(tsConfigPath)).options; } function nodesByPosition(first, second) { return first.getStart() - second.getStart(); } function updateTsSourceFile(host, sourceFile, filePath) { const newFileContents = host.read(filePath).toString('utf-8'); return sourceFile.update(newFileContents, { newLength: newFileContents.length, span: { length: sourceFile.text.length, start: 0, }, }); } function insertChange(host, sourceFile, filePath, insertPosition, contentToInsert) { const content = host.read(filePath).toString(); const prefix = content.substring(0, insertPosition); const suffix = content.substring(insertPosition); host.write(filePath, `${prefix}${contentToInsert}${suffix}`); return updateTsSourceFile(host, sourceFile, filePath); } function replaceChange(host, sourceFile, filePath, insertPosition, contentToInsert, oldContent) { const content = host.read(filePath, 'utf-8'); const prefix = content.substring(0, insertPosition); const suffix = content.substring(insertPosition + oldContent.length); const text = content.substring(insertPosition, insertPosition + oldContent.length); if (text !== oldContent) { throw new Error(`Invalid replace: "${text}" != "${oldContent}".`); } host.write(filePath, `${prefix}${contentToInsert}${suffix}`); return updateTsSourceFile(host, sourceFile, filePath); } function removeChange(host, sourceFile, filePath, removePosition, contentToRemove) { const content = host.read(filePath).toString(); const prefix = content.substring(0, removePosition); const suffix = content.substring(removePosition + contentToRemove.length); host.write(filePath, `${prefix}${suffix}`); return updateTsSourceFile(host, sourceFile, filePath); } function insertImport(host, source, fileToEdit, symbolName, fileName, isDefault = false) { if (!tsModule) { tsModule = (0, ensure_typescript_1.ensureTypescript)(); } const rootNode = source; const allImports = findNodes(rootNode, tsModule.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 === tsModule.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, tsModule.SyntaxKind.Identifier)); if (findNodes(n, tsModule.SyntaxKind.AsteriskToken).length > 0) { importsAsterisk = true; } }); // if imports * from fileName, don't add symbolName if (importsAsterisk) { return source; } const importTextNodes = imports.filter((n) => n.text === symbolName); // insert import if it's not there if (importTextNodes.length === 0) { const fallbackPos = findNodes(relevantImports[0], tsModule.SyntaxKind.CloseBraceToken)[0].getStart() || findNodes(relevantImports[0], tsModule.SyntaxKind.FromKeyword)[0].getStart(); return insertAfterLastOccurrence(host, source, imports, `, ${symbolName}`, fileToEdit, fallbackPos); } return source; } // no such import declaration exists const useStrict = findNodes(rootNode, tsModule.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(host, source, allImports, toInsert, fileToEdit, fallbackPos, tsModule.SyntaxKind.StringLiteral); } function insertAfterLastOccurrence(host, sourceFile, nodes, toInsert, pathToFile, 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 insertChange(host, sourceFile, pathToFile, lastItemPosition, toInsert); } function addGlobal(host, source, modulePath, statement) { if (!tsModule) { tsModule = (0, ensure_typescript_1.ensureTypescript)(); } const allImports = findNodes(source, tsModule.SyntaxKind.ImportDeclaration); if (allImports.length > 0) { const lastImport = allImports[allImports.length - 1]; return insertChange(host, source, modulePath, lastImport.end + 1, `\n${statement}\n`); } else { return insertChange(host, source, modulePath, 0, `${statement}\n`); } } function getImport(source, predicate) { if (!tsModule) { tsModule = (0, ensure_typescript_1.ensureTypescript)(); } const allImports = findNodes(source, tsModule.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 }; }); } function replaceNodeValue(host, sourceFile, modulePath, node, content) { return replaceChange(host, sourceFile, modulePath, node.getStart(node.getSourceFile()), content, node.getText()); } function addParameterToConstructor(tree, source, modulePath, opts) { if (!tsModule) { tsModule = (0, ensure_typescript_1.ensureTypescript)(); } const clazz = findClass(source, opts.className); const constructor = clazz.members.filter((m) => m.kind === tsModule.SyntaxKind.Constructor)[0]; if (constructor) { throw new Error('Should be tested'); // TODO: check this } return addMethod(tree, source, modulePath, { className: opts.className, methodHeader: `constructor(${opts.param})`, }); } function addMethod(tree, source, modulePath, opts) { const clazz = findClass(source, opts.className); const body = opts.body ? ` ${opts.methodHeader} { ${opts.body} } ` : ` ${opts.methodHeader} {} `; return insertChange(tree, source, modulePath, clazz.end - 1, body); } function findClass(source, className, silent = false) { if (!tsModule) { tsModule = (0, ensure_typescript_1.ensureTypescript)(); } const nodes = (0, get_source_nodes_1.getSourceNodes)(source); const clazz = nodes.filter((n) => n.kind === tsModule.SyntaxKind.ClassDeclaration && n.name.text === className)[0]; if (!clazz && !silent) { throw new Error(`Cannot find class '${className}'.`); } return clazz; } 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; }