import { addGlobal, Change, findNodes, InsertChange, ReplaceChange } from '@nrwl/workspace/src/utils/ast-utils'; import * as ts from 'typescript'; import { SchematicContext } from '@angular-devkit/schematics'; export function findMainRenderStatement( source: ts.SourceFile ): ts.CallExpression | null { // 1. Try to find ReactDOM.render. const calls = findNodes( source, ts.SyntaxKind.CallExpression ) as ts.CallExpression[]; for (const expr of calls) { const inner = expr.expression; if ( ts.isPropertyAccessExpression(inner) && /ReactDOM/i.test(inner.expression.getText()) && inner.name.getText() === 'render' ) { return expr; } } // 2. Try to find render from 'react-dom'. const imports = findNodes( source, ts.SyntaxKind.ImportDeclaration ) as ts.ImportDeclaration[]; const hasRenderImport = imports.some( i => i.moduleSpecifier.getText().includes('react-dom') && /\brender\b/.test(i.importClause.namedBindings.getText()) ); if (hasRenderImport) { const calls = findNodes( source, ts.SyntaxKind.CallExpression ) as ts.CallExpression[]; for (const expr of calls) { if (expr.expression.getText() === 'render') { return expr; } } } return null; } export function findDefaultExport(source: ts.SourceFile): ts.Node | null { return ( findDefaultExportDeclaration(source) || findDefaultClassOrFunction(source) ); } export function findDefaultExportDeclaration( source: ts.SourceFile ): ts.Node | null { const identifier = findDefaultExportIdentifier(source); if (identifier) { const variables = findNodes(source, ts.SyntaxKind.VariableDeclaration); const fns = findNodes(source, ts.SyntaxKind.FunctionDeclaration); const all = variables.concat(fns) as Array< ts.VariableDeclaration | ts.FunctionDeclaration >; const exported = all .filter(x => x.name.kind === ts.SyntaxKind.Identifier) .find(x => (x.name as ts.Identifier).text === identifier.text); return exported || null; } else { return null; } } export function findDefaultExportIdentifier( source: ts.SourceFile ): ts.Identifier | null { const exports = findNodes( source, ts.SyntaxKind.ExportAssignment ) as ts.ExportAssignment[]; const identifier = exports .map(x => x.expression) .find(x => x.kind === ts.SyntaxKind.Identifier) as ts.Identifier; return identifier || null; } export function findDefaultClassOrFunction( source: ts.SourceFile ): ts.FunctionDeclaration | ts.ClassDeclaration | null { const fns = findNodes( source, ts.SyntaxKind.FunctionDeclaration ) as ts.FunctionDeclaration[]; const cls = findNodes( source, ts.SyntaxKind.ClassDeclaration ) as ts.ClassDeclaration[]; return ( fns.find(hasDefaultExportModifier) || cls.find(hasDefaultExportModifier) || null ); } function hasDefaultExportModifier( x: ts.ClassDeclaration | ts.FunctionDeclaration ) { return ( x.modifiers.some(m => m.kind === ts.SyntaxKind.ExportKeyword) && x.modifiers.some(m => m.kind === ts.SyntaxKind.DefaultKeyword) ); } export function findComponentImportPath( componentName: string, source: ts.SourceFile ) { const allImports = findNodes( source, ts.SyntaxKind.ImportDeclaration ) as ts.ImportDeclaration[]; const matching = allImports.filter((i: ts.ImportDeclaration) => { return ( i.importClause && i.importClause.name && i.importClause.name.getText() === componentName ); }); if (matching.length === 0) { return null; } const appImport = matching[0]; return appImport.moduleSpecifier.getText().replace(/['"]/g, ''); } export function findElements(source: ts.SourceFile, tagName: string) { const nodes = findNodes(source, [ ts.SyntaxKind.JsxSelfClosingElement, ts.SyntaxKind.JsxOpeningElement ]); return nodes.filter(node => isTag(tagName, node)); } export function findClosestOpening(tagName: string, node: ts.Node) { if (!node) { return null; } if (isTag(tagName, node)) { return node; } else { return findClosestOpening(tagName, node.parent); } } export function isTag(tagName: string, node: ts.Node) { if (ts.isJsxOpeningLikeElement(node)) { return ( node.tagName.kind === ts.SyntaxKind.Identifier && node.tagName.text === tagName ); } if (ts.isJsxElement(node) && node.openingElement) { return ( node.openingElement.tagName.kind === ts.SyntaxKind.Identifier && node.openingElement.tagName.getText() === tagName ); } return false; } export function addInitialRoutes( sourcePath: string, source: ts.SourceFile, context: SchematicContext ): Change[] { const jsxClosingElements = findNodes(source, [ ts.SyntaxKind.JsxClosingElement, ts.SyntaxKind.JsxClosingFragment ]); const outerMostJsxClosing = jsxClosingElements[jsxClosingElements.length - 1]; if (!outerMostJsxClosing) { context.logger.warn( `Could not find JSX elements in ${sourcePath}; Skipping insert routes` ); return []; } const insertRoutes = new InsertChange( sourcePath, outerMostJsxClosing.getStart(), ` {/* START: routes */} {/* These routes and navigation have been generated for you */} {/* Feel free to move and update them to fit your needs */}


(
This is the generated root route. Click here for page 2.
)} /> (
Click here to go back to root page.
)} /> {/* END: routes */} ` ); return [ ...addGlobal( source, sourcePath, `import { Route, Link } from 'react-router-dom';` ), insertRoutes ]; } export function addRoute( sourcePath: string, source: ts.SourceFile, options: { routePath: string; componentName: string; moduleName: string; }, context: SchematicContext ): Change[] { const routes = findElements(source, 'Route'); const links = findElements(source, 'Link'); if (routes.length === 0) { context.logger.warn( `Could not find components in ${sourcePath}; Skipping add route` ); return []; } else { const changes: Change[] = []; const firstRoute = routes[0]; const firstLink = links[0]; changes.push( ...addGlobal( source, sourcePath, `import { ${options.componentName} } from '${options.moduleName}';` ) ); changes.push( new InsertChange( sourcePath, firstRoute.getEnd(), `` ) ); if (firstLink) { const parentLi = findClosestOpening('li', firstLink); if (parentLi) { changes.push( new InsertChange( sourcePath, parentLi.getEnd(), `
  • ${options.componentName}
  • ` ) ); } else { changes.push( new InsertChange( sourcePath, firstLink.parent.getEnd(), `${options.componentName}` ) ); } } return changes; } } export function addBrowserRouter( sourcePath: string, source: ts.SourceFile, context: SchematicContext ): Change[] { const app = findElements(source, 'App')[0]; if (app) { return [ ...addGlobal( source, sourcePath, `import { BrowserRouter } from 'react-router-dom';` ), new InsertChange(sourcePath, app.getStart(), ``), new InsertChange(sourcePath, app.getEnd(), ``) ]; } else { context.logger.warn( `Could not find App component in ${sourcePath}; Skipping add ` ); return []; } } export function addReduxStoreToMain( sourcePath: string, source: ts.SourceFile, context: SchematicContext ): Change[] { const renderStmt = findMainRenderStatement(source); if (!renderStmt) { context.logger.warn(`Could not find ReactDOM.render in ${sourcePath}`); return []; } const jsx = renderStmt.arguments[0]; return [ ...addGlobal( source, sourcePath, `import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux';` ), new InsertChange( sourcePath, renderStmt.getStart(), ` const store = configureStore({ reducer: {} }); ` ), new InsertChange(sourcePath, jsx.getStart(), ``), new InsertChange(sourcePath, jsx.getEnd(), ``) ]; } export function updateReduxStore( sourcePath: string, source: ts.SourceFile, context: SchematicContext, feature: { keyName: string; reducerName: string; modulePath: string; } ): Change[] { const calls = findNodes( source, ts.SyntaxKind.CallExpression ) as ts.CallExpression[]; let reducerDescriptor: ts.ObjectLiteralExpression; // Look for configureStore call for (const expr of calls) { if (!expr.expression.getText().includes('configureStore')) { continue; } const arg = expr.arguments[0]; if (ts.isObjectLiteralExpression(arg)) { let found: ts.ObjectLiteralExpression; for (const prop of arg.properties) { if ( ts.isPropertyAssignment(prop) && prop.name.getText() === 'reducer' && ts.isObjectLiteralExpression(prop.initializer) ) { found = prop.initializer; break; } } if (found) { reducerDescriptor = found; break; } } } // Look for combineReducer call if (!reducerDescriptor) { for (const expr of calls) { if (!expr.expression.getText().includes('combineReducer')) { continue; } const arg = expr.arguments[0]; if (ts.isObjectLiteralExpression(arg)) { reducerDescriptor = arg; break; } } } if (!reducerDescriptor) { context.logger.warn( `Could not find configureStore/combineReducer call in ${sourcePath}` ); return []; } return [ ...addGlobal( source, sourcePath, `import { ${feature.keyName}, ${feature.reducerName} } from '${feature.modulePath}';` ), new InsertChange( sourcePath, reducerDescriptor.getStart() + 1, `[${feature.keyName}]: ${feature.reducerName}${ reducerDescriptor.properties.length > 0 ? ',' : '' }` ) ]; }