UNPKG

@nstudio/angular

Version:

Angular Plugin for xplat

584 lines (583 loc) 25.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.addToCollection = addToCollection; exports.isStandalone = isStandalone; exports.getDecoratorMetadata = getDecoratorMetadata; exports._addSymbolToNgModuleMetadata = _addSymbolToNgModuleMetadata; exports.removeFromNgModule = removeFromNgModule; exports.addImportToComponent = addImportToComponent; exports.addImportToDirective = addImportToDirective; exports.addImportToPipe = addImportToPipe; exports.addImportToModule = addImportToModule; exports.addImportToTestBed = addImportToTestBed; exports.addDeclarationsToTestBed = addDeclarationsToTestBed; exports.replaceIntoToTestBed = replaceIntoToTestBed; exports.getBootstrapComponent = getBootstrapComponent; exports.addRouteToNgModule = addRouteToNgModule; exports.addProviderToBootstrapApplication = addProviderToBootstrapApplication; exports.addProviderToModule = addProviderToModule; exports.addProviderToComponent = addProviderToComponent; exports.addDeclarationToModule = addDeclarationToModule; exports.addEntryComponents = addEntryComponents; exports.readBootstrapInfo = readBootstrapInfo; exports.getDecoratorPropertyValueNode = getDecoratorPropertyValueNode; exports.getTsSourceFile = getTsSourceFile; const ensure_typescript_1 = require("@nx/js/src/utils/typescript/ensure-typescript"); const js_1 = require("@nx/js"); const ts = require("typescript"); const js_2 = require("@nx/js"); const path_1 = require("path"); const devkit_1 = require("@nx/devkit"); let tsModule; function addToCollection(tree, source, barrelIndexPath, symbolName, insertSpaces = '') { const collection = getCollection(source); if (!collection) return source; // if (!collection) return [new NoopChange()]; // return [new NoopChange()]; // console.log('collection.hasTrailingComma:', collection.hasTrailingComma); if (collection.hasTrailingComma || collection.length === 0) { return (0, js_2.insertChange)(tree, source, barrelIndexPath, collection.end, symbolName); } else { return (0, js_2.insertChange)(tree, source, barrelIndexPath, collection.end, `,\n${insertSpaces}${symbolName}`); } } function getCollection(source) { const allCollections = (0, js_1.findNodes)(source, ts.SyntaxKind.ArrayLiteralExpression); // console.log('allCollections:', allCollections); // allCollections.forEach((i: ts.ArrayLiteralExpression) => { // console.log('getText:',i.getText()); // }); // always assume the first is the standard components collection // other type could be an entry components collection which is not supported (yet) if (allCollections && allCollections.length) { return allCollections[0].elements; } return null; } function _angularImportsFromNode(node, _sourceFile) { if (!tsModule) { tsModule = (0, ensure_typescript_1.ensureTypescript)(); } const ms = node.moduleSpecifier; let modulePath; switch (ms.kind) { case tsModule.SyntaxKind.StringLiteral: modulePath = ms.text; break; default: return {}; } if (!modulePath.startsWith('@angular/')) { return {}; } if (node.importClause) { if (node.importClause.name) { // This is of the form `import Name from 'path'`. Ignore. return {}; } else if (node.importClause.namedBindings) { const nb = node.importClause.namedBindings; if (nb.kind == tsModule.SyntaxKind.NamespaceImport) { // This is of the form `import * as name from 'path'`. Return `name.`. return { [`${nb.name.text}.`]: modulePath, }; } else { // This is of the form `import {a,b,c} from 'path'` const namedImports = nb; return namedImports.elements .map((is) => is.propertyName ? is.propertyName.text : is.name.text) .reduce((acc, curr) => { acc[curr] = modulePath; return acc; }, {}); } } return {}; } else { // This is of the form `import 'path';`. Nothing to do. return {}; } } /** * Check if the Component, Directive or Pipe is standalone * @param sourceFile TS Source File containing the token to check * @param decoratorName The type of decorator to check (Component, Directive, Pipe) */ function isStandalone(sourceFile, decoratorName) { const decoratorMetadata = getDecoratorMetadata(sourceFile, decoratorName, '@angular/core'); return decoratorMetadata.some((node) => node.getText().includes('standalone: true')); } function getDecoratorMetadata(source, identifier, module) { if (!tsModule) { tsModule = (0, ensure_typescript_1.ensureTypescript)(); } const angularImports = (0, js_1.findNodes)(source, tsModule.SyntaxKind.ImportDeclaration) .map((node) => _angularImportsFromNode(node, source)) .reduce((acc, current) => { for (const key of Object.keys(current)) { acc[key] = current[key]; } return acc; }, {}); return (0, js_2.getSourceNodes)(source) .filter((node) => { return (node.kind == tsModule.SyntaxKind.Decorator && node.expression.kind == tsModule.SyntaxKind.CallExpression); }) .map((node) => node.expression) .filter((expr) => { if (expr.expression.kind == tsModule.SyntaxKind.Identifier) { const id = expr.expression; return (id.getFullText(source) == identifier && angularImports[id.getFullText(source)] === module); } else if (expr.expression.kind == tsModule.SyntaxKind.PropertyAccessExpression) { // This covers foo.NgModule when importing * as foo. const paExpr = expr.expression; // If the left expression is not an identifier, just give up at that point. if (paExpr.expression.kind !== tsModule.SyntaxKind.Identifier) { return false; } const id = paExpr.name.text; const moduleId = paExpr.expression.getText(source); return id === identifier && angularImports[`${moduleId}.`] === module; } return false; }) .filter((expr) => expr.arguments[0] && expr.arguments[0].kind == tsModule.SyntaxKind.ObjectLiteralExpression) .map((expr) => expr.arguments[0]); } function _addSymbolToDecoratorMetadata(host, source, filePath, metadataField, expression, decoratorName) { const nodes = getDecoratorMetadata(source, decoratorName, '@angular/core'); let node = nodes[0]; // tslint:disable-line:no-any // Find the decorator declaration. if (!node) { return source; } if (!tsModule) { tsModule = (0, ensure_typescript_1.ensureTypescript)(); } // Get all the children property assignment of object literals. const matchingProperties = node.properties .filter((prop) => prop.kind == tsModule.SyntaxKind.PropertyAssignment) // Filter out every fields that's not "metadataField". Also handles string literals // (but not expressions). .filter((prop) => { const name = prop.name; switch (name.kind) { case tsModule.SyntaxKind.Identifier: return name.getText(source) == metadataField; case tsModule.SyntaxKind.StringLiteral: return name.text == metadataField; } return false; }); // Get the last node of the array literal. if (!matchingProperties) { return source; } if (matchingProperties.length == 0) { // We haven't found the field in the metadata declaration. Insert a new field. const expr = node; let position; let toInsert; if (expr.properties.length == 0) { position = expr.getEnd() - 1; toInsert = ` ${metadataField}: [${expression}]\n`; } else { node = expr.properties[expr.properties.length - 1]; position = node.getEnd(); // Get the indentation of the last element, if any. const text = node.getFullText(source); if (text.match('^\r?\r?\n')) { toInsert = `,${text.match(/^\r?\n\s+/)[0]}${metadataField}: [${expression}]`; } else { toInsert = `, ${metadataField}: [${expression}]`; } } return (0, js_2.insertChange)(host, source, filePath, position, toInsert); } const assignment = matchingProperties[0]; // If it's not an array, nothing we can do really. if (assignment.initializer.kind !== tsModule.SyntaxKind.ArrayLiteralExpression) { return source; } const arrLiteral = assignment.initializer; if (arrLiteral.elements.length == 0) { // Forward the property. node = arrLiteral; } else { node = arrLiteral.elements; } if (!node) { console.log('No app module found. Please add your new class to your component.'); return source; } const isArray = Array.isArray(node); if (isArray) { const nodeArray = node; const symbolsArray = nodeArray.map((node) => node.getText()); if (symbolsArray.includes(expression)) { return source; } node = node[node.length - 1]; } let toInsert; let position = node.getEnd(); if (!isArray && node.kind == tsModule.SyntaxKind.ObjectLiteralExpression) { // We haven't found the field in the metadata declaration. Insert a new // field. const expr = node; if (expr.properties.length == 0) { position = expr.getEnd() - 1; toInsert = ` ${metadataField}: [${expression}]\n`; } else { node = expr.properties[expr.properties.length - 1]; position = node.getEnd(); // Get the indentation of the last element, if any. const text = node.getFullText(source); if (text.match('^\r?\r?\n')) { toInsert = `,${text.match(/^\r?\n\s+/)[0]}${metadataField}: [${expression}]`; } else { toInsert = `, ${metadataField}: [${expression}]`; } } } else if (!isArray && node.kind == tsModule.SyntaxKind.ArrayLiteralExpression) { // We found the field but it's empty. Insert it just before the `]`. position--; toInsert = `${expression}`; } else { // Get the indentation of the last element, if any. const text = node.getFullText(source); if (text.match(/^\r?\n/)) { toInsert = `,${text.match(/^\r?\n(\r?)\s+/)[0]}${expression}`; } else { toInsert = `, ${expression}`; } } return (0, js_2.insertChange)(host, source, filePath, position, toInsert); } function _addSymbolToNgModuleMetadata(host, source, ngModulePath, metadataField, expression) { return _addSymbolToDecoratorMetadata(host, source, ngModulePath, metadataField, expression, 'NgModule'); } function removeFromNgModule(host, source, modulePath, property) { const nodes = getDecoratorMetadata(source, 'NgModule', '@angular/core'); let node = nodes[0]; // tslint:disable-line:no-any // Find the decorator declaration. if (!node) { return source; } // Get all the children property assignment of object literals. const matchingProperty = getMatchingProperty(source, property, 'NgModule', '@angular/core'); if (matchingProperty) { return (0, js_2.removeChange)(host, source, modulePath, matchingProperty.getStart(source), matchingProperty.getFullText(source)); } } /** * Add an import to a Standalone Component * @param host Virtual Tree * @param source TS Source File containing the Component * @param componentPath The path to the Component * @param symbolName The import to add to the Component */ function addImportToComponent(host, source, componentPath, symbolName) { return _addSymbolToDecoratorMetadata(host, source, componentPath, 'imports', symbolName, 'Component'); } /** * Add an import to a Standalone Directive * @param host Virtual Tree * @param source TS Source File containing the Directive * @param directivePath The path to the Directive * @param symbolName The import to add to the Directive */ function addImportToDirective(host, source, directivePath, symbolName) { return _addSymbolToDecoratorMetadata(host, source, directivePath, 'imports', symbolName, 'Directive'); } /** * Add an import to a Standalone Pipe * @param host Virtual Tree * @param source TS Source File containing the Pipe * @param pipePath The path to the Pipe * @param symbolName The import to add to the Pipe */ function addImportToPipe(host, source, pipePath, symbolName) { return _addSymbolToDecoratorMetadata(host, source, pipePath, 'imports', symbolName, 'Pipe'); } /** * Add an import to an NgModule * @param host Virtual Tree * @param source TS Source File containing the NgModule * @param modulePath The path to the NgModule * @param symbolName The import to add to the NgModule */ function addImportToModule(host, source, modulePath, symbolName) { return _addSymbolToNgModuleMetadata(host, source, modulePath, 'imports', symbolName); } function addImportToTestBed(host, source, specPath, symbolName) { if (!tsModule) { tsModule = (0, ensure_typescript_1.ensureTypescript)(); } const allCalls = ((0, js_1.findNodes)(source, tsModule.SyntaxKind.CallExpression)); const configureTestingModuleObjectLiterals = allCalls .filter((c) => c.expression.kind === tsModule.SyntaxKind.PropertyAccessExpression) .filter((c) => c.expression.name.getText(source) === 'configureTestingModule') .map((c) => c.arguments[0].kind === tsModule.SyntaxKind.ObjectLiteralExpression ? c.arguments[0] : null); if (configureTestingModuleObjectLiterals.length > 0) { const startPosition = configureTestingModuleObjectLiterals[0] .getFirstToken(source) .getEnd(); return (0, js_2.insertChange)(host, source, specPath, startPosition, `imports: [${symbolName}], `); } return source; } function addDeclarationsToTestBed(host, source, specPath, symbolName) { if (!tsModule) { tsModule = (0, ensure_typescript_1.ensureTypescript)(); } const allCalls = ((0, js_1.findNodes)(source, tsModule.SyntaxKind.CallExpression)); const configureTestingModuleObjectLiterals = allCalls .filter((c) => c.expression.kind === tsModule.SyntaxKind.PropertyAccessExpression) .filter((c) => c.expression.name.getText(source) === 'configureTestingModule') .map((c) => c.arguments[0].kind === tsModule.SyntaxKind.ObjectLiteralExpression ? c.arguments[0] : null); if (configureTestingModuleObjectLiterals.length > 0) { const startPosition = configureTestingModuleObjectLiterals[0] .getFirstToken(source) .getEnd(); return (0, js_2.insertChange)(host, source, specPath, startPosition, `declarations: [${symbolName.join(',')}], `); } return source; } function replaceIntoToTestBed(host, source, specPath, newSymbol, previousSymbol) { if (!tsModule) { tsModule = (0, ensure_typescript_1.ensureTypescript)(); } const allCalls = ((0, js_1.findNodes)(source, tsModule.SyntaxKind.CallExpression)); const configureTestingModuleObjectLiterals = allCalls .filter((c) => c.expression.kind === tsModule.SyntaxKind.PropertyAccessExpression) .filter((c) => c.expression.name.getText(source) === 'configureTestingModule') .map((c) => c.arguments[0].kind === tsModule.SyntaxKind.ObjectLiteralExpression ? c.arguments[0] : null); if (configureTestingModuleObjectLiterals.length > 0) { const startPosition = configureTestingModuleObjectLiterals[0] .getFirstToken(source) .getEnd(); return (0, js_2.replaceChange)(host, source, specPath, startPosition, newSymbol, previousSymbol); } return source; } function getBootstrapComponent(source, moduleClassName) { const bootstrap = getMatchingProperty(source, 'bootstrap', 'NgModule', '@angular/core'); if (!bootstrap) { throw new Error(`Cannot find bootstrap components in '${moduleClassName}'`); } const c = bootstrap.getChildren(); const nodes = c[c.length - 1].getChildren(); const bootstrapComponent = nodes.slice(1, nodes.length - 1)[0]; if (!bootstrapComponent) { throw new Error(`Cannot find bootstrap components in '${moduleClassName}'`); } return bootstrapComponent.getText(); } function getMatchingProperty(source, property, identifier, module) { const nodes = getDecoratorMetadata(source, identifier, module); let node = nodes[0]; // tslint:disable-line:no-any if (!node) return null; // Get all the children property assignment of object literals. return getMatchingObjectLiteralElement(node, source, property); } function addRouteToNgModule(host, ngModulePath, source, route) { const routes = getListOfRoutes(source); if (!routes) return source; if (routes.hasTrailingComma || routes.length === 0) { return (0, js_2.insertChange)(host, source, ngModulePath, routes.end, route); } else { return (0, js_2.insertChange)(host, source, ngModulePath, routes.end, `, ${route}`); } } function getListOfRoutes(source) { if (!tsModule) { tsModule = (0, ensure_typescript_1.ensureTypescript)(); } const imports = getMatchingProperty(source, 'imports', 'NgModule', '@angular/core'); if ((imports === null || imports === void 0 ? void 0 : imports.initializer.kind) === tsModule.SyntaxKind.ArrayLiteralExpression) { const a = imports.initializer; for (const e of a.elements) { if (e.kind === tsModule.SyntaxKind.CallExpression) { const ee = e; const text = ee.expression.getText(source); if ((text === 'RouterModule.forRoot' || text === 'RouterModule.forChild') && ee.arguments.length > 0) { const routes = ee.arguments[0]; if (routes.kind === tsModule.SyntaxKind.ArrayLiteralExpression) { return routes.elements; } else if (routes.kind === tsModule.SyntaxKind.Identifier) { // find the array expression const variableDeclarations = (0, js_1.findNodes)(source, tsModule.SyntaxKind.VariableDeclaration); const routesDeclaration = variableDeclarations.find((x) => { return x.name.getText() === routes.getText(); }); if (routesDeclaration) { return routesDeclaration.initializer.elements; } } } } } } return null; } /** * Add a provider to bootstrapApplication call for Standalone Applications * @param tree Virtual Tree * @param filePath Path to the file containing the bootstrapApplication call * @param providerToAdd Provider to add */ function addProviderToBootstrapApplication(tree, filePath, providerToAdd) { (0, ensure_typescript_1.ensureTypescript)(); const { tsquery } = require('@phenomnomnominal/tsquery'); const PROVIDERS_ARRAY_SELECTOR = 'CallExpression:has(Identifier[name=bootstrapApplication]) ObjectLiteralExpression > PropertyAssignment:has(Identifier[name=providers]) > ArrayLiteralExpression'; const fileContents = tree.read(filePath, 'utf-8'); const ast = tsquery.ast(fileContents); const providersArrayNodes = tsquery(ast, PROVIDERS_ARRAY_SELECTOR, { visitAllChildren: true, }); if (providersArrayNodes.length === 0) { throw new Error(`Providers does not exist in the bootstrapApplication call within ${filePath}.`); } const arrayNode = providersArrayNodes[0]; const newFileContents = `${fileContents.slice(0, arrayNode.getStart() + 1)}${providerToAdd},${fileContents.slice(arrayNode.getStart() + 1, fileContents.length)}`; tree.write(filePath, newFileContents); } /** * Add a provider to an NgModule * @param host Virtual Tree * @param source TS Source File containing the NgModule * @param modulePath Path to the NgModule * @param symbolName The provider to add */ function addProviderToModule(host, source, modulePath, symbolName) { return _addSymbolToNgModuleMetadata(host, source, modulePath, 'providers', symbolName); } /** * Add a provider to a Standalone Component * @param host Virtual Tree * @param source TS Source File containing the Component * @param componentPath Path to the Component * @param symbolName The provider to add */ function addProviderToComponent(host, source, componentPath, symbolName) { return _addSymbolToDecoratorMetadata(host, source, componentPath, 'providers', symbolName, 'Component'); } function addDeclarationToModule(host, source, modulePath, symbolName) { return _addSymbolToNgModuleMetadata(host, source, modulePath, 'declarations', symbolName); } function addEntryComponents(host, source, modulePath, symbolName) { return _addSymbolToNgModuleMetadata(host, source, modulePath, 'entryComponents', symbolName); } function readBootstrapInfo(host, app) { if (!tsModule) { tsModule = (0, ensure_typescript_1.ensureTypescript)(); } const config = (0, devkit_1.readProjectConfiguration)(host, app); let mainPath; try { mainPath = config.targets.build.options.main; } catch (e) { throw new Error('Main file cannot be located'); } if (!host.exists(mainPath)) { throw new Error('Main file cannot be located'); } const mainSource = host.read(mainPath).toString('utf-8'); const main = tsModule.createSourceFile(mainPath, mainSource, tsModule.ScriptTarget.Latest, true); const moduleImports = (0, js_2.getImport)(main, (s) => s.indexOf('.module') > -1); if (moduleImports.length !== 1) { throw new Error(`main.ts can only import a single module`); } const moduleImport = moduleImports[0]; const moduleClassName = moduleImport.bindings.filter((b) => b.endsWith('Module'))[0]; const modulePath = `${(0, path_1.join)((0, path_1.dirname)(mainPath), moduleImport.moduleSpec)}.ts`; if (!host.exists(modulePath)) { throw new Error(`Cannot find '${modulePath}'`); } const moduleSourceText = host.read(modulePath).toString('utf-8'); const moduleSource = tsModule.createSourceFile(modulePath, moduleSourceText, tsModule.ScriptTarget.Latest, true); const bootstrapComponentClassName = getBootstrapComponent(moduleSource, moduleClassName); const bootstrapComponentFileName = `./${(0, path_1.join)((0, path_1.dirname)(moduleImport.moduleSpec), `${(0, devkit_1.names)(bootstrapComponentClassName.substring(0, bootstrapComponentClassName.length - 9)).fileName}.component`)}`; return { moduleSpec: moduleImport.moduleSpec, mainPath, modulePath, moduleSource, moduleClassName, bootstrapComponentClassName, bootstrapComponentFileName, }; } function getDecoratorPropertyValueNode(host, modulePath, identifier, property, module) { if (!tsModule) { tsModule = (0, ensure_typescript_1.ensureTypescript)(); } const moduleSourceText = host.read(modulePath).toString('utf-8'); const moduleSource = tsModule.createSourceFile(modulePath, moduleSourceText, tsModule.ScriptTarget.Latest, true); const templateNode = getMatchingProperty(moduleSource, property, identifier, module); return templateNode.getChildAt(templateNode.getChildCount() - 1); } function getMatchingObjectLiteralElement(node, source, property) { if (!tsModule) { tsModule = (0, ensure_typescript_1.ensureTypescript)(); } return (node.properties .filter((prop) => prop.kind == tsModule.SyntaxKind.PropertyAssignment) // Filter out every fields that's not "metadataField". Also handles string literals // (but not expressions). .filter((prop) => { const name = prop.name; switch (name.kind) { case tsModule.SyntaxKind.Identifier: return name.getText(source) === property; case tsModule.SyntaxKind.StringLiteral: return name.text === property; } return false; })[0]); } function getTsSourceFile(host, path) { if (!tsModule) { tsModule = (0, ensure_typescript_1.ensureTypescript)(); } const buffer = host.read(path); if (!buffer) { throw new Error(`Could not read TS file (${path}).`); } const content = buffer.toString(); const source = tsModule.createSourceFile(path, content, tsModule.ScriptTarget.Latest, true); return source; }