Skip to content

Commit

Permalink
fix(module-federation): Add migration for ssr server file to run on i…
Browse files Browse the repository at this point in the history
…t's own port
  • Loading branch information
ndcunningham committed Aug 14, 2024
1 parent ab162eb commit b08f9a3
Show file tree
Hide file tree
Showing 4 changed files with 359 additions and 1 deletion.
6 changes: 6 additions & 0 deletions packages/react/migrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.ts"
}
},
"packageJsonUpdates": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down
Original file line number Diff line number Diff line change
@@ -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;"`
);
});
});
146 changes: 146 additions & 0 deletions packages/react/src/migrations/update-19-6-0/update-ssr-server-port.ts
Original file line number Diff line number Diff line change
@@ -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<WebSsrDevServerOptions>(
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;
}

0 comments on commit b08f9a3

Please sign in to comment.