import { joinPathFragments, names, ProjectConfiguration, readProjectConfiguration, Tree, updateJson, } from '@nrwl/devkit'; import type { Schema } from '../schema'; import { tsquery } from '@phenomnomnominal/tsquery'; import type * as ts from 'typescript'; import { ArrayLiteralExpression } from 'typescript'; import { insertImport } from '@nrwl/workspace/src/utilities/ast-utils'; import { addRoute } from '../../../utils/nx-devkit/route-utils'; let tsModule: typeof import('typescript'); export function checkIsCommaNeeded(mfRemoteText: string) { const remoteText = mfRemoteText.replace(/\s+/g, ''); return !remoteText.endsWith(',]') ? remoteText === '[]' ? false : true : false; } export function addRemoteToHost(tree: Tree, options: Schema) { if (options.host) { const hostProject = readProjectConfiguration(tree, options.host); const pathToMFManifest = joinPathFragments( hostProject.sourceRoot, 'assets/module-federation.manifest.json' ); const hostFederationType = determineHostFederationType( tree, pathToMFManifest ); if (hostFederationType === 'static') { addRemoteToStaticHost(tree, options, hostProject); } else if (hostFederationType === 'dynamic') { addRemoteToDynamicHost(tree, options, pathToMFManifest); } const declarationFilePath = joinPathFragments( hostProject.sourceRoot, 'remotes.d.ts' ); const declarationFileContent = (tree.exists(declarationFilePath) ? tree.read(declarationFilePath, 'utf-8') : '') + `\ndeclare module '${options.appName}/${ options.standalone ? `Routes` : `Module` }';`; tree.write(declarationFilePath, declarationFileContent); addLazyLoadedRouteToHostAppModule(tree, options, hostFederationType); } } function determineHostFederationType( tree: Tree, pathToMfManifest: string ): 'dynamic' | 'static' { return tree.exists(pathToMfManifest) ? 'dynamic' : 'static'; } function addRemoteToStaticHost( tree: Tree, options: Schema, hostProject: ProjectConfiguration ) { const hostMFConfigPath = joinPathFragments( hostProject.root, 'module-federation.config.js' ); if (!hostMFConfigPath || !tree.exists(hostMFConfigPath)) { throw new Error( `The selected host application, ${options.host}, does not contain a module-federation.config.js or module-federation.manifest.json file. Are you sure it has been set up as a host application?` ); } const hostMFConfig = tree.read(hostMFConfigPath, 'utf-8'); const webpackAst = tsquery.ast(hostMFConfig); const mfRemotesNode = tsquery( webpackAst, 'Identifier[name=remotes] ~ ArrayLiteralExpression', { visitAllChildren: true } )[0] as ArrayLiteralExpression; const endOfPropertiesPos = mfRemotesNode.getEnd() - 1; const isCommaNeeded = checkIsCommaNeeded(mfRemotesNode.getText()); const updatedConfig = `${hostMFConfig.slice(0, endOfPropertiesPos)}${ isCommaNeeded ? ',' : '' }'${options.appName}',${hostMFConfig.slice(endOfPropertiesPos)}`; tree.write(hostMFConfigPath, updatedConfig); } function addRemoteToDynamicHost( tree: Tree, options: Schema, pathToMfManifest: string ) { updateJson(tree, pathToMfManifest, (manifest) => { return { ...manifest, [options.appName]: `http://localhost:${options.port}`, }; }); } // TODO(colum): future work: allow dev to pass to path to routing module function addLazyLoadedRouteToHostAppModule( tree: Tree, options: Schema, hostFederationType: 'dynamic' | 'static' ) { if (!tsModule) { tsModule = require('typescript'); } const hostAppConfig = readProjectConfiguration(tree, options.host); const pathToHostRootRouting = `${hostAppConfig.sourceRoot}/app/app.routes.ts`; if (!tree.exists(pathToHostRootRouting)) { return; } const hostRootRoutingFile = tree.read(pathToHostRootRouting, 'utf-8'); let sourceFile = tsModule.createSourceFile( pathToHostRootRouting, hostRootRoutingFile, tsModule.ScriptTarget.Latest, true ); if (hostFederationType === 'dynamic') { sourceFile = insertImport( tree, sourceFile, pathToHostRootRouting, 'loadRemoteModule', '@nrwl/angular/mf' ); } const routePathName = options.standalone ? 'Routes' : 'Module'; const exportedRemote = options.standalone ? 'remoteRoutes' : 'RemoteEntryModule'; const routeToAdd = hostFederationType === 'dynamic' ? `loadRemoteModule('${options.appName}', './${routePathName}')` : `import('${options.appName}/${routePathName}')`; addRoute( tree, pathToHostRootRouting, `{ path: '${options.appName}', loadChildren: () => ${routeToAdd}.then(m => m.${exportedRemote}) }` ); const pathToAppComponentTemplate = joinPathFragments( hostAppConfig.sourceRoot, 'app/app.component.html' ); const appComponent = tree.read(pathToAppComponentTemplate, 'utf-8'); if ( appComponent.includes(`
') ) { const indexOfClosingMenuTag = appComponent.indexOf(''); const newAppComponent = `${appComponent.slice( 0, indexOfClosingMenuTag )}