nx/packages/angular/src/migrations/update-13-5-0/update-mfe-configs.ts

270 lines
8.5 KiB
TypeScript

import type { Tree } from '@nrwl/devkit';
import {
logger,
readProjectConfiguration,
updateJson,
joinPathFragments,
formatFiles,
} from '@nrwl/devkit';
import { tsquery } from '@phenomnomnominal/tsquery';
import { forEachExecutorOptions } from '@nrwl/workspace/src/utilities/executor-options-utils';
export default async function (tree: Tree) {
const NRWL_WEBPACK_BROWSER_BUILDER = '@nrwl/angular:webpack-browser';
const CUSTOM_WEBPACK_OPTION = 'customWebpackConfig';
const projects: Record<string, string> = {};
forEachExecutorOptions(
tree,
NRWL_WEBPACK_BROWSER_BUILDER,
(opts, projectName) => {
// Update the webpack config
const webpackPath = opts[CUSTOM_WEBPACK_OPTION]?.path;
if (!webpackPath || !tree.exists(webpackPath)) {
return;
}
const webpackConfig = tree.read(webpackPath, 'utf-8');
const ast = tsquery.ast(webpackConfig);
const moduleFederationWebpackConfig = tsquery(
ast,
'Identifier[name=ModuleFederationPlugin]',
{
visitAllChildren: true,
}
);
if (
!moduleFederationWebpackConfig ||
moduleFederationWebpackConfig.length === 0
) {
return;
}
projects[projectName] = webpackPath;
}
);
useShareHelper(tree, projects);
turnMinimizeOn(tree, projects);
switchToES2020(tree, projects);
replaceBrowserModuleInRemoteEntry(tree, projects);
await formatFiles(tree);
}
function replaceBrowserModuleInRemoteEntry(
tree: Tree,
projects: Record<string, string>
) {
for (const projectName of Object.keys(projects)) {
const remoteEntryModulePath = joinPathFragments(
readProjectConfiguration(tree, projectName).sourceRoot,
`app/remote-entry/entry.module.ts`
);
if (!tree.exists(remoteEntryModulePath)) {
continue;
}
let remoteEntryModuleContents = tree.read(remoteEntryModulePath, 'utf-8');
remoteEntryModuleContents = replaceBrowserModuleWithCommonFromRemoteEntry(
remoteEntryModuleContents
);
tree.write(remoteEntryModulePath, remoteEntryModuleContents);
}
}
export function replaceBrowserModuleWithCommonFromRemoteEntry(
remoteEntryModule: string
): string {
const IS_BROWSER_MODULE_IN_IMPORTS_AST_QUERY =
'Identifier[name=imports] ~ ArrayLiteralExpression:has(Identifier[name=BrowserModule])';
const IS_COMMON_MODULE_IMPORTED_AST_QUERY =
'ImportDeclaration:has(ImportSpecifier:has(Identifier[name=CommonModule]))';
const IS_COMMON_MODULE_IN_IMPORTS_AST_QUERY =
'Identifier[name=imports] ~ ArrayLiteralExpression:has(Identifier[name=CommonModule])';
const BROWSER_MODULE_POS_AST_QUERY =
'Identifier[name=imports] ~ ArrayLiteralExpression > Identifier[name=BrowserModule]';
let ast = tsquery.ast(remoteEntryModule);
const importsArrayWithBrowserModule = tsquery(
ast,
IS_BROWSER_MODULE_IN_IMPORTS_AST_QUERY,
{ visitAllChildren: true }
);
const commonModuleImportsNode = tsquery(
ast,
IS_COMMON_MODULE_IN_IMPORTS_AST_QUERY,
{ visitAllChildren: true }
);
const commonModuleImportedInFileNode = tsquery(
ast,
IS_COMMON_MODULE_IMPORTED_AST_QUERY,
{ visitAllChildren: true }
);
const hasBrowserModule =
importsArrayWithBrowserModule && importsArrayWithBrowserModule.length > 0;
const needsCommonModuleInImports =
!commonModuleImportsNode || commonModuleImportsNode.length < 1;
const needsCommonModuleImportStatement =
!commonModuleImportedInFileNode ||
commonModuleImportedInFileNode.length < 1;
if (!hasBrowserModule) {
if (needsCommonModuleInImports && needsCommonModuleImportStatement) {
// no browser module and no common module imported
const IMPORTS_ARRAY_POS_AST_QUERY =
'Identifier[name=imports] ~ ArrayLiteralExpression';
const importsArrayNode = tsquery(ast, IMPORTS_ARRAY_POS_AST_QUERY, {
visitAllChildren: true,
})[0];
const updatedRemoteEntry = `import { CommonModule } from '@angular/common';\n${remoteEntryModule.slice(
0,
importsArrayNode.getStart() + 1
)}\nCommonModule,${remoteEntryModule.slice(
importsArrayNode.getStart() + 1
)}`;
return updatedRemoteEntry;
} else {
return remoteEntryModule;
}
}
const browserModuleNode = tsquery(ast, BROWSER_MODULE_POS_AST_QUERY, {
visitAllChildren: true,
})[0];
const updatedRemoteEntryModule = `${
needsCommonModuleImportStatement
? `import { CommonModule } from '@angular/common';\n`
: ``
}${remoteEntryModule.slice(0, browserModuleNode.getStart())}${
needsCommonModuleInImports ? `CommonModule` : ``
}${remoteEntryModule.slice(
browserModuleNode.getEnd() + (needsCommonModuleInImports ? 0 : 1)
)}`;
return updatedRemoteEntryModule;
}
function switchToES2020(tree: Tree, projects: Record<string, string>) {
for (const projectName of Object.keys(projects)) {
const { root } = readProjectConfiguration(tree, projectName);
let tsConfigPath = tree.exists(joinPathFragments(root, `tsconfig.app.json`))
? joinPathFragments(root, `tsconfig.app.json`)
: joinPathFragments(root, `tsconfig.json`);
updateJson(tree, tsConfigPath, (json) => ({
...json,
compilerOptions: {
...json.compilerOptions,
target: 'ES2020',
},
}));
}
}
function turnMinimizeOn(tree: Tree, projects: Record<string, string>) {
for (const webpackPath of Object.values(projects)) {
let webpackConfig = tree.read(webpackPath, 'utf-8');
tree.write(
webpackPath,
modifyConfigToUseMinimizeOptimization(webpackConfig)
);
}
}
export function modifyConfigToUseMinimizeOptimization(
webpackConfig: string
): string {
const OPTIMIZATION_OBJECT_AST_QUERY =
'PropertyAssignment:has(Identifier[name=optimization]) > ObjectLiteralExpression';
let ast = tsquery.ast(webpackConfig);
const optimizationObjectNode = tsquery(ast, OPTIMIZATION_OBJECT_AST_QUERY, {
visitAllChildren: true,
})[0];
const minimizeTrueNode = tsquery(
optimizationObjectNode,
'Identifier[name=minimize] ~ TrueKeyword',
{ visitAllChildren: true }
);
if (minimizeTrueNode && minimizeTrueNode.length > 0) {
// it's already turned on
return webpackConfig;
}
const minimizeFalseNode = tsquery(
optimizationObjectNode,
'Identifier[name=minimize] ~ FalseKeyword',
{ visitAllChildren: true }
);
if (minimizeFalseNode && minimizeFalseNode.length > 0) {
// it exists but it's set to false, so flip it
webpackConfig = `${webpackConfig.slice(
0,
minimizeFalseNode[0].getStart()
)}true${webpackConfig.slice(minimizeFalseNode[0].getEnd())}`;
return webpackConfig;
}
return webpackConfig;
}
function useShareHelper(tree: Tree, projects: Record<string, string>) {
for (const webpackPath of Object.values(projects)) {
let webpackConfig = tree.read(webpackPath, 'utf-8');
tree.write(webpackPath, modifyConfigToUseShareHelper(webpackConfig));
}
}
export function modifyConfigToUseShareHelper(webpackConfig: string): string {
const SHARE_CALL_AST_QUERY = 'CallExpression:has(Identifier[name=share])';
const MODULE_EXPORTS_AST_QUERY =
'ExpressionStatement:has(BinaryExpression:has(PropertyAccessExpression:has(Identifier[name=module], Identifier[name=exports])))';
const SHARED_OBJECT_AST_QUERY =
'PropertyAssignment:has(Identifier[name=shared]) > ObjectLiteralExpression';
let ast = tsquery.ast(webpackConfig);
const shareCall = tsquery(ast, SHARE_CALL_AST_QUERY, {
visitAllChildren: true,
});
if (shareCall && shareCall.length > 0) {
// skip this project if it's already using share
return webpackConfig;
}
const sharedObject = tsquery(ast, SHARED_OBJECT_AST_QUERY, {
visitAllChildren: true,
});
if (!sharedObject || sharedObject.length < 1) {
// skip because there are no libs being shared
return webpackConfig;
}
const sharedObjectNode = sharedObject[0];
webpackConfig = `${webpackConfig.slice(
0,
sharedObjectNode.getStart()
)}share(${webpackConfig.slice(
sharedObjectNode.getStart(),
sharedObjectNode.getEnd()
)})${webpackConfig.slice(sharedObjectNode.getEnd())}`;
// wrap the shared libs in the function
ast = tsquery.ast(webpackConfig);
const moduleExportsStatement = tsquery(ast, MODULE_EXPORTS_AST_QUERY, {
visitAllChildren: true,
})[0];
webpackConfig = `${webpackConfig.slice(
0,
moduleExportsStatement.getStart()
)}const share = mf.share;\nmf.setInferVersion(true);\n${webpackConfig.slice(
moduleExportsStatement.getStart()
)}`;
return webpackConfig;
}