From 087da7add2c44871b8f354df3885d6ff69ea0e0e Mon Sep 17 00:00:00 2001 From: Nicholas Cunningham Date: Tue, 13 Aug 2024 11:16:43 -0600 Subject: [PATCH] fix(module-federation): Add migration for ssr server file to run on it's own port --- packages/react/migrations.json | 6 + .../setup-ssr/files/server.ts__tmpl__ | 2 +- .../update-ssr-server-port.spec.ts | 206 ++++++++++++++++++ .../update-19-6-0/update-ssr-server-port.ts | 146 +++++++++++++ 4 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 packages/react/src/migrations/update-19-6-0/update-ssr-server-port.spec.ts create mode 100644 packages/react/src/migrations/update-19-6-0/update-ssr-server-port.ts diff --git a/packages/react/migrations.json b/packages/react/migrations.json index a760b4382804f..dfbd617e84e42 100644 --- a/packages/react/migrations.json +++ b/packages/react/migrations.json @@ -47,6 +47,12 @@ "version": "19.6.0-beta.4", "description": "Ensure Module Federation DTS is turned off by default.", "factory": "./src/migrations/update-19-6-0/turn-off-dts-by-default" + }, + "update-module-federation-ssr-server-file": { + "cli": "nx", + "version": "19.6.0-beta.4", + "description": "Update the server file for Module Federation SSR port value to be the same as the 'serve' target port value.", + "factory": "./src/migrations/update-19-6-0/update-ssr-server-port" } }, "packageJsonUpdates": { diff --git a/packages/react/src/generators/setup-ssr/files/server.ts__tmpl__ b/packages/react/src/generators/setup-ssr/files/server.ts__tmpl__ index 3e935a1fd337e..febade8f70c40 100644 --- a/packages/react/src/generators/setup-ssr/files/server.ts__tmpl__ +++ b/packages/react/src/generators/setup-ssr/files/server.ts__tmpl__ @@ -8,7 +8,7 @@ const port = process.env['PORT'] || <%= port %>; const app = express(); const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>'); -const indexPath =path.join(browserDist, 'index.html'); +const indexPath = path.join(browserDist, 'index.html'); app.use(cors()); diff --git a/packages/react/src/migrations/update-19-6-0/update-ssr-server-port.spec.ts b/packages/react/src/migrations/update-19-6-0/update-ssr-server-port.spec.ts new file mode 100644 index 0000000000000..c05b991bb46c3 --- /dev/null +++ b/packages/react/src/migrations/update-19-6-0/update-ssr-server-port.spec.ts @@ -0,0 +1,206 @@ +import { readProjectConfiguration, type Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports'; +import hostGenerator from '../../generators/host/host'; +import { Linter } from '@nx/eslint'; +import updateSsrServerPort from './update-ssr-server-port'; +describe('update-19-6-0 update-ssr-server-port migration', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + }); + + it('should update host and remote port server files', async () => { + await hostGenerator(tree, { + name: 'shell', + e2eTestRunner: 'none', + unitTestRunner: 'none', + ssr: true, + linter: Linter.EsLint, + projectNameAndRootFormat: 'as-provided', + style: 'css', + remotes: ['product'], + }); + const remotePort = readProjectConfiguration(tree, 'product').targets.serve + .options.port; + + const shellPort = readProjectConfiguration(tree, 'shell').targets.serve + .options.port; + + // This should already exists in the generated project + tree.write( + 'product/server.ts', + tree + .read('product/server.ts', 'utf-8') + .replace( + 'const port = 4201;', + `const port = process.env['PORT'] || 4200;` + ) + ); + + updateSsrServerPort(tree); + expect(tree.read('product/server.ts', 'utf-8')).toContain( + `port = process.env['PORT'] || ${remotePort}` + ); + expect(tree.read('product/server.ts', 'utf-8')).toMatchInlineSnapshot(` + "import * as path from 'path'; + import express from 'express'; + import cors from 'cors'; + + import { handleRequest } from './src/main.server'; + + const port = process.env['PORT'] || 4201; + const app = express(); + + const browserDist = path.join(process.cwd(), 'dist/product/browser'); + const serverDist = path.join(process.cwd(), 'dist/product/server'); + const indexPath = path.join(browserDist, 'index.html'); + + app.use(cors()); + + // Client-side static bundles + app.get( + '*.*', + express.static(browserDist, { + maxAge: '1y', + }) + ); + + // Static bundles for server-side module federation + app.use( + '/server', + express.static(serverDist, { + maxAge: '1y', + }) + ); + + app.use('*', handleRequest(indexPath)); + + const server = app.listen(port, () => { + console.log(\`Express server listening on http://localhost:\${port}\`); + + /** + * DO NOT REMOVE IF USING @nx/react:module-federation-dev-ssr executor + * to serve your Host application with this Remote application. + * This message allows Nx to determine when the Remote is ready to be + * consumed by the Host. + */ + process.send?.('nx.server.ready'); + }); + + server.on('error', console.error); + " + `); + + tree.write( + 'shell/server.ts', + tree + .read('shell/server.ts', 'utf-8') + .replace( + 'const port = 4200;', + `const port = process.env['PORT'] || 4200;` + ) + ); + + updateSsrServerPort(tree); + expect(tree.read('shell/server.ts', 'utf-8')).toContain( + `port = process.env.PORT || ${shellPort}` + ); + expect(tree.read('shell/server.ts', 'utf-8')).toMatchInlineSnapshot(` + "import * as path from 'path'; + import express from 'express'; + import cors from 'cors'; + import { handleRequest } from './src/main.server'; + const port = process.env.PORT || 4200; + const app = express(); + const browserDist = path.join(process.cwd(), 'dist/shell/browser'); + const indexPath = path.join(browserDist, 'index.html'); + app.use(cors()); + app.get('*.*', express.static(browserDist, { + maxAge: '1y', + })); + app.use('*', handleRequest(indexPath)); + const server = app.listen(port, () => { + console.log(\`Express server listening on http://localhost:\${port}\`); + }); + server.on('error', console.error); + " + `); + }); + + it('should update a host project server file', async () => { + await hostGenerator(tree, { + name: 'host', + e2eTestRunner: 'none', + unitTestRunner: 'none', + ssr: true, + linter: Linter.EsLint, + projectNameAndRootFormat: 'as-provided', + style: 'css', + }); + + const hostPort = readProjectConfiguration(tree, 'host').targets.serve + .options.port; + + tree.write( + 'host/server.ts', + tree + .read('host/server.ts', 'utf-8') + .replace( + 'const port = 4200;', + `const port = process.env['PORT'] || 4200;` + ) + ); + + updateSsrServerPort(tree); + + expect(tree.read('host/server.ts', 'utf-8')).toContain( + `port = process.env.PORT || ${hostPort}` + ); + expect(tree.read('host/server.ts', 'utf-8')).toMatchInlineSnapshot(` + "import * as path from 'path'; + import express from 'express'; + import cors from 'cors'; + import { handleRequest } from './src/main.server'; + const port = process.env.PORT || 4200; + const app = express(); + const browserDist = path.join(process.cwd(), 'dist/host/browser'); + const indexPath = path.join(browserDist, 'index.html'); + app.use(cors()); + app.get('*.*', express.static(browserDist, { + maxAge: '1y', + })); + app.use('*', handleRequest(indexPath)); + const server = app.listen(port, () => { + console.log(\`Express server listening on http://localhost:\${port}\`); + }); + server.on('error', console.error); + " + `); + }); + + it('should not update a mfe project that is not ssr', async () => { + await hostGenerator(tree, { + name: 'shell-not-ssr', + e2eTestRunner: 'none', + unitTestRunner: 'none', + ssr: false, + linter: Linter.EsLint, + projectNameAndRootFormat: 'as-provided', + style: 'css', + }); + + tree.write('shell-not-ssr/server.ts', 'const port = 9999;'); + const shellPort = readProjectConfiguration(tree, 'shell-not-ssr').targets + .serve.options.port; + + updateSsrServerPort(tree); + + expect(tree.read('shell-not-ssr/server.ts', 'utf-8')).not.toContain( + `port = ${shellPort}` + ); + expect(tree.read('shell-not-ssr/server.ts', 'utf-8')).toMatchInlineSnapshot( + `"const port = 9999;"` + ); + }); +}); diff --git a/packages/react/src/migrations/update-19-6-0/update-ssr-server-port.ts b/packages/react/src/migrations/update-19-6-0/update-ssr-server-port.ts new file mode 100644 index 0000000000000..196c53ab2b4ee --- /dev/null +++ b/packages/react/src/migrations/update-19-6-0/update-ssr-server-port.ts @@ -0,0 +1,146 @@ +import { + ProjectConfiguration, + type Tree, + getProjects, + joinPathFragments, + visitNotIgnoredFiles, +} from '@nx/devkit'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import * as ts from 'typescript'; +import { minimatch } from 'minimatch'; +import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils'; +import { type WebSsrDevServerOptions } from '@nx/webpack/src/executors/ssr-dev-server/schema'; + +export default function update(tree: Tree) { + const projects = getProjects(tree); + const executors = [ + '@nx/webpack:ssr-dev-server', + '@nx/react:module-federation-ssr-dev-server', + ]; + + executors.forEach((executor) => { + forEachExecutorOptions( + tree, + executor, + (options, projectName) => { + const project = projects.get(projectName); + if (isModuleFederationSSRProject(tree, project)) { + const port = options.port; + if (tree.exists(joinPathFragments(project.root, 'server.ts'))) { + const serverContent = tree.read( + joinPathFragments(project.root, 'server.ts'), + 'utf-8' + ); + if (serverContent && port) { + const updatedServerContent = updateServerPort( + serverContent, + port + ); + if (updatedServerContent) { + tree.write( + joinPathFragments(project.root, 'server.ts'), + updatedServerContent + ); + } + } + } + } + } + ); + }); +} + +function updateServerPort(serverContent: string, port: number) { + const sourceFile = tsquery.ast(serverContent); + + const serverPortNode = tsquery( + sourceFile, + `VariableDeclaration:has(Identifier[name="port"])` + )[0]; + if (serverPortNode) { + const binaryExpression = tsquery(serverPortNode, 'BinaryExpression')[0]; + if (binaryExpression) { + const leftExpression = tsquery( + binaryExpression, + 'PropertyAccessExpression:has(Identifier[name="env"])' + )[0]; + const rightExpression = tsquery( + binaryExpression, + 'NumericLiteral[text="4200"]' + )[0]; + + if (leftExpression && rightExpression) { + const serverPortDeclaration = serverPortNode as ts.VariableDeclaration; + const newInitializer = ts.factory.createBinaryExpression( + // process.env.PORT + ts.factory.createPropertyAccessExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('process'), + ts.factory.createIdentifier('env') + ), + 'PORT' + ), + // || + ts.SyntaxKind.BarBarToken, + // port value + ts.factory.createNumericLiteral(port.toString()) + ); + + const updatePortDeclaration = ts.factory.updateVariableDeclaration( + serverPortDeclaration, + serverPortDeclaration.name, + serverPortDeclaration.exclamationToken, + serverPortDeclaration.type, + newInitializer + ); + + const updatedStatements = sourceFile.statements.map((statement) => { + if (ts.isVariableStatement(statement)) { + const updatedDeclarationList = + statement.declarationList.declarations.map((decl) => + decl === serverPortDeclaration ? updatePortDeclaration : decl + ); + + const updatedDeclList = ts.factory.updateVariableDeclarationList( + statement.declarationList, + updatedDeclarationList + ); + + return ts.factory.updateVariableStatement( + statement, + statement.modifiers, + updatedDeclList + ); + } + + return statement; + }); + + const updatedSourceFile = ts.factory.updateSourceFile( + sourceFile, + updatedStatements + ); + + const printer = ts.createPrinter(); + return printer.printNode( + ts.EmitHint.Unspecified, + updatedSourceFile, + sourceFile + ); + } + } + } +} + +function isModuleFederationSSRProject( + tree: Tree, + project: ProjectConfiguration +) { + let hasMfeServerConfig = false; + visitNotIgnoredFiles(tree, project.root, (filePath) => { + if (minimatch(filePath, '**/module-federation*.server.config.*')) { + hasMfeServerConfig = true; + } + }); + return hasMfeServerConfig; +}