nx/packages/angular/src/generators/setup-mf/lib/add-remote-to-host.ts

190 lines
5.3 KiB
TypeScript

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(`<ul class="remote-menu">`) &&
appComponent.includes('</ul>')
) {
const indexOfClosingMenuTag = appComponent.indexOf('</ul>');
const newAppComponent = `${appComponent.slice(
0,
indexOfClosingMenuTag
)}<li><a routerLink='${options.appName}'>${
names(options.appName).className
}</a></li>\n${appComponent.slice(indexOfClosingMenuTag)}`;
tree.write(pathToAppComponentTemplate, newAppComponent);
}
}