feat(react): switch default to typescript configuration for module federation (#19031)

This commit is contained in:
Colum Ferry 2023-09-18 17:11:38 +01:00 committed by GitHub
parent b2a9d4d79a
commit 11fcb8f2d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 823 additions and 43 deletions

View File

@ -160,6 +160,11 @@
"description": "Generate a React app with a minimal setup. No nx starter template.", "description": "Generate a React app with a minimal setup. No nx starter template.",
"type": "boolean", "type": "boolean",
"default": false "default": false
},
"typescriptConfiguration": {
"type": "boolean",
"description": "Whether the module federation configuration and webpack configuration files should use TS.",
"default": true
} }
}, },
"required": ["name"], "required": ["name"],

View File

@ -158,6 +158,11 @@
"description": "Whether to configure SSR for the host application", "description": "Whether to configure SSR for the host application",
"type": "boolean", "type": "boolean",
"default": false "default": false
},
"typescriptConfiguration": {
"type": "boolean",
"description": "Whether the module federation configuration and webpack configuration files should use TS.",
"default": true
} }
}, },
"required": ["name"], "required": ["name"],

View File

@ -39,10 +39,10 @@ describe('React Module Federation', () => {
`generate @nx/react:remote ${remote3} --style=css --host=${shell} --no-interactive` `generate @nx/react:remote ${remote3} --style=css --host=${shell} --no-interactive`
); );
checkFilesExist(`apps/${shell}/module-federation.config.js`); checkFilesExist(`apps/${shell}/module-federation.config.ts`);
checkFilesExist(`apps/${remote1}/module-federation.config.js`); checkFilesExist(`apps/${remote1}/module-federation.config.ts`);
checkFilesExist(`apps/${remote2}/module-federation.config.js`); checkFilesExist(`apps/${remote2}/module-federation.config.ts`);
checkFilesExist(`apps/${remote3}/module-federation.config.js`); checkFilesExist(`apps/${remote3}/module-federation.config.ts`);
await expect(runCLIAsync(`test ${shell}`)).resolves.toMatchObject({ await expect(runCLIAsync(`test ${shell}`)).resolves.toMatchObject({
combinedOutput: expect.stringContaining('Test Suites: 1 passed, 1 total'), combinedOutput: expect.stringContaining('Test Suites: 1 passed, 1 total'),
@ -54,7 +54,7 @@ describe('React Module Federation', () => {
expect(readPort(remote3)).toEqual(4203); expect(readPort(remote3)).toEqual(4203);
updateFile( updateFile(
`apps/${shell}/webpack.config.js`, `apps/${shell}/webpack.config.ts`,
stripIndents` stripIndents`
import { composePlugins, withNx, ModuleFederationConfig } from '@nx/webpack'; import { composePlugins, withNx, ModuleFederationConfig } from '@nx/webpack';
import { withReact } from '@nx/react'; import { withReact } from '@nx/react';
@ -135,8 +135,8 @@ describe('React Module Federation', () => {
// check files are generated without the layout directory ("apps/") and // check files are generated without the layout directory ("apps/") and
// using the project name as the directory when no directory is provided // using the project name as the directory when no directory is provided
checkFilesExist(`${shell}/module-federation.config.js`); checkFilesExist(`${shell}/module-federation.config.ts`);
checkFilesExist(`${remote}/module-federation.config.js`); checkFilesExist(`${remote}/module-federation.config.ts`);
// check default generated host is built successfully // check default generated host is built successfully
const buildOutput = runCLI(`run ${shell}:build:development`); const buildOutput = runCLI(`run ${shell}:build:development`);

View File

@ -0,0 +1,128 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`hostGenerator should generate host files and configs 1`] = `
"const { composePlugins, withNx } = require('@nx/webpack');
const { withReact } = require('@nx/react');
const { withModuleFederation } = require('@nx/react/module-federation');
const baseConfig = require('./module-federation.config');
const config = {
...baseConfig,
};
// Nx plugins for webpack to build config object from Nx options and context.
module.exports = composePlugins(
withNx(),
withReact(),
withModuleFederation(config)
);
"
`;
exports[`hostGenerator should generate host files and configs 2`] = `
"module.exports = {
name: 'test',
remotes: [],
};
"
`;
exports[`hostGenerator should generate host files and configs for SSR 1`] = `
"const { composePlugins, withNx } = require('@nx/webpack');
const { withReact } = require('@nx/react');
const { withModuleFederationForSSR } = require('@nx/react/module-federation');
const baseConfig = require('./module-federation.config');
const defaultConfig = {
...baseConfig,
};
// Nx plugins for webpack to build config object from Nx options and context.
module.exports = composePlugins(
withNx(),
withReact({ ssr: true }),
withModuleFederationForSSR(defaultConfig)
);
"
`;
exports[`hostGenerator should generate host files and configs for SSR 2`] = `
"// @ts-check
/**
* @type {import('@nx/webpack').ModuleFederationConfig}
**/
const moduleFederationConfig = {
name: 'test',
remotes: [],
};
module.exports = moduleFederationConfig;
"
`;
exports[`hostGenerator should generate host files and configs for SSR when --typescriptConfiguration=true 1`] = `
"import { composePlugins, withNx } from '@nx/webpack';
import { withReact } from '@nx/react';
import { withModuleFederationForSSR } from '@nx/react/module-federation';
import baseConfig from './module-federation.config';
const defaultConfig = {
...baseConfig,
};
// Nx plugins for webpack to build config object from Nx options and context.
export default composePlugins(
withNx(),
withReact({ ssr: true }),
withModuleFederationForSSR(defaultConfig)
);
"
`;
exports[`hostGenerator should generate host files and configs for SSR when --typescriptConfiguration=true 2`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: 'test',
remotes: [],
};
export default config;
"
`;
exports[`hostGenerator should generate host files and configs when --typescriptConfiguration=true 1`] = `
"import { composePlugins, withNx } from '@nx/webpack';
import { withReact } from '@nx/react';
import { withModuleFederation } from '@nx/react/module-federation';
import baseConfig from './module-federation.config';
const config = {
...baseConfig,
};
// Nx plugins for webpack to build config object from Nx options and context.
export default composePlugins(
withNx(),
withReact(),
withModuleFederation(config)
);
"
`;
exports[`hostGenerator should generate host files and configs when --typescriptConfiguration=true 2`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: 'test',
remotes: [],
};
export default config;
"
`;

View File

@ -0,0 +1,10 @@
import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: '<%= projectName %>',
remotes: [
<% remotes.forEach(function(r) {%> "<%= r.fileName %>", <% }); %>
],
};
export default config;

View File

@ -0,0 +1,28 @@
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(), '<%= browserBuildOutputPath %>');
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);

View File

@ -0,0 +1,15 @@
{
"extends": "./tsconfig.app.json",
"compilerOptions": {
"outDir": "../../out-tsc/server",
"target": "es2019",
"types": [
"node"
]
},
"include": [
"src/remotes.d.ts",
"src/main.server.tsx",
"server.ts"
]
}

View File

@ -0,0 +1,12 @@
import {composePlugins, withNx} from '@nx/webpack';
import {withReact} from '@nx/react';
import {withModuleFederationForSSR} from '@nx/react/module-federation';
import baseConfig from './module-federation.config';
const defaultConfig = {
...baseConfig
};
// Nx plugins for webpack to build config object from Nx options and context.
export default composePlugins(withNx(), withReact({ssr: true}), withModuleFederationForSSR(defaultConfig));

View File

@ -0,0 +1,10 @@
import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: '<%= projectName %>',
remotes: [
<% remotes.forEach(function(r) {%> "<%= r.fileName %>", <% }); %>
],
};
export default config;

View File

@ -0,0 +1 @@
import('./bootstrap');

View File

@ -0,0 +1,4 @@
// Declare your remote Modules here
// Example declare module 'about/Module';
<% remotes.forEach(function(r) { %>declare module '<%= r.fileName %>/Module';<% }); %>

View File

@ -0,0 +1,32 @@
import { composePlugins, withNx } from '@nx/webpack';
import { withReact } from '@nx/react';
import { withModuleFederation } from '@nx/react/module-federation';
import baseConfig from './module-federation.config';
const prodConfig = {
...baseConfig,
/*
* Remote overrides for production.
* Each entry is a pair of a unique name and the URL where it is deployed.
*
* e.g.
* remotes: [
* ['app1', 'http://app1.example.com'],
* ['app2', 'http://app2.example.com'],
* ]
*
* You can also use a full path to the remoteEntry.js file if desired.
*
* remotes: [
* ['app1', 'http://example.com/path/to/app1/remoteEntry.js'],
* ['app2', 'http://example.com/path/to/app2/remoteEntry.js'],
* ]
*/
remotes: [
<% remotes.forEach(function(r) {%>['<%= r.fileName %>', 'http://localhost:<%= r.port %>/'],<% }); %>
],
};
// Nx plugins for webpack to build config object from Nx options and context.
export default composePlugins(withNx(), withReact(), withModuleFederation(prodConfig));

View File

@ -0,0 +1,12 @@
import {composePlugins, withNx} from '@nx/webpack';
import {withReact} from '@nx/react';
import {withModuleFederation} from '@nx/react/module-federation';
import baseConfig from './module-federation.config';
const config = {
...baseConfig,
};
// Nx plugins for webpack to build config object from Nx options and context.
export default composePlugins(withNx(), withReact(), withModuleFederation(config));

View File

@ -19,14 +19,44 @@ describe('hostGenerator', () => {
unitTestRunner: 'none', unitTestRunner: 'none',
e2eTestRunner: 'none', e2eTestRunner: 'none',
projectNameAndRootFormat: 'as-provided', projectNameAndRootFormat: 'as-provided',
typescriptConfiguration: false,
}); });
expect(tree.exists('test/tsconfig.json')); expect(tree.exists('test/tsconfig.json')).toBeTruthy();
expect(tree.exists('test/webpack.config.prod.js')); expect(tree.exists('test/webpack.config.prod.js')).toBeTruthy();
expect(tree.exists('test/webpack.config.js')); expect(tree.exists('test/webpack.config.js')).toBeTruthy();
expect(tree.exists('test/src/bootstrap.tsx')); expect(tree.exists('test/module-federation.config.js')).toBeTruthy();
expect(tree.exists('test/src/main.ts')); expect(tree.exists('test/src/bootstrap.tsx')).toBeTruthy();
expect(tree.exists('test/src/remotes.d.ts')); expect(tree.exists('test/src/main.ts')).toBeTruthy();
expect(tree.exists('test/src/remotes.d.ts')).toBeTruthy();
expect(tree.read('test/webpack.config.js', 'utf-8')).toMatchSnapshot();
expect(
tree.read('test/module-federation.config.js', 'utf-8')
).toMatchSnapshot();
});
it('should generate host files and configs when --typescriptConfiguration=true', async () => {
await hostGenerator(tree, {
name: 'test',
style: 'css',
linter: Linter.None,
unitTestRunner: 'none',
e2eTestRunner: 'none',
projectNameAndRootFormat: 'as-provided',
typescriptConfiguration: true,
});
expect(tree.exists('test/tsconfig.json')).toBeTruthy();
expect(tree.exists('test/webpack.config.prod.ts')).toBeTruthy();
expect(tree.exists('test/webpack.config.ts')).toBeTruthy();
expect(tree.exists('test/module-federation.config.ts')).toBeTruthy();
expect(tree.exists('test/src/bootstrap.tsx')).toBeTruthy();
expect(tree.exists('test/src/main.ts')).toBeTruthy();
expect(tree.exists('test/src/remotes.d.ts')).toBeTruthy();
expect(tree.read('test/webpack.config.ts', 'utf-8')).toMatchSnapshot();
expect(
tree.read('test/module-federation.config.ts', 'utf-8')
).toMatchSnapshot();
}); });
it('should install @nx/web for the file-server executor', async () => { it('should install @nx/web for the file-server executor', async () => {
@ -53,16 +83,19 @@ describe('hostGenerator', () => {
unitTestRunner: 'none', unitTestRunner: 'none',
e2eTestRunner: 'none', e2eTestRunner: 'none',
projectNameAndRootFormat: 'as-provided', projectNameAndRootFormat: 'as-provided',
typescriptConfiguration: false,
}); });
expect(tree.exists('test/tsconfig.json')); expect(tree.exists('test/tsconfig.json')).toBeTruthy();
expect(tree.exists('test/webpack.config.prod.js')); expect(tree.exists('test/webpack.config.prod.js')).toBeTruthy();
expect(tree.exists('test/webpack.config.server.js')); expect(tree.exists('test/webpack.server.config.js')).toBeTruthy();
expect(tree.exists('test/webpack.config.js')); expect(tree.exists('test/webpack.config.js')).toBeTruthy();
expect(tree.exists('test/src/main.server.tsx')); expect(tree.exists('test/module-federation.config.js')).toBeTruthy();
expect(tree.exists('test/src/bootstrap.tsx')); expect(tree.exists('test/module-federation.server.config.js')).toBeTruthy();
expect(tree.exists('test/src/main.ts')); expect(tree.exists('test/src/main.server.tsx')).toBeTruthy();
expect(tree.exists('test/src/remotes.d.ts')); expect(tree.exists('test/src/bootstrap.tsx')).toBeTruthy();
expect(tree.exists('test/src/main.ts')).toBeTruthy();
expect(tree.exists('test/src/remotes.d.ts')).toBeTruthy();
expect(readJson(tree, 'test/tsconfig.server.json')).toEqual({ expect(readJson(tree, 'test/tsconfig.server.json')).toEqual({
compilerOptions: { compilerOptions: {
@ -73,6 +106,54 @@ describe('hostGenerator', () => {
extends: './tsconfig.app.json', extends: './tsconfig.app.json',
include: ['src/remotes.d.ts', 'src/main.server.tsx', 'server.ts'], include: ['src/remotes.d.ts', 'src/main.server.tsx', 'server.ts'],
}); });
expect(
tree.read('test/webpack.server.config.js', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('test/module-federation.server.config.js', 'utf-8')
).toMatchSnapshot();
});
it('should generate host files and configs for SSR when --typescriptConfiguration=true', async () => {
await hostGenerator(tree, {
name: 'test',
ssr: true,
style: 'css',
linter: Linter.None,
unitTestRunner: 'none',
e2eTestRunner: 'none',
projectNameAndRootFormat: 'as-provided',
typescriptConfiguration: true,
});
expect(tree.exists('test/tsconfig.json')).toBeTruthy();
expect(tree.exists('test/webpack.config.prod.ts')).toBeTruthy();
expect(tree.exists('test/webpack.server.config.ts')).toBeTruthy();
expect(tree.exists('test/webpack.config.ts')).toBeTruthy();
expect(tree.exists('test/module-federation.config.ts')).toBeTruthy();
expect(tree.exists('test/module-federation.server.config.ts')).toBeTruthy();
expect(tree.exists('test/src/main.server.tsx')).toBeTruthy();
expect(tree.exists('test/src/bootstrap.tsx')).toBeTruthy();
expect(tree.exists('test/src/main.ts')).toBeTruthy();
expect(tree.exists('test/src/remotes.d.ts')).toBeTruthy();
expect(readJson(tree, 'test/tsconfig.server.json')).toEqual({
compilerOptions: {
outDir: '../../out-tsc/server',
target: 'es2019',
types: ['node'],
},
extends: './tsconfig.app.json',
include: ['src/remotes.d.ts', 'src/main.server.tsx', 'server.ts'],
});
expect(
tree.read('test/webpack.server.config.ts', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('test/module-federation.server.config.ts', 'utf-8')
).toMatchSnapshot();
}); });
it('should generate a host and remotes in a directory correctly when using --projectNameAndRootFormat=as-provided', async () => { it('should generate a host and remotes in a directory correctly when using --projectNameAndRootFormat=as-provided', async () => {
@ -87,6 +168,7 @@ describe('hostGenerator', () => {
linter: Linter.None, linter: Linter.None,
style: 'css', style: 'css',
unitTestRunner: 'none', unitTestRunner: 'none',
typescriptConfiguration: false,
}); });
expect(tree.exists('foo/remote1/project.json')).toBeTruthy(); expect(tree.exists('foo/remote1/project.json')).toBeTruthy();
@ -96,4 +178,27 @@ describe('hostGenerator', () => {
tree.read('foo/host-app/module-federation.config.js', 'utf-8') tree.read('foo/host-app/module-federation.config.js', 'utf-8')
).toContain(`'remote1', 'remote2', 'remote3'`); ).toContain(`'remote1', 'remote2', 'remote3'`);
}); });
it('should generate a host and remotes in a directory correctly when using --projectNameAndRootFormat=as-provided and --typescriptConfiguration=true', async () => {
const tree = createTreeWithEmptyWorkspace();
await hostGenerator(tree, {
name: 'hostApp',
directory: 'foo/hostApp',
remotes: ['remote1', 'remote2', 'remote3'],
projectNameAndRootFormat: 'as-provided',
e2eTestRunner: 'none',
linter: Linter.None,
style: 'css',
unitTestRunner: 'none',
typescriptConfiguration: true,
});
expect(tree.exists('foo/remote1/project.json')).toBeTruthy();
expect(tree.exists('foo/remote2/project.json')).toBeTruthy();
expect(tree.exists('foo/remote3/project.json')).toBeTruthy();
expect(
tree.read('foo/host-app/module-federation.config.ts', 'utf-8')
).toContain(`'remote1', 'remote2', 'remote3'`);
});
}); });

View File

@ -19,7 +19,7 @@ import {
} from './lib/normalize-remote'; } from './lib/normalize-remote';
import { setupSsrForHost } from './lib/setup-ssr-for-host'; import { setupSsrForHost } from './lib/setup-ssr-for-host';
import { updateModuleFederationE2eProject } from './lib/update-module-federation-e2e-project'; import { updateModuleFederationE2eProject } from './lib/update-module-federation-e2e-project';
import { Schema } from './schema'; import { NormalizedSchema, Schema } from './schema';
export async function hostGenerator(host: Tree, schema: Schema) { export async function hostGenerator(host: Tree, schema: Schema) {
return hostGeneratorInternal(host, { return hostGeneratorInternal(host, {
@ -30,11 +30,10 @@ export async function hostGenerator(host: Tree, schema: Schema) {
export async function hostGeneratorInternal(host: Tree, schema: Schema) { export async function hostGeneratorInternal(host: Tree, schema: Schema) {
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
const options = await normalizeOptions<Schema>( const options: NormalizedSchema = {
host, ...(await normalizeOptions<Schema>(host, schema, '@nx/react:host')),
schema, typescriptConfiguration: schema.typescriptConfiguration ?? true,
'@nx/react:host' };
);
const initTask = await applicationGenerator(host, { const initTask = await applicationGenerator(host, {
...options, ...options,
@ -65,6 +64,7 @@ export async function hostGeneratorInternal(host: Tree, schema: Schema) {
ssr: options.ssr, ssr: options.ssr,
skipFormat: true, skipFormat: true,
projectNameAndRootFormat: options.projectNameAndRootFormat, projectNameAndRootFormat: options.projectNameAndRootFormat,
typescriptConfiguration: options.typescriptConfiguration,
}); });
remotePort++; remotePort++;
} }
@ -93,7 +93,7 @@ export async function hostGeneratorInternal(host: Tree, schema: Schema) {
const projectConfig = readProjectConfiguration(host, options.projectName); const projectConfig = readProjectConfiguration(host, options.projectName);
projectConfig.targets.server.options.webpackConfig = joinPathFragments( projectConfig.targets.server.options.webpackConfig = joinPathFragments(
projectConfig.root, projectConfig.root,
'webpack.server.config.js' `webpack.server.config.${options.typescriptConfiguration ? 'ts' : 'js'}`
); );
updateProjectConfiguration(host, options.projectName, projectConfig); updateProjectConfiguration(host, options.projectName, projectConfig);
} }

View File

@ -1,5 +1,5 @@
import { NormalizedSchema } from '../schema'; import { NormalizedSchema } from '../schema';
import { generateFiles, names } from '@nx/devkit'; import { generateFiles, joinPathFragments, names } from '@nx/devkit';
import { join } from 'path'; import { join } from 'path';
export function addModuleFederationFiles( export function addModuleFederationFiles(
@ -34,11 +34,31 @@ export function addModuleFederationFiles(
templateVariables templateVariables
); );
const pathToModuleFederationFiles = options.typescriptConfiguration
? 'module-federation-ts'
: 'module-federation';
// New entry file is created here. // New entry file is created here.
generateFiles( generateFiles(
host, host,
join(__dirname, `../files/module-federation`), join(__dirname, `../files/${pathToModuleFederationFiles}`),
options.appProjectRoot, options.appProjectRoot,
templateVariables templateVariables
); );
if (options.typescriptConfiguration) {
const pathToWebpackConfig = joinPathFragments(
options.appProjectRoot,
'webpack.config.js'
);
const pathToWebpackProdConfig = joinPathFragments(
options.appProjectRoot,
'webpack.config.prod.js'
);
if (host.exists(pathToWebpackConfig)) {
host.delete(pathToWebpackConfig);
}
if (host.exists(pathToWebpackProdConfig)) {
host.delete(pathToWebpackProdConfig);
}
}
} }

View File

@ -23,9 +23,13 @@ export async function setupSsrForHost(
project.targets.serve.executor = '@nx/react:module-federation-ssr-dev-server'; project.targets.serve.executor = '@nx/react:module-federation-ssr-dev-server';
updateProjectConfiguration(tree, appName, project); updateProjectConfiguration(tree, appName, project);
const pathToModuleFederationSsrFiles = options.typescriptConfiguration
? 'module-federation-ssr-ts'
: 'module-federation-ssr';
generateFiles( generateFiles(
tree, tree,
joinPathFragments(__dirname, '../files/module-federation-ssr'), joinPathFragments(__dirname, `../files/${pathToModuleFederationSsrFiles}`),
project.root, project.root,
{ {
...options, ...options,

View File

@ -24,6 +24,7 @@ export interface Schema {
tags?: string; tags?: string;
unitTestRunner: 'jest' | 'vitest' | 'none'; unitTestRunner: 'jest' | 'vitest' | 'none';
minimal?: boolean; minimal?: boolean;
typescriptConfiguration?: boolean;
} }
export interface NormalizedSchema extends Schema { export interface NormalizedSchema extends Schema {

View File

@ -166,6 +166,11 @@
"description": "Generate a React app with a minimal setup. No nx starter template.", "description": "Generate a React app with a minimal setup. No nx starter template.",
"type": "boolean", "type": "boolean",
"default": false "default": false
},
"typescriptConfiguration": {
"type": "boolean",
"description": "Whether the module federation configuration and webpack configuration files should use TS.",
"default": true
} }
}, },
"required": ["name"], "required": ["name"],

View File

@ -0,0 +1,139 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`remote generator should create the remote with the correct config files 1`] = `
"const { composePlugins, withNx } = require('@nx/webpack');
const { withReact } = require('@nx/react');
const { withModuleFederation } = require('@nx/react/module-federation');
const baseConfig = require('./module-federation.config');
const config = {
...baseConfig,
};
// Nx plugins for webpack to build config object from Nx options and context.
module.exports = composePlugins(
withNx(),
withReact(),
withModuleFederation(config)
);
"
`;
exports[`remote generator should create the remote with the correct config files 2`] = `
"module.exports = require('./webpack.config');
"
`;
exports[`remote generator should create the remote with the correct config files 3`] = `
"module.exports = {
name: 'test',
exposes: {
'./Module': './src/remote-entry.ts',
},
};
"
`;
exports[`remote generator should create the remote with the correct config files when --typescriptConfiguration=true 1`] = `
"import { composePlugins, withNx } from '@nx/webpack';
import { withReact } from '@nx/react';
import { withModuleFederation } from '@nx/react/module-federation';
import baseConfig from './module-federation.config';
const config = {
...baseConfig,
};
// Nx plugins for webpack to build config object from Nx options and context.
export default composePlugins(
withNx(),
withReact(),
withModuleFederation(config)
);
"
`;
exports[`remote generator should create the remote with the correct config files when --typescriptConfiguration=true 2`] = `
"export default require('./webpack.config');
"
`;
exports[`remote generator should create the remote with the correct config files when --typescriptConfiguration=true 3`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: 'test',
exposes: {
'./Module': './src/remote-entry.ts',
},
};
export default config;
"
`;
exports[`remote generator should generate correct remote with config files when using --ssr 1`] = `
"const { composePlugins, withNx } = require('@nx/webpack');
const { withReact } = require('@nx/react');
const { withModuleFederationForSSR } = require('@nx/react/module-federation');
const baseConfig = require('./module-federation.server.config');
const defaultConfig = {
...baseConfig,
};
// Nx plugins for webpack to build config object from Nx options and context.
module.exports = composePlugins(
withNx(),
withReact({ ssr: true }),
withModuleFederationForSSR(defaultConfig)
);
"
`;
exports[`remote generator should generate correct remote with config files when using --ssr 2`] = `
"module.exports = {
name: 'test',
exposes: {
'./Module': 'test/src/remote-entry.ts',
},
};
"
`;
exports[`remote generator should generate correct remote with config files when using --ssr and --typescriptConfiguration=true 1`] = `
"import { composePlugins, withNx } from '@nx/webpack';
import { withReact } from '@nx/react';
import { withModuleFederationForSSR } from '@nx/react/module-federation';
import baseConfig from './module-federation.server.config';
const defaultConfig = {
...baseConfig,
};
// Nx plugins for webpack to build config object from Nx options and context.
export default composePlugins(
withNx(),
withReact({ ssr: true }),
withModuleFederationForSSR(defaultConfig)
);
"
`;
exports[`remote generator should generate correct remote with config files when using --ssr and --typescriptConfiguration=true 2`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: 'test',
exposes: {
'./Module': 'test/src/remote-entry.ts',
},
};
export default config;
"
`;

View File

@ -0,0 +1,10 @@
import {ModuleFederationConfig} from '@nx/webpack';
const config: ModuleFederationConfig = {
name: '<%= projectName %>',
exposes: {
'./Module': '<%= appProjectRoot %>/src/remote-entry.ts',
},
};
export default config;

View File

@ -0,0 +1,45 @@
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(), '<%= browserBuildOutputPath %>');
const serverDist = path.join(process.cwd(), '<%= serverBuildOutputPath %>');
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);

View File

@ -0,0 +1,12 @@
import {composePlugins, withNx} from '@nx/webpack';
import {withReact} from '@nx/react';
import {withModuleFederationForSSR} from '@nx/react/module-federation';
import baseConfig from "./module-federation.server.config";
const defaultConfig = {
...baseConfig,
};
// Nx plugins for webpack to build config object from Nx options and context.
export default composePlugins(withNx(), withReact({ssr: true}), withModuleFederationForSSR(defaultConfig));

View File

@ -0,0 +1,10 @@
import {ModuleFederationConfig} from '@nx/webpack';
const config: ModuleFederationConfig = {
name: '<%= projectName %>',
exposes: {
'./Module': './src/remote-entry.ts',
},
};
export default config;

View File

@ -0,0 +1 @@
import('./bootstrap');

View File

@ -0,0 +1 @@
export { default } from './app/app';

View File

@ -0,0 +1 @@
export default require('./webpack.config');

View File

@ -0,0 +1,12 @@
import {composePlugins, withNx} from '@nx/webpack';
import {withReact} from '@nx/react';
import {withModuleFederation} from '@nx/react/module-federation';
import baseConfig from './module-federation.config';
const config = {
...baseConfig,
};
// Nx plugins for webpack to build config object from Nx options and context.
export default composePlugins(withNx(), withReact(), withModuleFederation(config));

View File

@ -20,9 +20,13 @@ export async function setupSsrForRemote(
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
const project = readProjectConfiguration(tree, appName); const project = readProjectConfiguration(tree, appName);
const pathToModuleFederationSsrFiles = options.typescriptConfiguration
? 'module-federation-ssr-ts'
: 'module-federation-ssr';
generateFiles( generateFiles(
tree, tree,
joinPathFragments(__dirname, '../files/module-federation-ssr'), joinPathFragments(__dirname, `../files/${pathToModuleFederationSsrFiles}`),
project.root, project.root,
{ {
...options, ...options,

View File

@ -25,10 +25,18 @@ export function updateHostWithRemote(
} }
const hostConfig = readProjectConfiguration(host, hostName); const hostConfig = readProjectConfiguration(host, hostName);
const moduleFederationConfigPath = joinPathFragments( let moduleFederationConfigPath = joinPathFragments(
hostConfig.root, hostConfig.root,
'module-federation.config.js' 'module-federation.config.js'
); );
if (!host.exists(moduleFederationConfigPath)) {
moduleFederationConfigPath = joinPathFragments(
hostConfig.root,
'module-federation.config.ts'
);
}
const remoteDefsPath = joinPathFragments( const remoteDefsPath = joinPathFragments(
hostConfig.sourceRoot, hostConfig.sourceRoot,
'remotes.d.ts' 'remotes.d.ts'

View File

@ -4,6 +4,56 @@ import { Linter } from '@nx/linter';
import remote from './remote'; import remote from './remote';
describe('remote generator', () => { describe('remote generator', () => {
it('should create the remote with the correct config files', async () => {
const tree = createTreeWithEmptyWorkspace();
await remote(tree, {
name: 'test',
devServerPort: 4201,
e2eTestRunner: 'cypress',
linter: Linter.EsLint,
skipFormat: false,
style: 'css',
unitTestRunner: 'jest',
projectNameAndRootFormat: 'as-provided',
typescriptConfiguration: false,
});
expect(tree.exists('test/webpack.config.js')).toBeTruthy();
expect(tree.exists('test/webpack.config.prod.js')).toBeTruthy();
expect(tree.exists('test/module-federation.config.js')).toBeTruthy();
expect(tree.read('test/webpack.config.js', 'utf-8')).toMatchSnapshot();
expect(tree.read('test/webpack.config.prod.js', 'utf-8')).toMatchSnapshot();
expect(
tree.read('test/module-federation.config.js', 'utf-8')
).toMatchSnapshot();
});
it('should create the remote with the correct config files when --typescriptConfiguration=true', async () => {
const tree = createTreeWithEmptyWorkspace();
await remote(tree, {
name: 'test',
devServerPort: 4201,
e2eTestRunner: 'cypress',
linter: Linter.EsLint,
skipFormat: false,
style: 'css',
unitTestRunner: 'jest',
projectNameAndRootFormat: 'as-provided',
typescriptConfiguration: true,
});
expect(tree.exists('test/webpack.config.ts')).toBeTruthy();
expect(tree.exists('test/webpack.config.prod.ts')).toBeTruthy();
expect(tree.exists('test/module-federation.config.ts')).toBeTruthy();
expect(tree.read('test/webpack.config.ts', 'utf-8')).toMatchSnapshot();
expect(tree.read('test/webpack.config.prod.ts', 'utf-8')).toMatchSnapshot();
expect(
tree.read('test/module-federation.config.ts', 'utf-8')
).toMatchSnapshot();
});
it('should install @nx/web for the file-server executor', async () => { it('should install @nx/web for the file-server executor', async () => {
const tree = createTreeWithEmptyWorkspace(); const tree = createTreeWithEmptyWorkspace();
await remote(tree, { await remote(tree, {
@ -57,4 +107,58 @@ describe('remote generator', () => {
expect(mainFile).toContain(`join(process.cwd(), 'dist/test/browser')`); expect(mainFile).toContain(`join(process.cwd(), 'dist/test/browser')`);
expect(mainFile).toContain('nx.server.ready'); expect(mainFile).toContain('nx.server.ready');
}); });
it('should generate correct remote with config files when using --ssr', async () => {
const tree = createTreeWithEmptyWorkspace();
await remote(tree, {
name: 'test',
devServerPort: 4201,
e2eTestRunner: 'cypress',
linter: Linter.EsLint,
skipFormat: false,
style: 'css',
unitTestRunner: 'jest',
ssr: true,
projectNameAndRootFormat: 'as-provided',
typescriptConfiguration: false,
});
expect(tree.exists('test/webpack.server.config.js')).toBeTruthy();
expect(tree.exists('test/module-federation.server.config.js')).toBeTruthy();
expect(
tree.read('test/webpack.server.config.js', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('test/module-federation.server.config.js', 'utf-8')
).toMatchSnapshot();
});
it('should generate correct remote with config files when using --ssr and --typescriptConfiguration=true', async () => {
const tree = createTreeWithEmptyWorkspace();
await remote(tree, {
name: 'test',
devServerPort: 4201,
e2eTestRunner: 'cypress',
linter: Linter.EsLint,
skipFormat: false,
style: 'css',
unitTestRunner: 'jest',
ssr: true,
projectNameAndRootFormat: 'as-provided',
typescriptConfiguration: true,
});
expect(tree.exists('test/webpack.server.config.ts')).toBeTruthy();
expect(tree.exists('test/module-federation.server.config.ts')).toBeTruthy();
expect(
tree.read('test/webpack.server.config.ts', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('test/module-federation.server.config.ts', 'utf-8')
).toMatchSnapshot();
});
}); });

View File

@ -22,7 +22,7 @@ import { setupSsrForRemote } from './lib/setup-ssr-for-remote';
export function addModuleFederationFiles( export function addModuleFederationFiles(
host: Tree, host: Tree,
options: NormalizedSchema options: NormalizedSchema<Schema>
) { ) {
const templateVariables = { const templateVariables = {
...names(options.name), ...names(options.name),
@ -30,12 +30,33 @@ export function addModuleFederationFiles(
tmpl: '', tmpl: '',
}; };
const pathToModuleFederationFiles = options.typescriptConfiguration
? 'module-federation-ts'
: 'module-federation';
generateFiles( generateFiles(
host, host,
join(__dirname, `./files/module-federation`), join(__dirname, `./files/${pathToModuleFederationFiles}`),
options.appProjectRoot, options.appProjectRoot,
templateVariables templateVariables
); );
if (options.typescriptConfiguration) {
const pathToWebpackConfig = joinPathFragments(
options.appProjectRoot,
'webpack.config.js'
);
const pathToWebpackProdConfig = joinPathFragments(
options.appProjectRoot,
'webpack.config.prod.js'
);
if (host.exists(pathToWebpackConfig)) {
host.delete(pathToWebpackConfig);
}
if (host.exists(pathToWebpackProdConfig)) {
host.delete(pathToWebpackProdConfig);
}
}
} }
export async function remoteGenerator(host: Tree, schema: Schema) { export async function remoteGenerator(host: Tree, schema: Schema) {
@ -47,11 +68,10 @@ export async function remoteGenerator(host: Tree, schema: Schema) {
export async function remoteGeneratorInternal(host: Tree, schema: Schema) { export async function remoteGeneratorInternal(host: Tree, schema: Schema) {
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
const options = await normalizeOptions<Schema>( const options: NormalizedSchema<Schema> = {
host, ...(await normalizeOptions<Schema>(host, schema, '@nx/react:remote')),
schema, typescriptConfiguration: schema.typescriptConfiguration ?? true,
'@nx/react:remote' };
);
const initAppTask = await applicationGenerator(host, { const initAppTask = await applicationGenerator(host, {
...options, ...options,
// Only webpack works with module federation for now. // Only webpack works with module federation for now.
@ -93,7 +113,7 @@ export async function remoteGeneratorInternal(host: Tree, schema: Schema) {
const projectConfig = readProjectConfiguration(host, options.projectName); const projectConfig = readProjectConfiguration(host, options.projectName);
projectConfig.targets.server.options.webpackConfig = joinPathFragments( projectConfig.targets.server.options.webpackConfig = joinPathFragments(
projectConfig.root, projectConfig.root,
'webpack.server.config.js' `webpack.server.config.${options.typescriptConfiguration ? 'ts' : 'js'}`
); );
updateProjectConfiguration(host, options.projectName, projectConfig); updateProjectConfiguration(host, options.projectName, projectConfig);
} }

View File

@ -1,6 +1,7 @@
import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils'; import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
import type { Linter } from '@nx/linter'; import type { Linter } from '@nx/linter';
import type { SupportedStyles } from '../../../typings'; import type { SupportedStyles } from '../../../typings';
import type { NormalizedSchema as ApplicationNormalizedSchema } from '../application/schema';
export interface Schema { export interface Schema {
classComponent?: boolean; classComponent?: boolean;
@ -24,4 +25,9 @@ export interface Schema {
style: SupportedStyles; style: SupportedStyles;
tags?: string; tags?: string;
unitTestRunner: 'jest' | 'vitest' | 'none'; unitTestRunner: 'jest' | 'vitest' | 'none';
typescriptConfiguration?: boolean;
}
export interface NormalizedSchema extends ApplicationNormalizedSchema {
typescriptConfiguration: boolean;
} }

View File

@ -164,6 +164,11 @@
"description": "Whether to configure SSR for the host application", "description": "Whether to configure SSR for the host application",
"type": "boolean", "type": "boolean",
"default": false "default": false
},
"typescriptConfiguration": {
"type": "boolean",
"description": "Whether the module federation configuration and webpack configuration files should use TS.",
"default": true
} }
}, },
"required": ["name"], "required": ["name"],

View File

@ -13,6 +13,7 @@ export function updateModuleFederationProject(
projectName: string; projectName: string;
appProjectRoot: string; appProjectRoot: string;
devServerPort?: number; devServerPort?: number;
typescriptConfiguration?: boolean;
} }
): GeneratorCallback { ): GeneratorCallback {
const projectConfig = readProjectConfiguration(host, options.projectName); const projectConfig = readProjectConfiguration(host, options.projectName);
@ -20,12 +21,16 @@ export function updateModuleFederationProject(
projectConfig.targets.build.options = { projectConfig.targets.build.options = {
...projectConfig.targets.build.options, ...projectConfig.targets.build.options,
main: `${options.appProjectRoot}/src/main.ts`, main: `${options.appProjectRoot}/src/main.ts`,
webpackConfig: `${options.appProjectRoot}/webpack.config.js`, webpackConfig: `${options.appProjectRoot}/webpack.config.${
options.typescriptConfiguration ? 'ts' : 'js'
}`,
}; };
projectConfig.targets.build.configurations.production = { projectConfig.targets.build.configurations.production = {
...projectConfig.targets.build.configurations.production, ...projectConfig.targets.build.configurations.production,
webpackConfig: `${options.appProjectRoot}/webpack.config.prod.js`, webpackConfig: `${options.appProjectRoot}/webpack.config.prod.${
options.typescriptConfiguration ? 'ts' : 'js'
}`,
}; };
projectConfig.targets.serve.executor = projectConfig.targets.serve.executor =