feat(module-federation): consolidate module federation utils into module-federation package (#28919)

- feat(module-federation): consolidate module federation utils into
module-federation package
- chore(module-federation): fix tests and linting

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->
Our current support for Module Federation relies on utilities that are
spread and duplicated across the `@nx/webpack` package and the
`@nx/rspack` package.



## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
Now that we have a `@nx/module-federation` package, dedupe the utils and
consolidate them into a single package

## Todo
- [x] Migrations for React + Angular to install `@nx/module-federation`
and point `ModuleFederationConfig` export to that package from
webpack.config and rspack.config files
This commit is contained in:
Colum Ferry 2024-11-18 19:15:10 +00:00 committed by GitHub
parent 0407b7a7b4
commit 76d61ea5e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
99 changed files with 995 additions and 2156 deletions

View File

@ -94,10 +94,9 @@ rust-toolchain @nrwl/nx-native-reviewers
/packages/web/** @nrwl/nx-js-reviewers
/e2e/web/** @nrwl/nx-js-reviewers
/packages/webpack/** @nrwl/nx-js-reviewers
/packages/webpack/src/utils/module-federation @jaysoo @Coly010
/e2e/webpack/** @nrwl/nx-js-reviewers
/packages/rspack/** @nrwl/nx-js-reviewers
/packages/rspack/src/utils/module-federation @jaysoo @Coly010
/packages/rspack/src/utils/module-federation @nrwl/nx-js-reviewers
/e2e/rspack/** @nrwl/nx-js-reviewers
/packages/esbuild/** @nrwl/nx-js-reviewers
/e2e/esbuild/** @nrwl/nx-js-reviewers

View File

@ -266,6 +266,12 @@
"version": "19.6.1-beta.0",
"description": "Ensure Target Defaults are set correctly for Module Federation.",
"factory": "./src/migrations/update-19-6-1/ensure-depends-on-for-mf"
},
"update-20-2-0-update-module-federation-config-import": {
"cli": "nx",
"version": "20.2.0-beta.2",
"description": "Update the ModuleFederationConfig import use @nx/module-federation.",
"factory": "./src/migrations/update-20-2-0/migrate-mf-imports-to-new-package"
}
},
"packageJsonUpdates": {

View File

@ -63,6 +63,7 @@
"@nx/js": "file:../js",
"@nx/eslint": "file:../eslint",
"@nx/webpack": "file:../webpack",
"@nx/module-federation": "file:../module-federation",
"@nx/web": "file:../web",
"@nx/workspace": "file:../workspace",
"piscina": "^4.4.0"

View File

@ -4,7 +4,7 @@ import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { fork } from 'node:child_process';
import { join } from 'node:path';
import { createWriteStream } from 'node:fs';
import type { StaticRemotesConfig } from '@nx/webpack/src/utils/module-federation/parse-static-remotes-config';
import type { StaticRemotesConfig } from '@nx/module-federation/src/utils';
export async function buildStaticRemotes(
staticRemotesConfig: StaticRemotesConfig,

View File

@ -3,7 +3,7 @@ import { type Schema } from '../schema';
import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl';
import { join } from 'path';
import { cpSync } from 'fs';
import type { StaticRemotesConfig } from '@nx/webpack/src/utils/module-federation/parse-static-remotes-config';
import type { StaticRemotesConfig } from '@nx/module-federation/src/utils';
export function startStaticRemotesFileServer(
staticRemotesConfig: StaticRemotesConfig,

View File

@ -19,7 +19,9 @@ import {
import {
getModuleFederationConfig,
getRemotes,
} from '@nx/webpack/src/utils/module-federation';
startRemoteProxies,
parseStaticRemotesConfig,
} from '@nx/module-federation/src/utils';
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl';
import { createBuilderContext } from 'nx/src/adapter/ngcli-adapter';
@ -30,8 +32,6 @@ import {
} from '../../builders/utilities/module-federation';
import { extname, join } from 'path';
import { existsSync } from 'fs';
import { startRemoteProxies } from '@nx/webpack/src/utils/module-federation/start-remote-proxies';
import { parseStaticRemotesConfig } from '@nx/webpack/src/utils/module-federation/parse-static-remotes-config';
export async function* moduleFederationDevServerExecutor(
schema: Schema,

View File

@ -4,7 +4,7 @@ import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { fork } from 'node:child_process';
import { join } from 'node:path';
import { createWriteStream } from 'node:fs';
import type { StaticRemotesConfig } from '@nx/webpack/src/utils/module-federation/parse-static-remotes-config';
import type { StaticRemotesConfig } from '@nx/module-federation/src/utils';
export async function buildStaticRemotes(
staticRemotesConfig: StaticRemotesConfig,

View File

@ -3,7 +3,7 @@ import { type Schema } from '../schema';
import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl';
import { join } from 'path';
import { cpSync, rmSync } from 'fs';
import type { StaticRemotesConfig } from '@nx/webpack/src/utils/module-federation/parse-static-remotes-config';
import type { StaticRemotesConfig } from '@nx/module-federation/src/utils';
import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable';
export function startStaticRemotes(

View File

@ -10,8 +10,9 @@ import type { Schema } from './schema';
import {
getModuleFederationConfig,
getRemotes,
} from '@nx/webpack/src/utils/module-federation';
import { parseStaticSsrRemotesConfig } from '@nx/webpack/src/utils/module-federation/parse-static-remotes-config';
parseStaticSsrRemotesConfig,
startSsrRemoteProxies,
} from '@nx/module-federation/src/utils';
import { buildStaticRemotes } from './lib/build-static-remotes';
import { startRemotes } from './lib/start-dev-remotes';
import { startStaticRemotes } from './lib/start-static-remotes';
@ -24,7 +25,6 @@ import { eachValueFrom } from '@nx/devkit/src/utils/rxjs-for-await';
import { createBuilderContext } from 'nx/src/adapter/ngcli-adapter';
import { normalizeOptions } from './lib/normalize-options';
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
import { startSsrRemoteProxies } from '@nx/webpack/src/utils/module-federation/start-ssr-remote-proxies';
import { getInstalledAngularVersionInfo } from '../utilities/angular-version-utils';
export async function* moduleFederationSsrDevServerExecutor(

View File

@ -642,7 +642,7 @@ export default bootstrap;
exports[`Host App Generator --ssr should generate the correct files for standalone when --typescript=true 4`] = `"import('./src/main.server');"`;
exports[`Host App Generator --ssr should generate the correct files for standalone when --typescript=true 5`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
"import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: 'test',
@ -880,7 +880,7 @@ export default AppServerModule;
exports[`Host App Generator --ssr should generate the correct files when --typescript=true 5`] = `"import('./src/main.server');"`;
exports[`Host App Generator --ssr should generate the correct files when --typescript=true 6`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
"import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: 'test',

View File

@ -435,7 +435,7 @@ export default AppServerModule;
exports[`MF Remote App Generator --ssr should generate the correct files when --typescriptConfiguration=true 5`] = `"import('./src/main.server');"`;
exports[`MF Remote App Generator --ssr should generate the correct files when --typescriptConfiguration=true 6`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
"import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: 'test',
@ -691,7 +691,7 @@ bootstrapApplication(RemoteEntryComponent, appConfig).catch((err) =>
`;
exports[`MF Remote App Generator should generate the a remote setup for standalone components when --typescriptConfiguration=true 2`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
"import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: 'test',

View File

@ -98,7 +98,7 @@ module.exports = {
`;
exports[`Init MF should add a remote application and add it to a specified host applications webpack config that contains a remote application already when --typescriptConfiguration=true 1`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
"import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: 'app1',
@ -148,7 +148,7 @@ module.exports = {
`;
exports[`Init MF should add a remote application and add it to a specified host applications webpack config when no other remote has been added to it when --typescriptConfiguration=true 1`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
"import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: 'app1',
@ -284,7 +284,7 @@ export default withModuleFederation(config, { dts: false });
`;
exports[`Init MF should create webpack and mf configs correctly when --typescriptConfiguration=true 2`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
"import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: 'app1',
@ -324,7 +324,7 @@ export default withModuleFederation(config, { dts: false });
`;
exports[`Init MF should create webpack and mf configs correctly when --typescriptConfiguration=true 4`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
"import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: 'remote1',

View File

@ -1,4 +1,4 @@
import { ModuleFederationConfig } from '@nx/webpack';
import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: '<%= name %>',<% if(type === 'host') { %>

View File

@ -0,0 +1,334 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import migrateMfImportsToNewPackage from './migrate-mf-imports-to-new-package';
jest.mock('@nx/devkit', () => {
const original = jest.requireActual('@nx/devkit');
return {
...original,
createProjectGraphAsync: jest.fn().mockResolvedValue(
Promise.resolve({
dependencies: {
shell: [
{
source: 'shell',
target: 'npm:@nx/webpack',
type: 'static',
},
],
remote: [
{
source: 'remote',
target: 'npm:@nx/webpack',
type: 'static',
},
],
},
nodes: {
shell: {
name: 'shell',
type: 'app',
data: {
root: 'apps/shell',
sourceRoot: 'shell/src',
targets: {},
},
},
remote: {
name: 'remote',
type: 'app',
data: {
root: 'apps/remote',
sourceRoot: 'remote/src',
targets: {},
},
},
},
})
),
};
});
describe('migrate-mf-imports-to-new-package', () => {
it('should update the ModuleFederationConfig import to change the import when its a single line', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write(
'apps/shell/webpack.config.js',
`import { ModuleFederationConfig } from '@nx/webpack';`
);
tree.write(
'apps/shell/module-federation.config.ts',
`import { ModuleFederationConfig } from '@nx/webpack';`
);
tree.write(
'apps/remote/webpack.config.ts',
`import { ModuleFederationConfig } from '@nx/webpack';`
);
tree.write(
'apps/remote/module-federation.config.js',
`import { ModuleFederationConfig } from '@nx/webpack';`
);
// ACT
await migrateMfImportsToNewPackage(tree);
// ASSERT
expect(tree.read('apps/shell/webpack.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
expect(tree.read('apps/remote/webpack.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
expect(tree.read('apps/shell/module-federation.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
expect(tree.read('apps/remote/module-federation.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
});
it('should not update the ModuleFederationConfig import when its correct', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write(
'apps/shell/webpack.config.js',
`import { ModuleFederationConfig } from '@nx/module-federation';`
);
tree.write(
'apps/shell/module-federation.config.ts',
`import { ModuleFederationConfig } from '@nx/module-federation';`
);
tree.write(
'apps/remote/webpack.config.ts',
`import { ModuleFederationConfig } from '@nx/module-federation';`
);
tree.write(
'apps/remote/module-federation.config.js',
`import { ModuleFederationConfig } from '@nx/module-federation';`
);
// ACT
await migrateMfImportsToNewPackage(tree);
// ASSERT
expect(tree.read('apps/shell/webpack.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
expect(tree.read('apps/remote/webpack.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
expect(tree.read('apps/shell/module-federation.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
expect(tree.read('apps/remote/module-federation.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
});
it('should update the ModuleFederationConfig import to change the import when its across multiple lines', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write(
'apps/shell/webpack.config.js',
`import {
ModuleFederationConfig
} from '@nx/webpack';`
);
tree.write(
'apps/shell/module-federation.config.ts',
`import {
ModuleFederationConfig
} from '@nx/webpack';`
);
tree.write(
'apps/remote/webpack.config.ts',
`import {
ModuleFederationConfig
} from '@nx/webpack';`
);
tree.write(
'apps/remote/module-federation.config.js',
`import {
ModuleFederationConfig
} from '@nx/webpack';`
);
// ACT
await migrateMfImportsToNewPackage(tree);
// ASSERT
expect(tree.read('apps/shell/webpack.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
expect(tree.read('apps/remote/webpack.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
expect(tree.read('apps/shell/module-federation.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
expect(tree.read('apps/remote/module-federation.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
});
it('should update the ModuleFederationConfig import to change the import when its a part of multiple imports', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write(
'apps/shell/webpack.config.js',
`import { something, ModuleFederationConfig } from '@nx/webpack';`
);
tree.write(
'apps/shell/module-federation.config.ts',
`import { ModuleFederationConfig, something} from '@nx/webpack';`
);
tree.write(
'apps/remote/webpack.config.ts',
`import { something, ModuleFederationConfig } from '@nx/webpack';`
);
tree.write(
'apps/remote/module-federation.config.js',
`import { ModuleFederationConfig, something } from '@nx/webpack';`
);
// ACT
await migrateMfImportsToNewPackage(tree);
// ASSERT
expect(tree.read('apps/shell/webpack.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
import { something } from '@nx/webpack';
"
`);
expect(tree.read('apps/remote/webpack.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
import { something } from '@nx/webpack';
"
`);
expect(tree.read('apps/shell/module-federation.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
import { something } from '@nx/webpack';
"
`);
expect(tree.read('apps/remote/module-federation.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
import { something } from '@nx/webpack';
"
`);
});
it('should update the ModuleFederationConfig import to change the import when its a part of multiple imports across multiple lines', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write(
'apps/shell/webpack.config.js',
`import {
something,
ModuleFederationConfig
} from '@nx/webpack';`
);
tree.write(
'apps/shell/module-federation.config.ts',
`import {
ModuleFederationConfig,
something
} from '@nx/webpack';`
);
tree.write(
'apps/remote/webpack.config.ts',
`import {
something,
ModuleFederationConfig
} from '@nx/webpack';`
);
tree.write(
'apps/remote/module-federation.config.js',
`import {
ModuleFederationConfig,
something
} from '@nx/webpack';`
);
// ACT
await migrateMfImportsToNewPackage(tree);
// ASSERT
expect(tree.read('apps/shell/webpack.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
import { something } from '@nx/webpack';
"
`);
expect(tree.read('apps/remote/webpack.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
import { something } from '@nx/webpack';
"
`);
expect(tree.read('apps/shell/module-federation.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
import { something } from '@nx/webpack';
"
`);
expect(tree.read('apps/remote/module-federation.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
import { something } from '@nx/webpack';
"
`);
});
it('should not incorrectly update import when it is run twice', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write(
`apps/shell/webpack.config.ts`,
`import { composePlugins, withNx, ModuleFederationConfig } from '@nx/webpack';
import { withReact } from '@nx/react';
import { withModuleFederation } from '@nx/react/module-federation';`
);
// ACT
await migrateMfImportsToNewPackage(tree);
await migrateMfImportsToNewPackage(tree);
// ASSERT
expect(tree.read('apps/shell/webpack.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
import { composePlugins, withNx } from '@nx/webpack';
import { withReact } from '@nx/react';
import { withModuleFederation } from '@nx/react/module-federation';
"
`);
});
});

View File

@ -0,0 +1,78 @@
import { createProjectGraphAsync, Tree } from '@nx/devkit';
import { formatFiles, visitNotIgnoredFiles } from '@nx/devkit';
import { tsquery } from '@phenomnomnominal/tsquery';
const MF_IMPORT_TO_UPDATE = 'ModuleFederationConfig';
const MF_CONFIG_IMPORT_SELECTOR = `ImportDeclaration:has(StringLiteral[value=@nx/webpack]):has(Identifier[name=ModuleFederationConfig])`;
const IMPORT_TOKENS_SELECTOR = `ImportClause ImportSpecifier`;
const MF_CONFIG_IMPORT_SPECIFIER_SELECTOR = `ImportClause ImportSpecifier > Identifier[name=ModuleFederationConfig]`;
const WEBPACK_IMPORT_SELECTOR = `ImportDeclaration > StringLiteral[value=@nx/webpack]`;
export default async function migrateMfImportsToNewPackage(tree: Tree) {
const rootsToCheck = new Set<string>();
const graph = await createProjectGraphAsync();
for (const [project, dependencies] of Object.entries(graph.dependencies)) {
const usesNxWebpack = dependencies.some(
(dep) => dep.target === 'npm:@nx/webpack'
);
if (usesNxWebpack) {
const root = graph.nodes[project].data.root;
rootsToCheck.add(root);
}
}
for (const root of rootsToCheck) {
visitNotIgnoredFiles(tree, root, (filePath) => {
if (!filePath.endsWith('.ts') && !filePath.endsWith('.js')) {
return;
}
let contents = tree.read(filePath, 'utf-8');
if (!contents.includes(MF_IMPORT_TO_UPDATE)) {
return;
}
const ast = tsquery.ast(contents);
const importNodes = tsquery(ast, MF_CONFIG_IMPORT_SELECTOR);
if (importNodes.length === 0) {
return;
}
const importNode = importNodes[0];
const importSpecifiers = tsquery(importNode, IMPORT_TOKENS_SELECTOR);
if (importSpecifiers.length > 1) {
const mfConfigImportSpecifierNode = tsquery(
importNode,
MF_CONFIG_IMPORT_SPECIFIER_SELECTOR
)[0];
const end =
contents.charAt(mfConfigImportSpecifierNode.getEnd()) === ','
? mfConfigImportSpecifierNode.getEnd() + 1
: mfConfigImportSpecifierNode.getEnd();
contents = `import { ${MF_IMPORT_TO_UPDATE} } from '@nx/module-federation';
${contents.slice(
0,
mfConfigImportSpecifierNode.getStart()
)}${contents.slice(end)}`;
} else {
const nxWebpackImportStringNodes = tsquery(
ast,
WEBPACK_IMPORT_SELECTOR
);
if (nxWebpackImportStringNodes.length === 0) {
return;
}
const nxWebpackImportStringNode = nxWebpackImportStringNodes[0];
contents = `${contents.slice(
0,
nxWebpackImportStringNode.getStart()
)}'@nx/module-federation'${contents.slice(
nxWebpackImportStringNode.getEnd()
)}`;
}
tree.write(filePath, contents);
});
}
await formatFiles(tree);
}

View File

@ -8,7 +8,7 @@ import {
SharedLibraryConfig,
sharePackages,
shareWorkspaceLibraries,
} from '@nx/webpack/src/utils/module-federation';
} from '@nx/module-federation';
import {
createProjectGraphAsync,

View File

@ -1,7 +1,7 @@
import type {
ModuleFederationConfig,
NxModuleFederationConfigOverride,
} from '@nx/webpack/src/utils/module-federation';
} from '@nx/module-federation';
import { getModuleFederationConfig } from './utils';
export async function withModuleFederationForSSR(
@ -58,7 +58,7 @@ export async function withModuleFederationForSSR(
...(configOverride?.runtimePlugins ?? []),
require.resolve('@module-federation/node/runtimePlugin'),
require.resolve(
'@nx/webpack/src/utils/module-federation/plugins/runtime-library-control.plugin.js'
'@nx/module-federation/src/utils/plugins/runtime-library-control.plugin.js'
),
]
: [

View File

@ -1,7 +1,7 @@
import type {
ModuleFederationConfig,
NxModuleFederationConfigOverride,
} from '@nx/webpack/src/utils/module-federation';
} from '@nx/module-federation';
import { getModuleFederationConfig } from './utils';
import { ModuleFederationPlugin } from '@module-federation/enhanced/webpack';
@ -62,7 +62,7 @@ export async function withModuleFederation(
? [
...(configOverride?.runtimePlugins ?? []),
require.resolve(
'@nx/webpack/src/utils/module-federation/plugins/runtime-library-control.plugin.js'
'@nx/module-federation/src/utils/plugins/runtime-library-control.plugin.js'
),
]
: configOverride?.runtimePlugins,

View File

@ -0,0 +1 @@
export * from './src/utils/public-api';

View File

@ -24,7 +24,16 @@
"main": "index.js",
"executors": "./executors.json",
"dependencies": {
"tslib": "^2.3.0"
"tslib": "^2.3.0",
"@nx/devkit": "file:../devkit",
"@nx/js": "file:../js",
"picocolors": "^1.1.0",
"@module-federation/sdk": "0.6.9",
"webpack": "5.88.0",
"@rspack/core": "1.0.5",
"@module-federation/enhanced": "0.6.9",
"express": "^4.19.2",
"http-proxy-middleware": "^3.0.3"
},
"peerDependencies": {},
"nx-migrations": {

View File

@ -10,11 +10,6 @@
"outputPath": "build/packages/module-federation",
"tsConfig": "packages/module-federation/tsconfig.lib.json",
"main": "packages/module-federation/index.ts",
"generateExportsField": true,
"additionalEntryPoints": [
"{projectRoot}/{executors,generators,migrations}.json",
"{projectRoot}/plugin.ts"
],
"assets": [
{
"input": "packages/module-federation",

View File

@ -4,7 +4,7 @@ import { findMatchingProjects } from 'nx/src/utils/find-matching-projects';
import * as pc from 'picocolors';
import { join } from 'path';
import { existsSync, readFileSync } from 'fs';
import { ModuleFederationConfig } from './models/index';
import { ModuleFederationConfig } from './models';
interface ModuleFederationExecutorContext {
projectName: string;

View File

@ -4,3 +4,6 @@ export * from './package-json';
export * from './remotes';
export * from './models';
export * from './get-remotes-for-host';
export * from './parse-static-remotes-config';
export * from './start-remote-proxies';
export * from './start-ssr-remote-proxies';

View File

@ -1,5 +1,5 @@
import type { moduleFederationPlugin } from '@module-federation/sdk';
import type { NormalModuleReplacementPlugin } from '@rspack/core';
import type { NormalModuleReplacementPlugin as RspackNormalModuleReplacementPlugin } from '@rspack/core';
export type ModuleFederationLibrary = { type: string; name: string };
@ -15,7 +15,9 @@ export type SharedWorkspaceLibraryConfig = {
projectRoot: string,
eager?: boolean
) => Record<string, SharedLibraryConfig>;
getReplacementPlugin: () => NormalModuleReplacementPlugin;
getReplacementPlugin: () =>
| RspackNormalModuleReplacementPlugin
| import('webpack').NormalModuleReplacementPlugin;
};
export type Remotes = Array<string | [remoteName: string, remoteUrl: string]>;
@ -55,13 +57,7 @@ export interface ModuleFederationConfig {
export type NxModuleFederationConfigOverride = Omit<
moduleFederationPlugin.ModuleFederationPluginOptions,
| 'exposes'
| 'remotes'
| 'name'
| 'library'
| 'shared'
| 'filename'
| 'remoteType'
'exposes' | 'remotes' | 'name' | 'shared' | 'filename'
>;
export type WorkspaceLibrarySecondaryEntryPoint = {

View File

@ -2,6 +2,7 @@ import {
AdditionalSharedConfig,
ModuleFederationConfig,
ModuleFederationLibrary,
NxModuleFederationConfigOverride,
Remotes,
SharedFunction,
SharedLibraryConfig,
@ -26,6 +27,7 @@ import { readRootPackageJson } from './package-json';
export {
ModuleFederationConfig,
NxModuleFederationConfigOverride,
SharedLibraryConfig,
SharedWorkspaceLibraryConfig,
AdditionalSharedConfig,

View File

@ -20,6 +20,7 @@ import {
} from '@nx/devkit';
import { existsSync } from 'fs';
import type { PackageJson } from 'nx/src/utils/package-json';
import { NormalModuleReplacementPlugin as RspackNormalModuleReplacementPlugin } from '@rspack/core';
/**
* Build an object of functions to be used with the ModuleFederationPlugin to
@ -27,10 +28,12 @@ import type { PackageJson } from 'nx/src/utils/package-json';
*
* @param workspaceLibs - The Nx Workspace Libraries to share
* @param tsConfigPath - The path to TS Config File that contains the Path Mappings for the Libraries
* @param bundler - The bundler to use for the replacement plugin
*/
export function shareWorkspaceLibraries(
workspaceLibs: WorkspaceLibrary[],
tsConfigPath = process.env.NX_TSCONFIG_PATH ?? getRootTsConfigPath()
tsConfigPath = process.env.NX_TSCONFIG_PATH ?? getRootTsConfigPath(),
bundler: 'rspack' | 'webpack' = 'rspack'
): SharedWorkspaceLibraryConfig {
if (!workspaceLibs) {
return getEmptySharedLibrariesConfig();
@ -76,14 +79,17 @@ export function shareWorkspaceLibraries(
});
}
const webpack = require('webpack');
const normalModuleReplacementPluginImpl =
bundler === 'rspack'
? RspackNormalModuleReplacementPlugin
: require('webpack').NormalModuleReplacementPlugin;
return {
getAliases: () =>
pathMappings.reduce(
(aliases, library) => ({
...aliases,
// If the library path ends in a wildcard, remove it as webpack can't handle this in resolve.alias
// If the library path ends in a wildcard, remove it as webpack/rspack can't handle this in resolve.alias
// e.g. path/to/my/lib/* -> path/to/my/lib
[library.name]: library.path.replace(/\/\*$/, ''),
}),
@ -139,7 +145,7 @@ export function shareWorkspaceLibraries(
}, {} as Record<string, SharedLibraryConfig>);
},
getReplacementPlugin: () =>
new webpack.NormalModuleReplacementPlugin(/./, (req) => {
new normalModuleReplacementPluginImpl(/./, (req) => {
if (!req.request.startsWith('.')) {
return;
}
@ -156,7 +162,7 @@ export function shareWorkspaceLibraries(
* library.path is usually in the form of "/Users/username/path/to/Workspace/path/to/library"
*
* When a wildcard is used in the TS path mappings, we want to get everything after the import to
* re-route the request correctly inline with the webpack resolve.alias
* re-route the request correctly inline with the webpack/rspack resolve.alias
*/
join(
library.name,
@ -317,12 +323,17 @@ function addStringDependencyToSharedConfig(
}
}
function getEmptySharedLibrariesConfig() {
const webpack = require('webpack');
function getEmptySharedLibrariesConfig(
bundler: 'rspack' | 'webpack' = 'rspack'
) {
const normalModuleReplacementPluginImpl =
bundler === 'rspack'
? RspackNormalModuleReplacementPlugin
: require('webpack').NormalModuleReplacementPlugin;
return {
getAliases: () => ({}),
getLibraries: () => ({}),
getReplacementPlugin: () =>
new webpack.NormalModuleReplacementPlugin(/./, () => {}),
new normalModuleReplacementPluginImpl(/./, () => {}),
};
}

View File

@ -29,6 +29,12 @@
"version": "19.6.1-beta.0",
"description": "Ensure Target Defaults are set correctly for Module Federation.",
"factory": "./src/migrations/update-19-6-1/ensure-depends-on-for-mf"
},
"update-20-2-0-update-module-federation-config-import": {
"cli": "nx",
"version": "20.2.0-beta.2",
"description": "Update the ModuleFederationConfig import use @nx/module-federation.",
"factory": "./src/migrations/update-20-2-0/migrate-mf-imports-to-new-package"
}
},
"packageJsonUpdates": {

View File

@ -43,6 +43,7 @@
"@nx/js": "file:../js",
"@nx/eslint": "file:../eslint",
"@nx/web": "file:../web",
"@nx/module-federation": "file:../module-federation",
"express": "^4.19.2",
"http-proxy-middleware": "^3.0.3"
},

View File

@ -12,7 +12,10 @@ import { ModuleFederationDevServerOptions } from './schema';
import {
getModuleFederationConfig,
getRemotes,
} from '@nx/webpack/src/utils/module-federation';
startRemoteProxies,
parseStaticRemotesConfig,
type StaticRemotesConfig,
} from '@nx/module-federation/src/utils';
import {
combineAsyncIterables,
createAsyncIterable,
@ -20,11 +23,6 @@ import {
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
import { cpSync, existsSync } from 'fs';
import { extname, join } from 'path';
import { startRemoteProxies } from '@nx/webpack/src/utils/module-federation/start-remote-proxies';
import {
parseStaticRemotesConfig,
type StaticRemotesConfig,
} from '@nx/webpack/src/utils/module-federation/parse-static-remotes-config';
import { buildStaticRemotes } from '../../utils/build-static.remotes';
function getBuildOptions(buildTarget: string, context: ExecutorContext) {

View File

@ -12,7 +12,10 @@ import { extname, join } from 'path';
import {
getModuleFederationConfig,
getRemotes,
} from '@nx/webpack/src/utils/module-federation';
parseStaticSsrRemotesConfig,
type StaticRemotesConfig,
startSsrRemoteProxies,
} from '@nx/module-federation/src/utils';
import {
combineAsyncIterables,
@ -21,14 +24,8 @@ import {
import { fork } from 'child_process';
import { cpSync, createWriteStream, existsSync } from 'fs';
import {
parseStaticSsrRemotesConfig,
type StaticRemotesConfig,
} from '@nx/webpack/src/utils/module-federation/parse-static-remotes-config';
import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl';
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { startSsrRemoteProxies } from '@nx/webpack/src/utils/module-federation/start-ssr-remote-proxies';
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
type ModuleFederationSsrDevServerOptions = WebSsrDevServerOptions & {

View File

@ -13,11 +13,9 @@ import { cpSync, existsSync, readFileSync, rmSync } from 'fs';
import {
getModuleFederationConfig,
getRemotes,
} from '@nx/webpack/src/utils/module-federation';
import {
parseStaticRemotesConfig,
StaticRemotesConfig,
} from '@nx/webpack/src/utils/module-federation/parse-static-remotes-config';
} from '@nx/module-federation/src/utils';
import { buildStaticRemotes } from '../../utils/build-static.remotes';
import { fork } from 'child_process';
import type { WebpackExecutorOptions } from '@nx/webpack';

View File

@ -28,7 +28,7 @@ exports[`hostGenerator bundler=rspack should generate host files and configs for
"// @ts-check
/**
* @type {import('@nx/rspack/module-federation').ModuleFederationConfig}
* @type {import('@nx/module-federation').ModuleFederationConfig}
**/
const moduleFederationConfig = {
name: 'test',
@ -67,7 +67,7 @@ export default composePlugins(
`;
exports[`hostGenerator bundler=rspack should generate host files and configs for SSR when --typescriptConfiguration=true 2`] = `
"import { ModuleFederationConfig } from '@nx/rspack/module-federation';
"import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: 'test',
@ -130,7 +130,8 @@ module.exports = {
exports[`hostGenerator bundler=rspack should generate host files and configs when --typescriptConfiguration=true 1`] = `
"import {composePlugins, withNx, withReact} from '@nx/rspack';
import {withModuleFederation, ModuleFederationConfig} from '@nx/rspack/module-federation';
import { withModuleFederation } from '@nx/rspack/module-federation';
import { ModuleFederationConfig } from '@nx/module-federation';
import baseConfig from './module-federation.config';
@ -149,7 +150,7 @@ export default composePlugins(withNx(), withReact(), withModuleFederation(config
`;
exports[`hostGenerator bundler=rspack should generate host files and configs when --typescriptConfiguration=true 2`] = `
"import { ModuleFederationConfig } from '@nx/rspack/module-federation';
"import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: 'test',

View File

@ -29,7 +29,7 @@ exports[`hostGenerator bundler=webpack should generate host files and configs fo
"// @ts-check
/**
* @type {import('@nx/webpack').ModuleFederationConfig}
* @type {import('@nx/module-federation').ModuleFederationConfig}
**/
const moduleFederationConfig = {
name: 'test',
@ -69,7 +69,7 @@ export default composePlugins(
`;
exports[`hostGenerator bundler=webpack should generate host files and configs for SSR when --typescriptConfiguration=true 2`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
"import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: 'test',
@ -132,9 +132,10 @@ module.exports = {
`;
exports[`hostGenerator bundler=webpack should generate host files and configs when --typescriptConfiguration=true 1`] = `
"import {composePlugins, withNx, ModuleFederationConfig} from '@nx/webpack';
"import {composePlugins, withNx} from '@nx/webpack';
import {withReact} from '@nx/react';
import {withModuleFederation} from '@nx/react/module-federation';
import { ModuleFederationConfig } from '@nx/module-federation';
import baseConfig from './module-federation.config';
@ -153,7 +154,7 @@ export default composePlugins(withNx(), withReact(), withModuleFederation(config
`;
exports[`hostGenerator bundler=webpack should generate host files and configs when --typescriptConfiguration=true 2`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
"import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: 'test',

View File

@ -1,4 +1,4 @@
import { ModuleFederationConfig } from '@nx/rspack/module-federation';
import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: '<%= projectName %>',

View File

@ -1,7 +1,7 @@
// @ts-check
/**
* @type {import('@nx/rspack/module-federation').ModuleFederationConfig}
* @type {import('@nx/module-federation').ModuleFederationConfig}
**/
const moduleFederationConfig = {
name: '<%= projectName %>',

View File

@ -1,4 +1,4 @@
import { ModuleFederationConfig } from '@nx/rspack/module-federation';
import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: '<%= projectName %>',

View File

@ -1,5 +1,6 @@
import { composePlugins, withNx, withReact } from '@nx/rspack';
import { withModuleFederation, ModuleFederationConfig } from '@nx/rspack/module-federation';
import { withModuleFederation } from '@nx/rspack/module-federation';
import { ModuleFederationConfig } from '@nx/module-federation';
import baseConfig from './module-federation.config';

View File

@ -1,5 +1,6 @@
import {composePlugins, withNx, withReact} from '@nx/rspack';
import {withModuleFederation, ModuleFederationConfig} from '@nx/rspack/module-federation';
import { withModuleFederation } from '@nx/rspack/module-federation';
import { ModuleFederationConfig } from '@nx/module-federation';
import baseConfig from './module-federation.config';

View File

@ -1,4 +1,4 @@
import { ModuleFederationConfig } from '@nx/webpack';
import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: '<%= projectName %>',

View File

@ -1,7 +1,7 @@
// @ts-check
/**
* @type {import('@nx/webpack').ModuleFederationConfig}
* @type {import('@nx/module-federation').ModuleFederationConfig}
**/
const moduleFederationConfig = {
name: '<%= projectName %>',

View File

@ -1,4 +1,4 @@
import { ModuleFederationConfig } from '@nx/webpack';
import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: '<%= projectName %>',

View File

@ -1,7 +1,7 @@
import { composePlugins, withNx } from '@nx/webpack';
import { withReact } from '@nx/react';
import { withModuleFederation } from '@nx/react/module-federation';
import { ModuleFederationConfig } from '@nx/webpack';
import { ModuleFederationConfig } from '@nx/module-federation';
import baseConfig from './module-federation.config';

View File

@ -1,6 +1,7 @@
import {composePlugins, withNx, ModuleFederationConfig} from '@nx/webpack';
import {composePlugins, withNx} from '@nx/webpack';
import {withReact} from '@nx/react';
import {withModuleFederation} from '@nx/react/module-federation';
import { ModuleFederationConfig } from '@nx/module-federation';
import baseConfig from './module-federation.config';

View File

@ -100,7 +100,7 @@ exports[`remote generator bundler=rspack should create the remote with the corre
`;
exports[`remote generator bundler=rspack should create the remote with the correct config files when --typescriptConfiguration=true 3`] = `
"import { ModuleFederationConfig } from '@nx/rspack/module-federation';
"import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: 'test',
@ -174,7 +174,7 @@ export default composePlugins(
`;
exports[`remote generator bundler=rspack should generate correct remote with config files when using --ssr and --typescriptConfiguration=true 2`] = `
"import { ModuleFederationConfig } from '@nx/rspack/module-federation';
"import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: 'test',

View File

@ -103,7 +103,7 @@ exports[`remote generator bundler=webpack should create the remote with the corr
`;
exports[`remote generator bundler=webpack should create the remote with the correct config files when --typescriptConfiguration=true 3`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
"import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: 'test',
@ -179,7 +179,7 @@ export default composePlugins(
`;
exports[`remote generator bundler=webpack should generate correct remote with config files when using --ssr and --typescriptConfiguration=true 2`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
"import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: 'test',

View File

@ -1,4 +1,4 @@
import {ModuleFederationConfig} from '@nx/rspack/module-federation';
import {ModuleFederationConfig} from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: '<%= projectName %>',

View File

@ -1,4 +1,4 @@
import {ModuleFederationConfig} from '@nx/rspack/module-federation';
import {ModuleFederationConfig} from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: '<%= projectName %>',

View File

@ -1,4 +1,4 @@
import {ModuleFederationConfig} from '@nx/webpack';
import {ModuleFederationConfig} from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: '<%= projectName %>',

View File

@ -1,4 +1,4 @@
import {ModuleFederationConfig} from '@nx/webpack';
import {ModuleFederationConfig} from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: '<%= projectName %>',

View File

@ -0,0 +1,340 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import migrateMfImportsToNewPackage from './migrate-mf-imports-to-new-package';
jest.mock('@nx/devkit', () => {
const original = jest.requireActual('@nx/devkit');
return {
...original,
createProjectGraphAsync: jest.fn().mockResolvedValue(
Promise.resolve({
dependencies: {
shell: [
{
source: 'shell',
target: 'npm:@nx/webpack',
type: 'static',
},
{
source: 'shell',
target: 'npm:@nx/rspack',
type: 'static',
},
],
remote: [
{
source: 'remote',
target: 'npm:@nx/webpack',
type: 'static',
},
],
},
nodes: {
shell: {
name: 'shell',
type: 'app',
data: {
root: 'apps/shell',
sourceRoot: 'shell/src',
targets: {},
},
},
remote: {
name: 'remote',
type: 'app',
data: {
root: 'apps/remote',
sourceRoot: 'remote/src',
targets: {},
},
},
},
})
),
};
});
describe.each([
['webpack', '@nx/webpack'],
['rspack', '@nx/rspack/module-federation'],
])('migrate-mf-imports-to-new-package --bundler=%s', (bundler, importPath) => {
it('should update the ModuleFederationConfig import to change the import when its a single line', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write(
`apps/shell/${bundler}.config.js`,
`import { ModuleFederationConfig } from '${importPath}';`
);
tree.write(
'apps/shell/module-federation.config.ts',
`import { ModuleFederationConfig } from '${importPath}';`
);
tree.write(
`apps/remote/${bundler}.config.ts`,
`import { ModuleFederationConfig } from '${importPath}';`
);
tree.write(
'apps/remote/module-federation.config.js',
`import { ModuleFederationConfig } from '${importPath}';`
);
// ACT
await migrateMfImportsToNewPackage(tree);
// ASSERT
expect(tree.read(`apps/shell/${bundler}.config.js`, 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
expect(tree.read(`apps/remote/${bundler}.config.ts`, 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
expect(tree.read('apps/shell/module-federation.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
expect(tree.read('apps/remote/module-federation.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
});
it('should not update the ModuleFederationConfig import when its correct', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write(
`apps/shell/${bundler}.config.js`,
`import { ModuleFederationConfig } from '@nx/module-federation';`
);
tree.write(
'apps/shell/module-federation.config.ts',
`import { ModuleFederationConfig } from '@nx/module-federation';`
);
tree.write(
`apps/remote/${bundler}.config.ts`,
`import { ModuleFederationConfig } from '@nx/module-federation';`
);
tree.write(
'apps/remote/module-federation.config.js',
`import { ModuleFederationConfig } from '@nx/module-federation';`
);
// ACT
await migrateMfImportsToNewPackage(tree);
// ASSERT
expect(tree.read(`apps/shell/${bundler}.config.js`, 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
expect(tree.read(`apps/remote/${bundler}.config.ts`, 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
expect(tree.read('apps/shell/module-federation.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
expect(tree.read('apps/remote/module-federation.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
});
it('should update the ModuleFederationConfig import to change the import when its across multiple lines', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write(
`apps/shell/${bundler}.config.js`,
`import {
ModuleFederationConfig
} from '${importPath}';`
);
tree.write(
'apps/shell/module-federation.config.ts',
`import {
ModuleFederationConfig
} from '${importPath}';`
);
tree.write(
`apps/remote/${bundler}.config.ts`,
`import {
ModuleFederationConfig
} from '${importPath}';`
);
tree.write(
'apps/remote/module-federation.config.js',
`import {
ModuleFederationConfig
} from '${importPath}';`
);
// ACT
await migrateMfImportsToNewPackage(tree);
// ASSERT
expect(tree.read(`apps/shell/${bundler}.config.js`, 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
expect(tree.read(`apps/remote/${bundler}.config.ts`, 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
expect(tree.read('apps/shell/module-federation.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
expect(tree.read('apps/remote/module-federation.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
});
it('should update the ModuleFederationConfig import to change the import when its a part of multiple imports', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write(
`apps/shell/${bundler}.config.js`,
`import { something, ModuleFederationConfig } from '${importPath}';`
);
tree.write(
'apps/shell/module-federation.config.ts',
`import { ModuleFederationConfig, something} from '${importPath}';`
);
tree.write(
`apps/remote/${bundler}.config.ts`,
`import { something, ModuleFederationConfig } from '${importPath}';`
);
tree.write(
'apps/remote/module-federation.config.js',
`import { ModuleFederationConfig, something } from '${importPath}';`
);
// ACT
await migrateMfImportsToNewPackage(tree);
// ASSERT
expect(tree.read(`apps/shell/${bundler}.config.js`, 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
import { something } from '${importPath}';
"
`);
expect(tree.read(`apps/remote/${bundler}.config.ts`, 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
import { something } from '${importPath}';
"
`);
expect(tree.read('apps/shell/module-federation.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
import { something } from '${importPath}';
"
`);
expect(tree.read('apps/remote/module-federation.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
import { something } from '${importPath}';
"
`);
});
it('should update the ModuleFederationConfig import to change the import when its a part of multiple imports across multiple lines', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write(
`apps/shell/${bundler}.config.js`,
`import {
something,
ModuleFederationConfig
} from '${importPath}';`
);
tree.write(
'apps/shell/module-federation.config.ts',
`import {
ModuleFederationConfig,
something
} from '${importPath}';`
);
tree.write(
`apps/remote/${bundler}.config.ts`,
`import {
something,
ModuleFederationConfig
} from '${importPath}';`
);
tree.write(
'apps/remote/module-federation.config.js',
`import {
ModuleFederationConfig,
something
} from '${importPath}';`
);
// ACT
await migrateMfImportsToNewPackage(tree);
// ASSERT
expect(tree.read(`apps/shell/${bundler}.config.js`, 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
import { something } from '${importPath}';
"
`);
expect(tree.read(`apps/remote/${bundler}.config.ts`, 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
import { something } from '${importPath}';
"
`);
expect(tree.read('apps/shell/module-federation.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
import { something } from '${importPath}';
"
`);
expect(tree.read('apps/remote/module-federation.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ModuleFederationConfig } from '@nx/module-federation';
import { something } from '${importPath}';
"
`);
});
it('should update the correct import when there are multiple from the bundler', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write(
`apps/shell/${bundler}.config.js`,
`import {something} from '${importPath}';
import {
ModuleFederationConfig
} from '${importPath}';`
);
// ACT
await migrateMfImportsToNewPackage(tree);
// ASSERT
expect(tree.read(`apps/shell/${bundler}.config.js`, 'utf-8'))
.toMatchInlineSnapshot(`
"import { something } from '${importPath}';
import { ModuleFederationConfig } from '@nx/module-federation';
"
`);
});
});

View File

@ -0,0 +1,87 @@
import { createProjectGraphAsync, Tree } from '@nx/devkit';
import { formatFiles, visitNotIgnoredFiles } from '@nx/devkit';
import { tsquery } from '@phenomnomnominal/tsquery';
const MF_IMPORT_TO_UPDATE = 'ModuleFederationConfig';
const MF_CONFIG_IMPORT_SELECTOR = `ImportDeclaration:has(StringLiteral[value=@nx/webpack]):has(Identifier[name=ModuleFederationConfig]),ImportDeclaration:has(StringLiteral[value=@nx/rspack/module-federation]):has(Identifier[name=ModuleFederationConfig])`;
const IMPORT_TOKENS_SELECTOR = `ImportClause ImportSpecifier`;
const MF_CONFIG_IMPORT_SPECIFIER_SELECTOR = `ImportClause ImportSpecifier > Identifier[name=ModuleFederationConfig]`;
const WEBPACK_IMPORT_SELECTOR = `ImportDeclaration > StringLiteral[value=@nx/webpack]`;
const RSPACK_IMPORT_SELECTOR = `ImportDeclaration > StringLiteral[value=@nx/rspack/module-federation]`;
export default async function migrateMfImportsToNewPackage(tree: Tree) {
const rootsToCheck = new Set<string>();
const graph = await createProjectGraphAsync();
for (const [project, dependencies] of Object.entries(graph.dependencies)) {
const usesNxWebpackOrRspack = dependencies.some(
(dep) =>
dep.target === 'npm:@nx/webpack' || dep.target === 'npm:@nx/rspack'
);
if (usesNxWebpackOrRspack) {
const root = graph.nodes[project].data.root;
rootsToCheck.add(root);
}
}
for (const root of rootsToCheck) {
visitNotIgnoredFiles(tree, root, (filePath) => {
if (!filePath.endsWith('.ts') && !filePath.endsWith('.js')) {
return;
}
let contents = tree.read(filePath, 'utf-8');
if (!contents.includes(MF_IMPORT_TO_UPDATE)) {
return;
}
const ast = tsquery.ast(contents);
const importNodes = tsquery(ast, MF_CONFIG_IMPORT_SELECTOR);
if (importNodes.length === 0) {
return;
}
const importNode = importNodes[0];
const importSpecifiers = tsquery(importNode, IMPORT_TOKENS_SELECTOR);
if (importSpecifiers.length > 1) {
const mfConfigImportSpecifierNode = tsquery(
importNode,
MF_CONFIG_IMPORT_SPECIFIER_SELECTOR
)[0];
const end =
contents.charAt(mfConfigImportSpecifierNode.getEnd()) === ','
? mfConfigImportSpecifierNode.getEnd() + 1
: mfConfigImportSpecifierNode.getEnd();
contents = `import { ${MF_IMPORT_TO_UPDATE} } from '@nx/module-federation';
${contents.slice(
0,
mfConfigImportSpecifierNode.getStart()
)}${contents.slice(end)}`;
} else {
const nxWebpackImportStringNodes = tsquery(
importNode,
WEBPACK_IMPORT_SELECTOR
);
const nxRspackImportStringNodes = tsquery(
importNode,
RSPACK_IMPORT_SELECTOR
);
if (
nxWebpackImportStringNodes.length === 0 &&
nxRspackImportStringNodes.length === 0
) {
return;
}
const bundlerImportStringNode = nxWebpackImportStringNodes.length
? nxWebpackImportStringNodes[0]
: nxRspackImportStringNodes[0];
contents = `${contents.slice(
0,
bundlerImportStringNode.getStart()
)}'@nx/module-federation'${contents.slice(
bundlerImportStringNode.getEnd()
)}`;
}
tree.write(filePath, contents);
});
}
await formatFiles(tree);
}

View File

@ -1,23 +0,0 @@
import { ExecutorContext } from '@nx/devkit';
import { join } from 'path';
import { ModuleFederationConfig } from './models';
export function loadModuleFederationConfigFromContext(
context: ExecutorContext
): ModuleFederationConfig {
const p = context.projectsConfigurations.projects[context.projectName];
const moduleFederationConfigPath = join(
context.root,
p.root,
'module-federation.config.js'
);
try {
return require(moduleFederationConfigPath) as ModuleFederationConfig;
} catch {
// TODO(jack): Add a link to guide
throw new Error(
`Could not load ${moduleFederationConfigPath}. Was this project generated with "@nx/react:host"?`
);
}
}

View File

@ -1,30 +0,0 @@
export type ModuleFederationLibrary = { type: string; name: string };
export type Remotes = string[] | [remoteName: string, remoteUrl: string][];
export interface SharedLibraryConfig {
singleton?: boolean;
strictVersion?: boolean;
requiredVersion?: false | string;
eager?: boolean;
}
export type SharedFunction = (
libraryName: string,
sharedConfig: SharedLibraryConfig
) => undefined | false | SharedLibraryConfig;
export type AdditionalSharedConfig = Array<
| string
| [libraryName: string, sharedConfig: SharedLibraryConfig]
| { libraryName: string; sharedConfig: SharedLibraryConfig }
>;
export interface ModuleFederationConfig {
name: string;
remotes?: Remotes;
library?: ModuleFederationLibrary;
exposes?: Record<string, string>;
shared?: SharedFunction;
additionalShared?: AdditionalSharedConfig;
}

View File

@ -1,16 +0,0 @@
import { joinPathFragments, readJsonFile, workspaceRoot } from '@nx/devkit';
import { existsSync } from 'fs';
export function readRootPackageJson(): {
dependencies?: { [key: string]: string };
devDependencies?: { [key: string]: string };
} {
const pkgJsonPath = joinPathFragments(workspaceRoot, 'package.json');
if (!existsSync(pkgJsonPath)) {
throw new Error(
'NX MFE: Could not find root package.json to determine dependency versions.'
);
}
return readJsonFile(pkgJsonPath);
}

View File

@ -7,7 +7,7 @@ import {
ModuleFederationConfig,
sharePackages,
shareWorkspaceLibraries,
} from '@nx/webpack/src/utils/module-federation';
} from '@nx/module-federation';
import {
createProjectGraphAsync,

View File

@ -1,8 +1,9 @@
import {
ModuleFederationConfig,
NxModuleFederationConfigOverride,
} from '@nx/webpack/src/utils/module-federation';
} from '@nx/module-federation';
import { getModuleFederationConfig } from './utils';
import type { NormalModuleReplacementPlugin } from 'webpack';
export async function withModuleFederationForSSR(
options: ModuleFederationConfig,
@ -45,7 +46,7 @@ export async function withModuleFederationForSSR(
? [
...(configOverride?.runtimePlugins ?? []),
require.resolve(
'@nx/webpack/src/utils/module-federation/plugins/runtime-library-control.plugin.js'
'@nx/module-federation/src/utils/plugins/runtime-library-control.plugin.js'
),
]
: [
@ -56,7 +57,7 @@ export async function withModuleFederationForSSR(
},
{}
),
sharedLibraries.getReplacementPlugin()
sharedLibraries.getReplacementPlugin() as NormalModuleReplacementPlugin
);
// The env var is only set from the module-federation-dev-server

View File

@ -1,10 +1,11 @@
import {
ModuleFederationConfig,
NxModuleFederationConfigOverride,
} from '@nx/webpack/src/utils/module-federation';
} from '@nx/module-federation';
import { getModuleFederationConfig } from './utils';
import type { AsyncNxComposableWebpackPlugin } from '@nx/webpack';
import { ModuleFederationPlugin } from '@module-federation/enhanced/webpack';
import type { NormalModuleReplacementPlugin } from 'webpack';
/**
* @param {ModuleFederationConfig} options
@ -65,13 +66,13 @@ export async function withModuleFederation(
? [
...(configOverride?.runtimePlugins ?? []),
require.resolve(
'@nx/webpack/src/utils/module-federation/plugins/runtime-library-control.plugin.js'
'@nx/module-federation/src/utils/plugins/runtime-library-control.plugin.js'
),
]
: configOverride?.runtimePlugins,
virtualRuntimeEntry: true,
}),
sharedLibraries.getReplacementPlugin()
sharedLibraries.getReplacementPlugin() as NormalModuleReplacementPlugin
);
// The env var is only set from the module-federation-dev-server

View File

@ -27,6 +27,7 @@
"@nx/js": "file:../js",
"@nx/devkit": "file:../devkit",
"@nx/web": "file:../web",
"@nx/module-federation": "file:../module-federation",
"@phenomnomnominal/tsquery": "~5.0.1",
"@rspack/core": "^1.0.4",
"@rspack/dev-server": "^1.0.4",

View File

@ -17,13 +17,11 @@ import { extname, join } from 'path';
import {
getModuleFederationConfig,
getRemotes,
} from '../../utils/module-federation';
import { buildStaticRemotes } from '../../utils/module-federation/build-static.remotes';
import {
parseStaticRemotesConfig,
type StaticRemotesConfig,
} from '../../utils/module-federation/parse-static-remotes-config';
import { startRemoteProxies } from '../../utils/module-federation/start-remote-proxies';
startRemoteProxies,
} from '@nx/module-federation/src/utils';
import { buildStaticRemotes } from '../../utils/module-federation/build-static.remotes';
import devServerExecutor from '../dev-server/dev-server.impl';
import { ModuleFederationDevServerOptions } from './schema';

View File

@ -10,7 +10,10 @@ import { extname, join } from 'path';
import {
getModuleFederationConfig,
getRemotes,
} from '../../utils/module-federation';
parseStaticSsrRemotesConfig,
type StaticRemotesConfig,
startSsrRemoteProxies,
} from '@nx/module-federation/src/utils';
import { RspackSsrDevServerOptions } from '../ssr-dev-server/schema';
import ssrDevServerExecutor from '../ssr-dev-server/ssr-dev-server.impl';
@ -21,15 +24,9 @@ import {
import { fork } from 'child_process';
import { cpSync, createWriteStream, existsSync } from 'fs';
import {
parseStaticSsrRemotesConfig,
type StaticRemotesConfig,
} from '../../utils/module-federation/parse-static-remotes-config';
import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl';
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { startSsrRemoteProxies } from '../../utils/module-federation/start-ssr-remote-proxies';
type ModuleFederationSsrDevServerOptions = RspackSsrDevServerOptions & {
devRemotes?: (

View File

@ -19,12 +19,10 @@ import { basename, extname, join } from 'path';
import {
getModuleFederationConfig,
getRemotes,
} from '../../utils/module-federation';
import { buildStaticRemotes } from '../../utils/module-federation/build-static.remotes';
import {
parseStaticRemotesConfig,
StaticRemotesConfig,
} from '../../utils/module-federation/parse-static-remotes-config';
} from '@nx/module-federation/src/utils';
import { buildStaticRemotes } from '../../utils/module-federation/build-static.remotes';
import { ModuleFederationDevServerOptions } from '../module-federation-dev-server/schema';
import type { RspackExecutorSchema } from '../rspack/schema';
import { ModuleFederationStaticServerSchema } from './schema';

View File

@ -141,11 +141,12 @@ describe('Convert webpack', () => {
expect(tree.exists('demo/rspack.config.ts')).toBeTruthy();
expect(tree.read('demo/rspack.config.ts', 'utf-8')).toMatchInlineSnapshot(`
"import { withModuleFederation } from '@nx/rspack/module-federation';
import { ModuleFederationConfig } from '@nx/rspack/module-federation';
import { withReact } from '@nx/rspack';
import { withNx } from '@nx/rspack';
import { composePlugins } from '@nx/rspack';
import { ModuleFederationConfig } from '@nx/module-federation';
import baseConfig from './module-federation.config';
const config: ModuleFederationConfig = {

View File

@ -5,7 +5,7 @@ import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { logger } from 'nx/src/utils/logger';
import { join } from 'path';
import { ModuleFederationDevServerOptions } from '../../executors/module-federation-dev-server/schema';
import type { StaticRemotesConfig } from './parse-static-remotes-config';
import type { StaticRemotesConfig } from '@nx/module-federation/src/utils';
export async function buildStaticRemotes(
staticRemotesConfig: StaticRemotesConfig,

View File

@ -1,195 +0,0 @@
import { logger, type ProjectGraph } from '@nx/devkit';
import { registerTsProject } from '@nx/js/src/internal';
import chalk from 'chalk';
import { existsSync, readFileSync } from 'fs';
import { findMatchingProjects } from 'nx/src/utils/find-matching-projects';
import { join } from 'path';
import { ModuleFederationConfig } from './models';
interface ModuleFederationExecutorContext {
projectName: string;
projectGraph: ProjectGraph;
root: string;
}
function extractRemoteProjectsFromConfig(
config: ModuleFederationConfig,
pathToManifestFile?: string
) {
const remotes = [];
const dynamicRemotes = [];
if (pathToManifestFile && existsSync(pathToManifestFile)) {
const moduleFederationManifestJson = readFileSync(
pathToManifestFile,
'utf-8'
);
if (moduleFederationManifestJson) {
// This should have shape of
// {
// "remoteName": "remoteLocation",
// }
const parsedManifest = JSON.parse(moduleFederationManifestJson);
if (
Object.keys(parsedManifest).every(
(key) =>
typeof key === 'string' && typeof parsedManifest[key] === 'string'
)
) {
dynamicRemotes.push(...Object.keys(parsedManifest));
}
}
}
const staticRemotes =
config.remotes?.map((r) => (Array.isArray(r) ? r[0] : r)) ?? [];
remotes.push(...staticRemotes);
return { remotes, dynamicRemotes };
}
function collectRemoteProjects(
remote: string,
collected: Set<string>,
context: ModuleFederationExecutorContext
) {
const remoteProject = context.projectGraph.nodes[remote]?.data;
if (!context.projectGraph.nodes[remote] || collected.has(remote)) {
return;
}
collected.add(remote);
const remoteProjectRoot = remoteProject.root;
const remoteProjectTsConfig = remoteProject.targets['build'].options.tsConfig;
const remoteProjectConfig = getModuleFederationConfig(
remoteProjectTsConfig,
context.root,
remoteProjectRoot
);
const { remotes: remoteProjectRemotes } =
extractRemoteProjectsFromConfig(remoteProjectConfig);
remoteProjectRemotes.forEach((r) =>
collectRemoteProjects(r, collected, context)
);
}
export function getRemotes(
devRemotes: string[],
skipRemotes: string[],
config: ModuleFederationConfig,
context: ModuleFederationExecutorContext,
pathToManifestFile?: string
) {
const collectedRemotes = new Set<string>();
const { remotes, dynamicRemotes } = extractRemoteProjectsFromConfig(
config,
pathToManifestFile
);
remotes.forEach((r) => collectRemoteProjects(r, collectedRemotes, context));
const remotesToSkip = new Set(
findMatchingProjects(skipRemotes, context.projectGraph.nodes) ?? []
);
if (remotesToSkip.size > 0) {
logger.info(
`Remotes not served automatically: ${[...remotesToSkip.values()].join(
', '
)}`
);
}
const knownRemotes = Array.from(collectedRemotes).filter(
(r) => !remotesToSkip.has(r)
);
const knownDynamicRemotes = dynamicRemotes.filter(
(r) => !remotesToSkip.has(r) && context.projectGraph.nodes[r]
);
logger.info(
`NX Starting module federation dev-server for ${chalk.bold(
context.projectName
)} with ${[...knownRemotes, ...knownDynamicRemotes].length} remotes`
);
const devServeApps = new Set(
!devRemotes
? []
: Array.isArray(devRemotes)
? findMatchingProjects(devRemotes, context.projectGraph.nodes)
: findMatchingProjects([devRemotes], context.projectGraph.nodes)
);
const staticRemotes = knownRemotes.filter((r) => !devServeApps.has(r));
const devServeRemotes = [...knownRemotes, ...knownDynamicRemotes].filter(
(r) => devServeApps.has(r)
);
const staticDynamicRemotes = knownDynamicRemotes.filter(
(r) => !devServeApps.has(r)
);
const remotePorts = [...devServeRemotes, ...staticDynamicRemotes].map(
(r) => context.projectGraph.nodes[r].data.targets['serve'].options.port
);
const staticRemotePort =
Math.max(
...([
...remotePorts,
...staticRemotes.map(
(r) =>
context.projectGraph.nodes[r].data.targets['serve'].options.port
),
] as number[])
) +
(remotesToSkip.size + 1);
return {
staticRemotes,
devRemotes: devServeRemotes,
dynamicRemotes: staticDynamicRemotes,
remotePorts,
staticRemotePort,
};
}
export function getModuleFederationConfig(
tsconfigPath: string,
workspaceRoot: string,
projectRoot: string,
pluginName: 'react' | 'angular' = 'react'
) {
const moduleFederationConfigPathJS = join(
workspaceRoot,
projectRoot,
'module-federation.config.js'
);
const moduleFederationConfigPathTS = join(
workspaceRoot,
projectRoot,
'module-federation.config.ts'
);
let moduleFederationConfigPath = moduleFederationConfigPathJS;
// create a no-op so this can be called with issue
const fullTSconfigPath = tsconfigPath.startsWith(workspaceRoot)
? tsconfigPath
: join(workspaceRoot, tsconfigPath);
let cleanupTranspiler = () => undefined;
if (existsSync(moduleFederationConfigPathTS)) {
cleanupTranspiler = registerTsProject(fullTSconfigPath);
moduleFederationConfigPath = moduleFederationConfigPathTS;
}
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const config = require(moduleFederationConfigPath);
cleanupTranspiler();
return config.default || config;
} catch {
throw new Error(
`Could not load ${moduleFederationConfigPath}. Was this project generated with "@nx/${pluginName}:host"?\nSee: https://nx.dev/concepts/more-concepts/faster-builds-with-module-federation`
);
}
}

View File

@ -1,51 +1,4 @@
import {
AdditionalSharedConfig,
ModuleFederationConfig,
ModuleFederationLibrary,
Remotes,
SharedFunction,
SharedLibraryConfig,
SharedWorkspaceLibraryConfig,
WorkspaceLibrary,
WorkspaceLibrarySecondaryEntryPoint,
} from './models';
import {
applyAdditionalShared,
applySharedFunction,
getNpmPackageSharedConfig,
sharePackages,
shareWorkspaceLibraries,
} from './share';
import { mapRemotes, mapRemotesForSSR } from './remotes';
import { getDependentPackagesForProject } from './dependencies';
import { readRootPackageJson } from './package-json';
import { withModuleFederation } from './with-module-federation/with-module-federation';
import { withModuleFederationForSSR } from './with-module-federation/with-module-federation-ssr';
export {
AdditionalSharedConfig,
applyAdditionalShared,
applySharedFunction,
getDependentPackagesForProject,
getNpmPackageSharedConfig,
mapRemotes,
mapRemotesForSSR,
ModuleFederationConfig,
ModuleFederationLibrary,
readRootPackageJson,
Remotes,
SharedFunction,
SharedLibraryConfig,
SharedWorkspaceLibraryConfig,
sharePackages,
shareWorkspaceLibraries,
withModuleFederation,
withModuleFederationForSSR,
WorkspaceLibrary,
WorkspaceLibrarySecondaryEntryPoint,
};
export { withModuleFederation, withModuleFederationForSSR };

View File

@ -1,119 +0,0 @@
import { extname } from 'path';
import { Remotes } from './models';
/**
* Map remote names to a format that can be understood and used by Module
* Federation.
*
* @param remotes - The remotes to map
* @param remoteEntryExt - The file extension of the remoteEntry file
* @param determineRemoteUrl - The function used to lookup the URL of the served remote
*/
export function mapRemotes(
remotes: Remotes,
remoteEntryExt: 'js' | 'mjs',
determineRemoteUrl: (remote: string) => string
): Record<string, string> {
const mappedRemotes = {};
for (const nxRemoteProjectName of remotes) {
if (Array.isArray(nxRemoteProjectName)) {
const mfRemoteName = normalizeRemoteName(nxRemoteProjectName[0]);
mappedRemotes[mfRemoteName] = handleArrayRemote(
nxRemoteProjectName,
remoteEntryExt
);
} else if (typeof nxRemoteProjectName === 'string') {
const mfRemoteName = normalizeRemoteName(nxRemoteProjectName);
mappedRemotes[mfRemoteName] = handleStringRemote(
nxRemoteProjectName,
determineRemoteUrl
);
}
}
return mappedRemotes;
}
// Helper function to deal with remotes that are arrays
function handleArrayRemote(
remote: [string, string],
remoteEntryExt: 'js' | 'mjs'
): string {
let [nxRemoteProjectName, remoteLocation] = remote;
const mfRemoteName = normalizeRemoteName(nxRemoteProjectName);
const remoteLocationExt = extname(remoteLocation);
// If remote location already has .js or .mjs extension
if (['.js', '.mjs', '.json'].includes(remoteLocationExt)) {
return remoteLocation;
}
const baseRemote = remoteLocation.endsWith('/')
? remoteLocation.slice(0, -1)
: remoteLocation;
const globalPrefix = `${normalizeRemoteName(mfRemoteName)}@`;
// if the remote is defined with anything other than http then we assume it's a promise based remote
// In that case we should use what the user provides as the remote location
if (!remoteLocation.startsWith('promise new Promise')) {
return `${globalPrefix}${baseRemote}/remoteEntry.${remoteEntryExt}`;
} else {
return remoteLocation;
}
}
// Helper function to deal with remotes that are strings
function handleStringRemote(
nxRemoteProjectName: string,
determineRemoteUrl: (nxRemoteProjectName: string) => string
): string {
const globalPrefix = `${normalizeRemoteName(nxRemoteProjectName)}@`;
return `${globalPrefix}${determineRemoteUrl(nxRemoteProjectName)}`;
}
/**
* Map remote names to a format that can be understood and used by Module
* Federation.
*
* @param remotes - The remotes to map
* @param remoteEntryExt - The file extension of the remoteEntry file
* @param determineRemoteUrl - The function used to lookup the URL of the served remote
*/
export function mapRemotesForSSR(
remotes: Remotes,
remoteEntryExt: 'js' | 'mjs',
determineRemoteUrl: (remote: string) => string
): Record<string, string> {
const mappedRemotes = {};
for (const remote of remotes) {
if (Array.isArray(remote)) {
let [nxRemoteProjectName, remoteLocation] = remote;
const mfRemoteName = normalizeRemoteName(nxRemoteProjectName);
const remoteLocationExt = extname(remoteLocation);
mappedRemotes[mfRemoteName] = `${mfRemoteName}@${
['.js', '.mjs'].includes(remoteLocationExt)
? remoteLocation
: `${
remoteLocation.endsWith('/')
? remoteLocation.slice(0, -1)
: remoteLocation
}/remoteEntry.${remoteEntryExt}`
}`;
} else if (typeof remote === 'string') {
const mfRemoteName = normalizeRemoteName(remote);
mappedRemotes[mfRemoteName] = `${mfRemoteName}@${determineRemoteUrl(
remote
)}`;
}
}
return mappedRemotes;
}
function normalizeRemoteName(nxRemoteProjectName: string): string {
return nxRemoteProjectName.replace(/-/g, '_');
}

View File

@ -1,144 +0,0 @@
import { joinPathFragments, readJsonFile, workspaceRoot } from '@nx/devkit';
import { existsSync, lstatSync, readdirSync } from 'fs';
import { PackageJson, readModulePackageJson } from 'nx/src/utils/package-json';
import { dirname, join, relative } from 'path';
import type { WorkspaceLibrary } from './models';
import { WorkspaceLibrarySecondaryEntryPoint } from './models';
export function collectWorkspaceLibrarySecondaryEntryPoints(
library: WorkspaceLibrary,
tsconfigPathAliases: Record<string, string[]>
): WorkspaceLibrarySecondaryEntryPoint[] {
const libraryRoot = join(workspaceRoot, library.root);
const needsSecondaryEntryPointsCollected = existsSync(
join(libraryRoot, 'ng-package.json')
);
const secondaryEntryPoints: WorkspaceLibrarySecondaryEntryPoint[] = [];
if (needsSecondaryEntryPointsCollected) {
const tsConfigAliasesForLibWithSecondaryEntryPoints = Object.entries(
tsconfigPathAliases
).reduce((acc, [tsKey, tsPaths]) => {
if (!tsKey.startsWith(library.importKey)) {
return { ...acc };
}
if (tsPaths.some((path) => path.startsWith(`${library.root}/`))) {
acc = { ...acc, [tsKey]: tsPaths };
}
return acc;
}, {});
for (const [alias] of Object.entries(
tsConfigAliasesForLibWithSecondaryEntryPoints
)) {
const pathToLib = dirname(
join(workspaceRoot, tsconfigPathAliases[alias][0])
);
let searchDir = pathToLib;
while (searchDir !== libraryRoot) {
if (existsSync(join(searchDir, 'ng-package.json'))) {
secondaryEntryPoints.push({ name: alias, path: pathToLib });
break;
}
searchDir = dirname(searchDir);
}
}
}
return secondaryEntryPoints;
}
export function getNonNodeModulesSubDirs(directory: string): string[] {
return readdirSync(directory)
.filter((file) => file !== 'node_modules')
.map((file) => join(directory, file))
.filter((file) => lstatSync(file).isDirectory());
}
export function recursivelyCollectSecondaryEntryPointsFromDirectory(
pkgName: string,
pkgVersion: string,
pkgRoot: string,
mainEntryPointExports: any | undefined,
directories: string[],
collectedPackages: { name: string; version: string }[]
): void {
for (const directory of directories) {
const packageJsonPath = join(directory, 'package.json');
const relativeEntryPointPath = relative(pkgRoot, directory);
const entryPointName = joinPathFragments(pkgName, relativeEntryPointPath);
if (existsSync(packageJsonPath)) {
try {
// require the secondary entry point to try to rule out sample code
require.resolve(entryPointName, { paths: [workspaceRoot] });
const { name } = readJsonFile(packageJsonPath);
// further check to make sure what we were able to require is the
// same as the package name
if (name === entryPointName) {
collectedPackages.push({ name, version: pkgVersion });
}
} catch {
// do nothing
}
} else if (mainEntryPointExports) {
// if the package.json doesn't exist, check if the directory is
// exported by the main entry point
const entryPointExportKey = `./${relativeEntryPointPath}`;
const entryPointInfo = mainEntryPointExports[entryPointExportKey];
if (entryPointInfo) {
collectedPackages.push({
name: entryPointName,
version: pkgVersion,
});
}
}
const subDirs = getNonNodeModulesSubDirs(directory);
recursivelyCollectSecondaryEntryPointsFromDirectory(
pkgName,
pkgVersion,
pkgRoot,
mainEntryPointExports,
subDirs,
collectedPackages
);
}
}
export function collectPackageSecondaryEntryPoints(
pkgName: string,
pkgVersion: string,
collectedPackages: { name: string; version: string }[]
): void {
let pathToPackage: string;
let packageJsonPath: string;
let packageJson: PackageJson;
try {
({ path: packageJsonPath, packageJson } = readModulePackageJson(pkgName));
pathToPackage = dirname(packageJsonPath);
} catch {
// the package.json might not resolve if the package has the "exports"
// entry and is not exporting the package.json file, fall back to trying
// to find it from the top-level node_modules
pathToPackage = join(workspaceRoot, 'node_modules', pkgName);
packageJsonPath = join(pathToPackage, 'package.json');
if (!existsSync(packageJsonPath)) {
// might not exist if it's nested in another package, just return here
return;
}
packageJson = readJsonFile(packageJsonPath);
}
const { exports } = packageJson;
const subDirs = getNonNodeModulesSubDirs(pathToPackage);
recursivelyCollectSecondaryEntryPointsFromDirectory(
pkgName,
pkgVersion,
pathToPackage,
exports,
subDirs,
collectedPackages
);
}

View File

@ -1,381 +0,0 @@
import * as fs from 'fs';
import * as nxFileutils from 'nx/src/devkit-exports';
import { sharePackages, shareWorkspaceLibraries } from './share';
import * as tsUtils from './typescript';
jest.mock('nx/src/devkit-exports', () => {
return {
...jest.requireActual('nx/src/devkit-exports'),
readJsonFile: jest.fn(),
};
});
describe('MF Share Utils', () => {
afterEach(() => jest.clearAllMocks());
describe('ShareWorkspaceLibraries', () => {
it('should error when the tsconfig file does not exist', () => {
// ARRANGE
jest
.spyOn(fs, 'existsSync')
.mockImplementation((p: string) => p?.endsWith('.node'));
// ACT
try {
shareWorkspaceLibraries([
{ name: 'shared', root: 'libs/shared', importKey: '@myorg/shared' },
]);
} catch (error) {
// ASSERT
expect(error.message).toContain(
'NX MF: TsConfig Path for workspace libraries does not exist!'
);
}
});
it('should create an object with correct setup', () => {
// ARRANGE
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
'@myorg/shared': ['/libs/shared/src/index.ts'],
});
// ACT
const sharedLibraries = shareWorkspaceLibraries([
{ name: 'shared', root: 'libs/shared', importKey: '@myorg/shared' },
]);
// ASSERT
expect(sharedLibraries.getAliases()).toHaveProperty('@myorg/shared');
expect(sharedLibraries.getAliases()['@myorg/shared']).toContain(
'libs/shared/src/index.ts'
);
expect(sharedLibraries.getLibraries('libs/shared')).toEqual({
'@myorg/shared': {
eager: undefined,
requiredVersion: false,
},
});
});
it('should order nested projects first', () => {
// ARRANGE
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
'@myorg/shared': ['/libs/shared/src/index.ts'],
'@myorg/shared/components': ['/libs/shared/components/src/index.ts'],
});
// ACT
const sharedLibraries = shareWorkspaceLibraries([
{ name: 'shared', root: 'libs/shared', importKey: '@myorg/shared' },
{
name: 'shared-components',
root: 'libs/shared/components',
importKey: '@myorg/shared/components',
},
]);
// ASSERT
expect(Object.keys(sharedLibraries.getAliases())[0]).toEqual(
'@myorg/shared/components'
);
});
it('should handle path mappings with wildcards correctly in non-buildable libraries', () => {
// ARRANGE
jest.spyOn(fs, 'existsSync').mockImplementation(() => true);
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
'@myorg/shared': ['/libs/shared/src/index.ts'],
'@myorg/shared/*': ['/libs/shared/src/lib/*'],
});
// ACT
const sharedLibraries = shareWorkspaceLibraries([
{ name: 'shared', root: 'libs/shared', importKey: '@myorg/shared' },
]);
// ASSERT
expect(sharedLibraries.getAliases()).toHaveProperty('@myorg/shared');
expect(sharedLibraries.getAliases()['@myorg/shared']).toContain(
'libs/shared/src/index.ts'
);
expect(sharedLibraries.getLibraries('libs/shared')).toEqual({
'@myorg/shared': {
eager: undefined,
requiredVersion: false,
},
});
});
it('should create an object with empty setup when tsconfig does not contain the shared lib', () => {
// ARRANGE
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({});
// ACT
const sharedLibraries = shareWorkspaceLibraries([
{ name: 'shared', root: 'libs/shared', importKey: '@myorg/shared' },
]);
// ASSERT
expect(sharedLibraries.getAliases()).toEqual({});
expect(sharedLibraries.getLibraries('libs/shared')).toEqual({});
});
});
describe('SharePackages', () => {
it('should throw when it cannot find root package.json', () => {
// ARRANGE
jest
.spyOn(fs, 'existsSync')
.mockImplementation((p: string) => p.endsWith('.node'));
// ACT
try {
sharePackages(['@angular/core']);
} catch (error) {
// ASSERT
expect(error.message).toEqual(
'NX MF: Could not find root package.json to determine dependency versions.'
);
}
});
it('should correctly map the shared packages to objects', () => {
// ARRANGE
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
jest.spyOn(nxFileutils, 'readJsonFile').mockImplementation((file) => ({
name: file.replace(/\\/g, '/').replace(/^.*node_modules[/]/, ''),
dependencies: {
'@angular/core': '~13.2.0',
'@angular/common': '~13.2.0',
rxjs: '~7.4.0',
},
}));
jest.spyOn(fs, 'readdirSync').mockReturnValue([]);
// ACT
const packages = sharePackages([
'@angular/core',
'@angular/common',
'rxjs',
]);
// ASSERT
expect(packages).toEqual({
'@angular/core': {
singleton: true,
strictVersion: true,
requiredVersion: '~13.2.0',
},
'@angular/common': {
singleton: true,
strictVersion: true,
requiredVersion: '~13.2.0',
},
rxjs: {
singleton: true,
strictVersion: true,
requiredVersion: '~7.4.0',
},
});
});
// TODO: Get with colum and figure out why this stopped working
xit('should correctly map the shared packages to objects even with nested entry points', () => {
// ARRANGE
/**
* This creates a bunch of mocks that aims to test that
* the sharePackages function can handle nested
* entrypoints in the package that is being shared.
*
* This will set up a directory structure that matches
* the following:
*
* - @angular/core/
* - package.json
* - @angular/common/
* - http/
* - testing/
* - package.json
* - package.json
* - rxjs
* - package.json
*
* The result is that there would be 4 packages that
* need to be shared, as determined by the folders
* containing the package.json files
*/
createMockedFSForNestedEntryPoints();
// ACT
const packages = sharePackages([
'@angular/core',
'@angular/common',
'rxjs',
]);
// ASSERT
expect(packages).toEqual({
'@angular/core': {
singleton: true,
strictVersion: true,
requiredVersion: '~13.2.0',
},
'@angular/common': {
singleton: true,
strictVersion: true,
requiredVersion: '~13.2.0',
},
'@angular/common/http/testing': {
singleton: true,
strictVersion: true,
requiredVersion: '~13.2.0',
},
rxjs: {
singleton: true,
strictVersion: true,
requiredVersion: '~7.4.0',
},
});
});
it('should not throw when the main entry point package.json cannot be required', () => {
// ARRANGE
jest
.spyOn(fs, 'existsSync')
.mockImplementation(
(file: string) =>
!file.endsWith('non-existent-top-level-package/package.json')
);
jest.spyOn(nxFileutils, 'readJsonFile').mockImplementation((file) => {
return {
name: file
.replace(/\\/g, '/')
.replace(/^.*node_modules[/]/, '')
.replace('/package.json', ''),
dependencies: { '@angular/core': '~13.2.0' },
};
});
// ACT & ASSERT
expect(() =>
sharePackages(['non-existent-top-level-package'])
).not.toThrow();
});
});
it('should using shared library version from root package.json if available', () => {
// ARRANGE
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
jest
.spyOn(nxFileutils, 'readJsonFile')
.mockImplementation((file: string) => {
if (file.endsWith('package.json')) {
return {
dependencies: {
'@myorg/shared': '1.0.0',
},
};
}
});
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
'@myorg/shared': ['/libs/shared/src/index.ts'],
'@myorg/shared/*': ['/libs/shared/src/lib/*'],
});
// ACT
const sharedLibraries = shareWorkspaceLibraries(
[{ name: 'shared', root: 'libs/shared', importKey: '@myorg/shared' }],
'/'
);
// ASSERT
expect(sharedLibraries.getLibraries('libs/shared')).toEqual({
'@myorg/shared': {
eager: undefined,
requiredVersion: '1.0.0',
singleton: true,
},
});
});
it('should use shared library version from library package.json if project package.json does not have it', () => {
// ARRANGE
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
jest
.spyOn(nxFileutils, 'readJsonFile')
.mockImplementation((file: string) => {
if (file.endsWith('libs/shared/package.json')) {
return {
version: '1.0.0',
};
} else {
return {};
}
});
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
'@myorg/shared': ['/libs/shared/src/index.ts'],
'@myorg/shared/*': ['/libs/shared/src/lib/*'],
});
// ACT
const sharedLibraries = shareWorkspaceLibraries(
[{ name: 'shared', root: 'libs/shared', importKey: '@myorg/shared' }],
null
);
// ASSERT
expect(sharedLibraries.getLibraries('libs/shared')).toEqual({
'@myorg/shared': {
eager: undefined,
requiredVersion: '1.0.0',
singleton: true,
},
});
});
});
function createMockedFSForNestedEntryPoints() {
jest.spyOn(fs, 'existsSync').mockImplementation((file: string) => {
if (file.endsWith('http/package.json')) {
return false;
} else {
return true;
}
});
jest.spyOn(nxFileutils, 'readJsonFile').mockImplementation((file) => ({
name: file
.replace(/\\/g, '/')
.replace(/^.*node_modules[/]/, '')
.replace('/package.json', ''),
dependencies: {
'@angular/core': '~13.2.0',
'@angular/common': '~13.2.0',
rxjs: '~7.4.0',
},
}));
jest.spyOn(fs, 'readdirSync').mockImplementation((directoryPath: string) => {
const PACKAGE_SETUP = {
'@angular/core': [],
'@angular/common': ['http'],
http: ['testing'],
testing: [],
};
for (const key of Object.keys(PACKAGE_SETUP)) {
if (directoryPath.endsWith(key)) {
return PACKAGE_SETUP[key];
}
}
return [];
});
jest
.spyOn(fs, 'lstatSync')
.mockReturnValue({ isDirectory: () => true } as any);
}

View File

@ -1,322 +0,0 @@
import {
joinPathFragments,
logger,
type ProjectGraph,
readJsonFile,
workspaceRoot,
} from '@nx/devkit';
import { NormalModuleReplacementPlugin } from '@rspack/core';
import { existsSync } from 'fs';
import type { PackageJson } from 'nx/src/utils/package-json';
import { dirname, join, normalize } from 'path';
import type {
SharedLibraryConfig,
SharedWorkspaceLibraryConfig,
WorkspaceLibrary,
} from './models';
import { AdditionalSharedConfig, SharedFunction } from './models';
import { readRootPackageJson } from './package-json';
import {
collectPackageSecondaryEntryPoints,
collectWorkspaceLibrarySecondaryEntryPoints,
} from './secondary-entry-points';
import { getRootTsConfigPath, readTsPathMappings } from './typescript';
/**
* Build an object of functions to be used with the ModuleFederationPlugin to
* share Nx Workspace Libraries between Hosts and Remotes.
*
* @param workspaceLibs - The Nx Workspace Libraries to share
* @param tsConfigPath - The path to TS Config File that contains the Path Mappings for the Libraries
*/
export function shareWorkspaceLibraries(
workspaceLibs: WorkspaceLibrary[],
tsConfigPath = process.env.NX_TSCONFIG_PATH ?? getRootTsConfigPath()
): SharedWorkspaceLibraryConfig {
if (!workspaceLibs) {
return getEmptySharedLibrariesConfig();
}
const tsconfigPathAliases = readTsPathMappings(tsConfigPath);
if (!Object.keys(tsconfigPathAliases).length) {
return getEmptySharedLibrariesConfig();
}
// Nested projects must come first, sort them as such
const sortedTsConfigPathAliases = {};
Object.keys(tsconfigPathAliases)
.sort((a, b) => {
return b.split('/').length - a.split('/').length;
})
.forEach(
(key) => (sortedTsConfigPathAliases[key] = tsconfigPathAliases[key])
);
const pathMappings: { name: string; path: string }[] = [];
for (const [key, paths] of Object.entries(sortedTsConfigPathAliases)) {
const library = workspaceLibs.find((lib) => lib.importKey === key);
if (!library) {
continue;
}
// This is for Angular Projects that use ng-package.json
// It will do nothing for React Projects
collectWorkspaceLibrarySecondaryEntryPoints(
library,
sortedTsConfigPathAliases
).forEach(({ name, path }) =>
pathMappings.push({
name,
path,
})
);
pathMappings.push({
name: key,
path: normalize(join(workspaceRoot, paths[0])),
});
}
return {
getAliases: () =>
pathMappings.reduce(
(aliases, library) => ({
...aliases,
// If the library path ends in a wildcard, remove it as rspack can't handle this in resolve.alias
// e.g. path/to/my/lib/* -> path/to/my/lib
[library.name]: library.path.replace(/\/\*$/, ''),
}),
{}
),
getLibraries: (
projectRoot: string,
eager?: boolean
): Record<string, SharedLibraryConfig> => {
let pkgJson: PackageJson = null;
if (
projectRoot &&
existsSync(
joinPathFragments(workspaceRoot, projectRoot, 'package.json')
)
) {
pkgJson = readJsonFile(
joinPathFragments(workspaceRoot, projectRoot, 'package.json')
);
}
return pathMappings.reduce((libraries, library) => {
// Check to see if the library version is declared in the app's package.json
let version = pkgJson?.dependencies?.[library.name];
if (!version && workspaceLibs.length > 0) {
const workspaceLib = workspaceLibs.find(
(lib) => lib.importKey === library.name
);
const libPackageJsonPath = workspaceLib
? join(workspaceLib.root, 'package.json')
: null;
if (libPackageJsonPath && existsSync(libPackageJsonPath)) {
pkgJson = readJsonFile(libPackageJsonPath);
if (pkgJson) {
version = pkgJson.version;
}
}
}
return {
...libraries,
[library.name]: {
...(version
? {
requiredVersion: version,
singleton: true,
}
: { requiredVersion: false }),
eager,
},
};
}, {} as Record<string, SharedLibraryConfig>);
},
getReplacementPlugin: () =>
new NormalModuleReplacementPlugin(/./, (req) => {
if (!req.request.startsWith('.')) {
return;
}
const from = req.context;
const to = normalize(join(req.context, req.request));
for (const library of pathMappings) {
const libFolder = normalize(dirname(library.path));
if (!from.startsWith(libFolder) && to.startsWith(libFolder)) {
const newReq = library.name.endsWith('/*')
? /**
* req usually is in the form of "../../../path/to/file"
* library.path is usually in the form of "/Users/username/path/to/Workspace/path/to/library"
*
* When a wildcard is used in the TS path mappings, we want to get everything after the import to
* re-route the request correctly inline with the rspack resolve.alias
*/
join(
library.name,
req.request.split(
library.path.replace(workspaceRoot, '').replace('/*', '')
)[1]
)
: library.name;
req.request = newReq;
}
}
}),
};
}
/**
* Build the Module Federation Share Config for a specific package and the
* specified version of that package.
* @param pkgName - Name of the package to share
* @param version - Version of the package to require by other apps in the Module Federation setup
*/
export function getNpmPackageSharedConfig(
pkgName: string,
version: string
): SharedLibraryConfig | undefined {
if (!version) {
logger.warn(
`Could not find a version for "${pkgName}" in the root "package.json" ` +
'when collecting shared packages for the Module Federation setup. ' +
'The package will not be shared.'
);
return undefined;
}
return { singleton: true, strictVersion: true, requiredVersion: version };
}
/**
* Create a dictionary of packages and their Module Federation Shared Config
* from an array of package names.
*
* Lookup the versions of the packages from the root package.json file in the
* workspace.
* @param packages - Array of package names as strings
*/
export function sharePackages(
packages: string[]
): Record<string, SharedLibraryConfig> {
const pkgJson = readRootPackageJson();
const allPackages: { name: string; version: string }[] = [];
packages.forEach((pkg) => {
const pkgVersion =
pkgJson.dependencies?.[pkg] ?? pkgJson.devDependencies?.[pkg];
allPackages.push({ name: pkg, version: pkgVersion });
collectPackageSecondaryEntryPoints(pkg, pkgVersion, allPackages);
});
return allPackages.reduce((shared, pkg) => {
const config = getNpmPackageSharedConfig(pkg.name, pkg.version);
if (config) {
shared[pkg.name] = config;
}
return shared;
}, {} as Record<string, SharedLibraryConfig>);
}
/**
* Apply a custom function provided by the user that will modify the Shared Config
* of the dependencies for the Module Federation build.
*
* @param sharedConfig - The original Shared Config to be modified
* @param sharedFn - The custom function to run
*/
export function applySharedFunction(
sharedConfig: Record<string, SharedLibraryConfig>,
sharedFn: SharedFunction | undefined
): void {
if (!sharedFn) {
return;
}
for (const [libraryName, library] of Object.entries(sharedConfig)) {
const mappedDependency = sharedFn(libraryName, library);
if (mappedDependency === false) {
delete sharedConfig[libraryName];
continue;
} else if (!mappedDependency) {
continue;
}
sharedConfig[libraryName] = mappedDependency;
}
}
/**
* Add additional dependencies to the shared package that may not have been
* discovered by the project graph.
*
* This can be useful for applications that use a Dependency Injection system
* that expects certain Singleton values to be present in the shared injection
* hierarchy.
*
* @param sharedConfig - The original Shared Config
* @param additionalShared - The additional dependencies to add
* @param projectGraph - The Nx project graph
*/
export function applyAdditionalShared(
sharedConfig: Record<string, SharedLibraryConfig>,
additionalShared: AdditionalSharedConfig | undefined,
projectGraph: ProjectGraph
): void {
if (!additionalShared) {
return;
}
for (const shared of additionalShared) {
if (typeof shared === 'string') {
addStringDependencyToSharedConfig(sharedConfig, shared, projectGraph);
} else if (Array.isArray(shared)) {
sharedConfig[shared[0]] = shared[1];
} else if (typeof shared === 'object') {
sharedConfig[shared.libraryName] = shared.sharedConfig;
}
}
}
function addStringDependencyToSharedConfig(
sharedConfig: Record<string, SharedLibraryConfig>,
dependency: string,
projectGraph: ProjectGraph
): void {
if (projectGraph.nodes[dependency]) {
sharedConfig[dependency] = { requiredVersion: false };
} else if (projectGraph.externalNodes?.[`npm:${dependency}`]) {
const pkgJson = readRootPackageJson();
const config = getNpmPackageSharedConfig(
dependency,
pkgJson.dependencies?.[dependency] ??
pkgJson.devDependencies?.[dependency]
);
if (!config) {
return;
}
sharedConfig[dependency] = config;
} else {
throw new Error(
`The specified dependency "${dependency}" in the additionalShared configuration does not exist in the project graph. ` +
`Please check your additionalShared configuration and make sure you are including valid workspace projects or npm packages.`
);
}
}
function getEmptySharedLibrariesConfig() {
return {
getAliases: () => ({}),
getLibraries: () => ({}),
getReplacementPlugin: () =>
new NormalModuleReplacementPlugin(/./, () => undefined),
};
}

View File

@ -1,62 +0,0 @@
import { logger } from '@nx/devkit';
import type { Express } from 'express';
import { existsSync, readFileSync } from 'fs';
import { StaticRemotesConfig } from './parse-static-remotes-config';
export function startRemoteProxies(
staticRemotesConfig: StaticRemotesConfig,
mappedLocationsOfRemotes: Record<string, string>,
sslOptions?: { pathToCert: string; pathToKey: string }
) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { createProxyMiddleware } = require('http-proxy-middleware');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const express = require('express');
let sslCert: Buffer;
let sslKey: Buffer;
if (sslOptions && sslOptions.pathToCert && sslOptions.pathToKey) {
if (existsSync(sslOptions.pathToCert) && existsSync(sslOptions.pathToKey)) {
sslCert = readFileSync(sslOptions.pathToCert);
sslKey = readFileSync(sslOptions.pathToKey);
} else {
logger.warn(
`Encountered SSL options in project.json, however, the certificate files do not exist in the filesystem. Using http.`
);
logger.warn(
`Attempted to find '${sslOptions.pathToCert}' and '${sslOptions.pathToKey}'.`
);
}
}
// eslint-disable-next-line @typescript-eslint/no-var-requires
const http: typeof import('http') = require('http');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const https: typeof import('https') = require('https');
logger.info(`NX Starting static remotes proxies...`);
for (const app of staticRemotesConfig.remotes) {
const expressProxy: Express = express();
expressProxy.use(
createProxyMiddleware({
target: mappedLocationsOfRemotes[app],
changeOrigin: true,
secure: sslCert ? false : undefined,
})
);
const proxyServer = (
sslCert
? https.createServer(
{
cert: sslCert,
key: sslKey,
},
expressProxy
)
: http.createServer(expressProxy)
).listen(staticRemotesConfig.config[app].port);
process.on('SIGTERM', () => proxyServer.close());
process.on('exit', () => proxyServer.close());
}
logger.info(`NX Static remotes proxies started successfully`);
}

View File

@ -1,77 +0,0 @@
import { logger } from '@nx/devkit';
import type { Express } from 'express';
import { existsSync, readFileSync } from 'fs';
import type { StaticRemotesConfig } from './parse-static-remotes-config';
export function startSsrRemoteProxies(
staticRemotesConfig: StaticRemotesConfig,
mappedLocationsOfRemotes: Record<string, string>,
sslOptions?: { pathToCert: string; pathToKey: string }
) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { createProxyMiddleware } = require('http-proxy-middleware');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const express = require('express');
let sslCert: Buffer;
let sslKey: Buffer;
if (sslOptions && sslOptions.pathToCert && sslOptions.pathToKey) {
if (existsSync(sslOptions.pathToCert) && existsSync(sslOptions.pathToKey)) {
sslCert = readFileSync(sslOptions.pathToCert);
sslKey = readFileSync(sslOptions.pathToKey);
} else {
logger.warn(
`Encountered SSL options in project.json, however, the certificate files do not exist in the filesystem. Using http.`
);
logger.warn(
`Attempted to find '${sslOptions.pathToCert}' and '${sslOptions.pathToKey}'.`
);
}
}
// eslint-disable-next-line @typescript-eslint/no-var-requires
const http: typeof import('http') = require('http');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const https: typeof import('https') = require('https');
logger.info(`NX Starting static remotes proxies...`);
for (const app of staticRemotesConfig.remotes) {
const expressProxy: Express = express();
/**
* SSR remotes have two output paths: one for the browser and one for the server.
* We need to handle paths for both of them.
* The browser output path is used to serve the client-side code.
* The server output path is used to serve the server-side code.
*/
expressProxy.use(
createProxyMiddleware({
target: `${mappedLocationsOfRemotes[app]}`,
secure: sslCert ? false : undefined,
changeOrigin: true,
pathRewrite: (path) => {
if (path.includes('/server')) {
return path;
} else {
return `browser/${path}`;
}
},
})
);
const proxyServer = (
sslCert
? https.createServer(
{
cert: sslCert,
key: sslKey,
},
expressProxy
)
: http.createServer(expressProxy)
).listen(staticRemotesConfig.config[app].port);
process.on('SIGTERM', () => proxyServer.close());
process.on('exit', () => proxyServer.close());
}
logger.info(`Nx SSR Static remotes proxies started successfully`);
}

View File

@ -1,75 +0,0 @@
import { existsSync } from 'fs';
import { ParsedCommandLine } from 'typescript';
import { dirname, join } from 'path';
import { workspaceRoot } from '@nx/devkit';
const tsConfig: Map<string, ParsedCommandLine> = new Map();
const tsPathMappings: Map<string, ParsedCommandLine['options']['paths']> =
new Map();
export function readTsPathMappings(
tsConfigPath: string = process.env.NX_TSCONFIG_PATH ?? getRootTsConfigPath()
): ParsedCommandLine['options']['paths'] {
if (tsPathMappings.has(tsConfigPath)) {
return tsPathMappings.get(tsConfigPath);
}
if (!tsConfig.has(tsConfigPath)) {
tsConfig.set(tsConfigPath, readTsConfiguration(tsConfigPath));
}
tsPathMappings.set(tsConfigPath, {});
Object.entries(tsConfig.get(tsConfigPath).options?.paths ?? {}).forEach(
([alias, paths]) => {
tsPathMappings.set(tsConfigPath, {
...tsPathMappings.get(tsConfigPath),
[alias]: paths.map((path) => path.replace(/^\.\//, '')),
});
}
);
return tsPathMappings.get(tsConfigPath);
}
function readTsConfiguration(tsConfigPath: string): ParsedCommandLine {
if (!existsSync(tsConfigPath)) {
throw new Error(
`NX MF: TsConfig Path for workspace libraries does not exist! (${tsConfigPath}).`
);
}
return readTsConfig(tsConfigPath);
}
let tsModule: typeof import('typescript');
export function readTsConfig(tsConfigPath: string): ParsedCommandLine {
if (!tsModule) {
tsModule = require('typescript');
}
const readResult = tsModule.readConfigFile(
tsConfigPath,
tsModule.sys.readFile
);
return tsModule.parseJsonConfigFileContent(
readResult.config,
tsModule.sys,
dirname(tsConfigPath)
);
}
export function getRootTsConfigPath(): string | null {
const tsConfigFileName = getRootTsConfigFileName();
return tsConfigFileName ? join(workspaceRoot, tsConfigFileName) : null;
}
function getRootTsConfigFileName(): string | null {
for (const tsConfigName of ['tsconfig.base.json', 'tsconfig.json']) {
const tsConfigPath = join(workspaceRoot, tsConfigName);
if (existsSync(tsConfigPath)) {
return tsConfigName;
}
}
return null;
}

View File

@ -1,16 +0,0 @@
import { joinPathFragments, readJsonFile, workspaceRoot } from '@nx/devkit';
import { existsSync } from 'fs';
export function readRootPackageJson(): {
dependencies?: { [key: string]: string };
devDependencies?: { [key: string]: string };
} {
const pkgJsonPath = joinPathFragments(workspaceRoot, 'package.json');
if (!existsSync(pkgJsonPath)) {
throw new Error(
'NX MFE: Could not find root package.json to determine dependency versions.'
);
}
return readJsonFile(pkgJsonPath);
}

View File

@ -4,15 +4,16 @@ import {
readCachedProjectGraph,
} from '@nx/devkit';
import { readCachedProjectConfiguration } from 'nx/src/project-graph/project-graph';
import { getDependentPackagesForProject } from '../dependencies';
import { ModuleFederationConfig } from '../models';
import { mapRemotes, mapRemotesForSSR } from '../remotes';
import {
ModuleFederationConfig,
applyAdditionalShared,
applySharedFunction,
sharePackages,
shareWorkspaceLibraries,
} from '../share';
mapRemotes,
mapRemotesForSSR,
getDependentPackagesForProject,
} from '@nx/module-federation';
export function getFunctionDeterminateRemoteUrl(isServer = false) {
const target = 'serve';
@ -123,7 +124,8 @@ export async function getModuleFederationConfig(
mappedRemotes = mapRemotesFunction(
mfConfig.remotes,
'js',
determineRemoteUrlFunction
determineRemoteUrlFunction,
true
);
}

View File

@ -2,7 +2,7 @@ import { DefinePlugin } from '@rspack/core';
import {
ModuleFederationConfig,
NxModuleFederationConfigOverride,
} from '../models';
} from '@nx/module-federation';
import { getModuleFederationConfig } from './utils';
import { NxRspackExecutionContext } from '../../config';
@ -57,7 +57,7 @@ export async function withModuleFederationForSSR(
...(configOverride?.runtimePlugins ?? []),
require.resolve('@module-federation/node/runtimePlugin'),
require.resolve(
'@nx/rspack/src/utils/module-federation/plugins/runtime-library-control.plugin.js'
'@nx/module-federation/src/utils/plugins/runtime-library-control.plugin.js'
),
]
: [

View File

@ -4,7 +4,7 @@ import { DefinePlugin } from '@rspack/core';
import {
ModuleFederationConfig,
NxModuleFederationConfigOverride,
} from '../models';
} from '@nx/module-federation';
import { getModuleFederationConfig } from './utils';
import { NxRspackExecutionContext } from '../../config';
@ -79,7 +79,7 @@ export async function withModuleFederation(
? [
...(configOverride?.runtimePlugins ?? []),
require.resolve(
'@nx/rspack/src/utils/module-federation/plugins/runtime-library-control.plugin.js'
'@nx/module-federation/src/utils/plugins/runtime-library-control.plugin.js'
),
]
: configOverride?.runtimePlugins,

View File

@ -36,5 +36,4 @@ export * from './src/executors/webpack/webpack.impl';
export * from './src/utils/get-css-module-local-ident';
export * from './src/utils/with-nx';
export * from './src/utils/with-web';
export * from './src/utils/module-federation/public-api';
export * from './src/utils/e2e-web-server-info-utils';

View File

@ -32,8 +32,6 @@
"dependencies": {
"@babel/core": "^7.23.2",
"@phenomnomnominal/tsquery": "~5.0.1",
"@module-federation/sdk": "^0.6.0",
"@module-federation/enhanced": "^0.6.0",
"ajv": "^8.12.0",
"autoprefixer": "^10.4.9",
"babel-loader": "^9.1.2",
@ -42,9 +40,7 @@
"copy-webpack-plugin": "^10.2.4",
"css-loader": "^6.4.0",
"css-minimizer-webpack-plugin": "^5.0.0",
"express": "^4.19.2",
"fork-ts-checker-webpack-plugin": "7.2.13",
"http-proxy-middleware": "^3.0.3",
"less": "4.1.3",
"less-loader": "11.1.0",
"license-webpack-plugin": "^4.0.2",

View File

@ -1,158 +0,0 @@
import * as tsUtils from './typescript';
import { getDependentPackagesForProject } from './dependencies';
describe('getDependentPackagesForProject', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should collect npm packages and workspaces libraries without duplicates', () => {
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
'@myorg/lib1': ['libs/lib1/src/index.ts'],
'@myorg/lib2': ['libs/lib2/src/index.ts'],
});
const dependencies = getDependentPackagesForProject(
{
dependencies: {
shell: [
{ source: 'shell', target: 'lib1', type: 'static' },
{ source: 'shell', target: 'lib2', type: 'static' },
{ source: 'shell', target: 'npm:lodash', type: 'static' },
],
lib1: [{ source: 'lib1', target: 'lib2', type: 'static' }],
lib2: [{ source: 'lib2', target: 'npm:lodash', type: 'static' }],
},
nodes: {
shell: {
name: 'shell',
data: { root: 'apps/shell', sourceRoot: 'apps/shell/src' },
type: 'app',
},
lib1: {
name: 'lib1',
data: { root: 'libs/lib1', sourceRoot: 'libs/lib1/src' },
type: 'lib',
},
lib2: {
name: 'lib2',
data: { root: 'libs/lib2', sourceRoot: 'libs/lib2/src' },
type: 'lib',
},
} as any,
},
'shell'
);
expect(dependencies).toEqual({
workspaceLibraries: [
{ name: 'lib1', root: 'libs/lib1', importKey: '@myorg/lib1' },
{ name: 'lib2', root: 'libs/lib2', importKey: '@myorg/lib2' },
],
npmPackages: ['lodash'],
});
});
it('should collect workspaces libraries recursively', () => {
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
'@myorg/lib1': ['libs/lib1/src/index.ts'],
'@myorg/lib2': ['libs/lib2/src/index.ts'],
'@myorg/lib3': ['libs/lib3/src/index.ts'],
});
const dependencies = getDependentPackagesForProject(
{
dependencies: {
shell: [{ source: 'shell', target: 'lib1', type: 'static' }],
lib1: [{ source: 'lib1', target: 'lib2', type: 'static' }],
lib2: [{ source: 'lib2', target: 'lib3', type: 'static' }],
},
nodes: {
shell: {
name: 'shell',
data: { root: 'apps/shell', sourceRoot: 'apps/shell/src' },
type: 'app',
},
lib1: {
name: 'lib1',
data: { root: 'libs/lib1', sourceRoot: 'libs/lib1/src' },
type: 'lib',
},
lib2: {
name: 'lib2',
data: { root: 'libs/lib2', sourceRoot: 'libs/lib2/src' },
type: 'lib',
},
lib3: {
name: 'lib3',
data: { root: 'libs/lib3', sourceRoot: 'libs/lib3/src' },
type: 'lib',
},
} as any,
},
'shell'
);
expect(dependencies).toEqual({
workspaceLibraries: [
{ name: 'lib1', root: 'libs/lib1', importKey: '@myorg/lib1' },
{ name: 'lib2', root: 'libs/lib2', importKey: '@myorg/lib2' },
{ name: 'lib3', root: 'libs/lib3', importKey: '@myorg/lib3' },
],
npmPackages: [],
});
});
it('should ignore TS path mappings with wildcards', () => {
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
'@myorg/lib1': ['libs/lib1/src/index.ts'],
'@myorg/lib1/*': ['libs/lib1/src/lib/*'],
'@myorg/lib2': ['libs/lib2/src/index.ts'],
'@myorg/lib2/*': ['libs/lib2/src/lib/*'],
'@myorg/lib3': ['libs/lib3/src/index.ts'],
'@myorg/lib3/*': ['libs/lib3/src/lib/*'],
});
const dependencies = getDependentPackagesForProject(
{
dependencies: {
shell: [{ source: 'shell', target: 'lib1', type: 'static' }],
lib1: [{ source: 'lib1', target: 'lib2', type: 'static' }],
lib2: [{ source: 'lib2', target: 'lib3', type: 'static' }],
},
nodes: {
shell: {
name: 'shell',
data: { root: 'apps/shell', sourceRoot: 'apps/shell/src' },
type: 'app',
},
lib1: {
name: 'lib1',
data: { root: 'libs/lib1', sourceRoot: 'libs/lib1/src' },
type: 'lib',
},
lib2: {
name: 'lib2',
data: { root: 'libs/lib2', sourceRoot: 'libs/lib2/src' },
type: 'lib',
},
lib3: {
name: 'lib3',
data: { root: 'libs/lib3', sourceRoot: 'libs/lib3/src' },
type: 'lib',
},
} as any,
},
'shell'
);
expect(dependencies).toEqual({
workspaceLibraries: [
{ name: 'lib1', root: 'libs/lib1', importKey: '@myorg/lib1' },
{ name: 'lib2', root: 'libs/lib2', importKey: '@myorg/lib2' },
{ name: 'lib3', root: 'libs/lib3', importKey: '@myorg/lib3' },
],
npmPackages: [],
});
});
});

View File

@ -1,94 +0,0 @@
import type { ProjectGraph } from '@nx/devkit';
import type { WorkspaceLibrary } from './models';
import { readTsPathMappings } from './typescript';
import {
getOutputsForTargetAndConfiguration,
parseTargetString,
} from '@nx/devkit';
export function getDependentPackagesForProject(
projectGraph: ProjectGraph,
name: string
): {
workspaceLibraries: WorkspaceLibrary[];
npmPackages: string[];
} {
const { npmPackages, workspaceLibraries } = collectDependencies(
projectGraph,
name
);
return {
workspaceLibraries: [...workspaceLibraries.values()],
npmPackages: [...npmPackages],
};
}
function collectDependencies(
projectGraph: ProjectGraph,
name: string,
dependencies = {
workspaceLibraries: new Map<string, WorkspaceLibrary>(),
npmPackages: new Set<string>(),
},
seen: Set<string> = new Set()
): {
workspaceLibraries: Map<string, WorkspaceLibrary>;
npmPackages: Set<string>;
} {
if (seen.has(name)) {
return dependencies;
}
seen.add(name);
(projectGraph.dependencies[name] ?? []).forEach((dependency) => {
if (dependency.target.startsWith('npm:')) {
dependencies.npmPackages.add(dependency.target.replace('npm:', ''));
} else {
dependencies.workspaceLibraries.set(dependency.target, {
name: dependency.target,
root: projectGraph.nodes[dependency.target].data.root,
importKey: getLibraryImportPath(dependency.target, projectGraph),
});
collectDependencies(projectGraph, dependency.target, dependencies, seen);
}
});
return dependencies;
}
function getLibraryImportPath(
library: string,
projectGraph: ProjectGraph
): string | undefined {
let buildLibsFromSource = true;
if (process.env.NX_BUILD_LIBS_FROM_SOURCE) {
buildLibsFromSource = process.env.NX_BUILD_LIBS_FROM_SOURCE === 'true';
}
const libraryNode = projectGraph.nodes[library];
let sourceRoots = [libraryNode.data.sourceRoot];
if (!buildLibsFromSource && process.env.NX_BUILD_TARGET) {
const buildTarget = parseTargetString(
process.env.NX_BUILD_TARGET,
projectGraph
);
sourceRoots = getOutputsForTargetAndConfiguration(
buildTarget,
{},
libraryNode
);
}
const tsConfigPathMappings = readTsPathMappings();
for (const [key, value] of Object.entries(tsConfigPathMappings)) {
for (const src of sourceRoots) {
if (value.find((path) => path.startsWith(src))) {
return key;
}
}
}
return undefined;
}

View File

@ -1,6 +0,0 @@
export * from './share';
export * from './dependencies';
export * from './package-json';
export * from './remotes';
export * from './models';
export * from './get-remotes-for-host';

View File

@ -1,70 +0,0 @@
import type { NormalModuleReplacementPlugin } from 'webpack';
import type { moduleFederationPlugin } from '@module-federation/sdk';
export type ModuleFederationLibrary = { type: string; name: string };
export type WorkspaceLibrary = {
name: string;
root: string;
importKey: string | undefined;
};
export type SharedWorkspaceLibraryConfig = {
getAliases: () => Record<string, string>;
getLibraries: (
projectRoot: string,
eager?: boolean
) => Record<string, SharedLibraryConfig>;
getReplacementPlugin: () => NormalModuleReplacementPlugin;
};
export type Remotes = Array<string | [remoteName: string, remoteUrl: string]>;
export interface SharedLibraryConfig {
singleton?: boolean;
strictVersion?: boolean;
requiredVersion?: false | string;
eager?: boolean;
}
export type SharedFunction = (
libraryName: string,
sharedConfig: SharedLibraryConfig
) => undefined | false | SharedLibraryConfig;
export type AdditionalSharedConfig = Array<
| string
| [libraryName: string, sharedConfig: SharedLibraryConfig]
| { libraryName: string; sharedConfig: SharedLibraryConfig }
>;
export interface ModuleFederationConfig {
name: string;
remotes?: Remotes;
library?: ModuleFederationLibrary;
exposes?: Record<string, string>;
shared?: SharedFunction;
additionalShared?: AdditionalSharedConfig;
/**
* `nxRuntimeLibraryControlPlugin` is a runtime module federation plugin to ensure
* that shared libraries are resolved from a remote with live reload capabilities.
* If you run into any issues with loading shared libraries, try disabling this option.
*/
disableNxRuntimeLibraryControlPlugin?: boolean;
}
export type NxModuleFederationConfigOverride = Omit<
moduleFederationPlugin.ModuleFederationPluginOptions,
| 'exposes'
| 'remotes'
| 'name'
| 'library'
| 'shared'
| 'filename'
| 'remoteType'
>;
export type WorkspaceLibrarySecondaryEntryPoint = {
name: string;
path: string;
};

View File

@ -1,16 +0,0 @@
import { existsSync } from 'fs';
import { workspaceRoot, readJsonFile, joinPathFragments } from '@nx/devkit';
export function readRootPackageJson(): {
dependencies?: { [key: string]: string };
devDependencies?: { [key: string]: string };
} {
const pkgJsonPath = joinPathFragments(workspaceRoot, 'package.json');
if (!existsSync(pkgJsonPath)) {
throw new Error(
'NX MF: Could not find root package.json to determine dependency versions.'
);
}
return readJsonFile(pkgJsonPath);
}

View File

@ -1,57 +0,0 @@
import type { ExecutorContext } from '@nx/devkit';
import { basename, dirname } from 'path';
export type StaticRemoteConfig = {
basePath: string;
outputPath: string;
urlSegment: string;
port: number;
};
export type StaticRemotesConfig = {
remotes: string[];
config: Record<string, StaticRemoteConfig> | undefined;
};
export function parseStaticRemotesConfig(
staticRemotes: string[] | undefined,
context: ExecutorContext
): StaticRemotesConfig {
if (!staticRemotes?.length) {
return { remotes: [], config: undefined };
}
const config: Record<string, StaticRemoteConfig> = {};
for (const app of staticRemotes) {
const outputPath =
context.projectGraph.nodes[app].data.targets['build'].options.outputPath;
const basePath = dirname(outputPath);
const urlSegment = basename(outputPath);
const port =
context.projectGraph.nodes[app].data.targets['serve'].options.port;
config[app] = { basePath, outputPath, urlSegment, port };
}
return { remotes: staticRemotes, config };
}
export function parseStaticSsrRemotesConfig(
staticRemotes: string[] | undefined,
context: ExecutorContext
): StaticRemotesConfig {
if (!staticRemotes?.length) {
return { remotes: [], config: undefined };
}
const config: Record<string, StaticRemoteConfig> = {};
for (const app of staticRemotes) {
const outputPath = dirname(
context.projectGraph.nodes[app].data.targets['build'].options.outputPath // dist/checkout/browser -> checkout
) as string;
const basePath = dirname(outputPath); // dist/checkout -> dist
const urlSegment = basename(outputPath); // dist/checkout -> checkout
const port =
context.projectGraph.nodes[app].data.targets['serve'].options.port;
config[app] = { basePath, outputPath, urlSegment, port };
}
return { remotes: staticRemotes, config };
}

View File

@ -1,71 +0,0 @@
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
const runtimeStore: {
name?: string;
devRemotes?: string[];
sharedPackagesFromDev: Record<string, string>;
} = {
sharedPackagesFromDev: {},
};
if (process.env.NX_MF_DEV_REMOTES) {
// process.env.NX_MF_DEV_REMOTES is replaced by an array value via DefinePlugin, even though the original value is a stringified array.
runtimeStore.devRemotes = process.env
.NX_MF_DEV_REMOTES as unknown as string[];
}
const nxRuntimeLibraryControlPlugin: () => FederationRuntimePlugin =
function () {
return {
name: 'nx-runtime-library-control-plugin',
beforeInit(args) {
runtimeStore.name = args.options.name;
return args;
},
resolveShare: (args) => {
const { shareScopeMap, scope, pkgName, version, GlobalFederation } =
args;
const originalResolver = args.resolver;
args.resolver = function () {
if (!runtimeStore.sharedPackagesFromDev[pkgName]) {
if (!GlobalFederation.__INSTANCES__) {
return originalResolver();
} else if (!runtimeStore.devRemotes) {
return originalResolver();
}
const devRemoteInstanceToUse = GlobalFederation.__INSTANCES__.find(
(instance) =>
instance.options.shared[pkgName] &&
runtimeStore.devRemotes.find((dr) => instance.name === dr)
);
if (!devRemoteInstanceToUse) {
return originalResolver();
}
runtimeStore.sharedPackagesFromDev[pkgName] =
devRemoteInstanceToUse.name;
}
const remoteInstanceName =
runtimeStore.sharedPackagesFromDev[pkgName];
const remoteInstance = GlobalFederation.__INSTANCES__.find(
(instance) => instance.name === remoteInstanceName
);
try {
const remotePkgInfo = remoteInstance.options.shared[pkgName].find(
(shared) => shared.from === remoteInstanceName
);
remotePkgInfo.useIn.push(runtimeStore.name);
remotePkgInfo.useIn = Array.from(new Set(remotePkgInfo.useIn));
shareScopeMap[scope][pkgName][version] = remotePkgInfo;
return remotePkgInfo;
} catch {
return originalResolver();
}
};
return args;
},
};
};
export default nxRuntimeLibraryControlPlugin;

View File

@ -1,45 +0,0 @@
import * as fs from 'fs';
import { readTsPathMappings } from './typescript';
let readConfigFileResult: any;
let parseJsonConfigFileContentResult: any;
jest.mock('typescript', () => ({
...jest.requireActual('typescript'),
readConfigFile: jest.fn().mockImplementation(() => readConfigFileResult),
parseJsonConfigFileContent: jest
.fn()
.mockImplementation(() => parseJsonConfigFileContentResult),
}));
describe('readTsPathMappings', () => {
it('should normalize paths', () => {
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
readConfigFileResult = {
config: {
options: {
paths: {
'@myorg/lib1': ['./libs/lib1/src/index.ts'],
'@myorg/lib2': ['libs/lib2/src/index.ts'],
},
},
},
};
parseJsonConfigFileContentResult = {
options: {
paths: {
'@myorg/lib1': ['./libs/lib1/src/index.ts'],
'@myorg/lib2': ['libs/lib2/src/index.ts'],
},
},
fileNames: [],
errors: [],
};
const paths = readTsPathMappings('/path/to/tsconfig.json');
expect(paths).toEqual({
'@myorg/lib1': ['libs/lib1/src/index.ts'],
'@myorg/lib2': ['libs/lib2/src/index.ts'],
});
});
});

View File

@ -47,6 +47,8 @@
"@nx/jest/*": ["packages/jest/*"],
"@nx/js": ["packages/js/src/index.ts"],
"@nx/js/*": ["packages/js/*"],
"@nx/module-federation": ["packages/module-federation"],
"@nx/module-federation/*": ["packages/module-federation/*"],
"@nx/nest": ["packages/nest"],
"@nx/next": ["packages/next"],
"@nx/next/*": ["packages/next/*"],