From 6f952dbdfd088fd10feabba7ef5661719f94e09f Mon Sep 17 00:00:00 2001
From: Nicholas Cunningham <ndcunningham@gmail.com>
Date: Thu, 5 Oct 2023 12:19:08 -0600
Subject: [PATCH] feat(react): Add federate-module generator

---
 docs/generated/manifests/menus.json           |   8 +
 docs/generated/manifests/nx-api.json          |   9 +
 docs/generated/packages-metadata.json         |   9 +
 .../react/generators/federate-module.json     | 118 +++++++++++++
 docs/shared/reference/sitemap.md              |   1 +
 .../src/react-module-federation.test.ts       | 102 ++++++++++-
 packages/react/generators.json                |  14 ++
 .../federate-module/federate-module.spec.ts   | 101 +++++++++++
 .../federate-module/federate-module.ts        |  79 +++++++++
 .../federate-module/lib/utils.spec.ts         |  89 ++++++++++
 .../generators/federate-module/lib/utils.ts   | 164 ++++++++++++++++++
 .../generators/federate-module/schema.d.ts    |  12 ++
 .../generators/federate-module/schema.json    | 116 +++++++++++++
 .../react/src/generators/remote/remote.ts     |   2 +-
 .../with-module-federation.ts                 |   3 +-
 15 files changed, 822 insertions(+), 5 deletions(-)
 create mode 100644 docs/generated/packages/react/generators/federate-module.json
 create mode 100644 packages/react/src/generators/federate-module/federate-module.spec.ts
 create mode 100644 packages/react/src/generators/federate-module/federate-module.ts
 create mode 100644 packages/react/src/generators/federate-module/lib/utils.spec.ts
 create mode 100644 packages/react/src/generators/federate-module/lib/utils.ts
 create mode 100644 packages/react/src/generators/federate-module/schema.d.ts
 create mode 100644 packages/react/src/generators/federate-module/schema.json

diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json
index 6e85264771710..5c640605218d6 100644
--- a/docs/generated/manifests/menus.json
+++ b/docs/generated/manifests/menus.json
@@ -8058,6 +8058,14 @@
                 "children": [],
                 "isExternal": false,
                 "disableCollapsible": false
+              },
+              {
+                "id": "federate-module",
+                "path": "/nx-api/react/generators/federate-module",
+                "name": "federate-module",
+                "children": [],
+                "isExternal": false,
+                "disableCollapsible": false
               }
             ],
             "isExternal": false,
diff --git a/docs/generated/manifests/nx-api.json b/docs/generated/manifests/nx-api.json
index 4db7c83acf990..228eaf00234a9 100644
--- a/docs/generated/manifests/nx-api.json
+++ b/docs/generated/manifests/nx-api.json
@@ -2164,6 +2164,15 @@
         "originalFilePath": "/packages/react/src/generators/setup-ssr/schema.json",
         "path": "/nx-api/react/generators/setup-ssr",
         "type": "generator"
+      },
+      "/nx-api/react/generators/federate-module": {
+        "description": "Federate a module.",
+        "file": "generated/packages/react/generators/federate-module.json",
+        "hidden": false,
+        "name": "federate-module",
+        "originalFilePath": "/packages/react/src/generators/federate-module/schema.json",
+        "path": "/nx-api/react/generators/federate-module",
+        "type": "generator"
       }
     },
     "path": "/nx-api/react"
diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json
index 1429c5cc8270c..667fc7768181f 100644
--- a/docs/generated/packages-metadata.json
+++ b/docs/generated/packages-metadata.json
@@ -2140,6 +2140,15 @@
         "originalFilePath": "/packages/react/src/generators/setup-ssr/schema.json",
         "path": "react/generators/setup-ssr",
         "type": "generator"
+      },
+      {
+        "description": "Federate a module.",
+        "file": "generated/packages/react/generators/federate-module.json",
+        "hidden": false,
+        "name": "federate-module",
+        "originalFilePath": "/packages/react/src/generators/federate-module/schema.json",
+        "path": "react/generators/federate-module",
+        "type": "generator"
       }
     ],
     "githubRoot": "https://github.com/nrwl/nx/blob/master",
diff --git a/docs/generated/packages/react/generators/federate-module.json b/docs/generated/packages/react/generators/federate-module.json
new file mode 100644
index 0000000000000..0fe80424ead41
--- /dev/null
+++ b/docs/generated/packages/react/generators/federate-module.json
@@ -0,0 +1,118 @@
+{
+  "name": "federate-module",
+  "factory": "./src/generators/federate-module/federate-module#federateModuleGenerator",
+  "schema": {
+    "$schema": "http://json-schema.org/schema",
+    "cli": "nx",
+    "$id": "NxReactFederateModule",
+    "title": "Federate Module",
+    "description": "Create a federated module, which can be loaded by a remote host.",
+    "examples": [
+      {
+        "command": "nx g federate-module MyModule --path=./src/component/my-cmp.ts --remote=my-remote-app",
+        "description": "Create a federated module from my-remote-app, that exposes my-cmp from ./src/component/my-cmp.ts as MyModule."
+      }
+    ],
+    "type": "object",
+    "properties": {
+      "name": {
+        "description": "The name of the module.",
+        "type": "string",
+        "$default": { "$source": "argv", "index": 0 },
+        "x-prompt": "What name would you like to use for the module?",
+        "pattern": "^[a-zA-Z][^:]*$",
+        "x-priority": "important"
+      },
+      "path": {
+        "type": "string",
+        "description": "The path to locate the federated module.",
+        "x-prompt": "What is the path to the module to be federated?"
+      },
+      "remote": {
+        "type": "string",
+        "description": "The name of the remote.",
+        "x-prompt": "What is/should the remote be named?"
+      },
+      "projectNameAndRootFormat": {
+        "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
+        "type": "string",
+        "enum": ["as-provided", "derived"]
+      },
+      "style": {
+        "description": "The file extension to be used for style files.",
+        "type": "string",
+        "default": "css",
+        "alias": "s",
+        "x-prompt": {
+          "message": "Which stylesheet format would you like to use?",
+          "type": "list",
+          "items": [
+            { "value": "css", "label": "CSS" },
+            {
+              "value": "scss",
+              "label": "SASS(.scss)       [ http://sass-lang.com                     ]"
+            },
+            {
+              "value": "less",
+              "label": "LESS              [ http://lesscss.org                       ]"
+            },
+            {
+              "value": "styled-components",
+              "label": "styled-components [ https://styled-components.com            ]"
+            },
+            {
+              "value": "@emotion/styled",
+              "label": "emotion           [ https://emotion.sh                       ]"
+            },
+            {
+              "value": "styled-jsx",
+              "label": "styled-jsx        [ https://www.npmjs.com/package/styled-jsx ]"
+            },
+            {
+              "value": "styl",
+              "label": "DEPRECATD: Stylus(.styl) [ http://stylus-lang.com            ]"
+            },
+            { "value": "none", "label": "None" }
+          ]
+        }
+      },
+      "linter": {
+        "description": "The tool to use for running lint checks.",
+        "type": "string",
+        "enum": ["eslint"],
+        "default": "eslint"
+      },
+      "skipFormat": {
+        "description": "Skip formatting files.",
+        "type": "boolean",
+        "default": false,
+        "x-priority": "internal"
+      },
+      "unitTestRunner": {
+        "type": "string",
+        "enum": ["jest", "none"],
+        "description": "Test runner to use for unit tests.",
+        "default": "jest"
+      },
+      "e2eTestRunner": {
+        "type": "string",
+        "enum": ["cypress", "none"],
+        "description": "Test runner to use for end to end (e2e) tests.",
+        "default": "cypress"
+      },
+      "host": {
+        "type": "string",
+        "description": "The host / shell application for this remote."
+      }
+    },
+    "required": ["name", "path", "remote"],
+    "additionalProperties": false,
+    "presets": []
+  },
+  "description": "Federate a module.",
+  "hidden": false,
+  "implementation": "/packages/react/src/generators/federate-module/federate-module#federateModuleGenerator.ts",
+  "aliases": [],
+  "path": "/packages/react/src/generators/federate-module/schema.json",
+  "type": "generator"
+}
diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md
index 0aba966e6963f..cc8fc339724dd 100644
--- a/docs/shared/reference/sitemap.md
+++ b/docs/shared/reference/sitemap.md
@@ -557,6 +557,7 @@
       - [component-test](/nx-api/react/generators/component-test)
       - [setup-tailwind](/nx-api/react/generators/setup-tailwind)
       - [setup-ssr](/nx-api/react/generators/setup-ssr)
+      - [federate-module](/nx-api/react/generators/federate-module)
   - [react-native](/nx-api/react-native)
     - [documents](/nx-api/react-native/documents)
       - [Overview](/nx-api/react-native/documents/overview)
diff --git a/e2e/react-core/src/react-module-federation.test.ts b/e2e/react-core/src/react-module-federation.test.ts
index 75f87080b6c8b..922e9b215525a 100644
--- a/e2e/react-core/src/react-module-federation.test.ts
+++ b/e2e/react-core/src/react-module-federation.test.ts
@@ -1,4 +1,4 @@
-import { stripIndents } from '@nx/devkit';
+import { Tree, stripIndents } from '@nx/devkit';
 import {
   checkFilesExist,
   cleanupProject,
@@ -15,11 +15,16 @@ import {
   updateJson,
 } from '@nx/e2e/utils';
 import { join } from 'path';
+import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports';
 
 describe('React Module Federation', () => {
   let proj: string;
+  let tree: Tree;
 
-  beforeAll(() => (proj = newProject()));
+  beforeAll(() => {
+    tree = createTreeWithEmptyWorkspace();
+    proj = newProject();
+  });
 
   afterAll(() => cleanupProject());
 
@@ -382,6 +387,99 @@ describe('React Module Federation', () => {
     }
   }, 500_000);
 
+  // Federate Module
+  describe('Federate Module', () => {
+    it('should federate a module from a library and update an existing remote', async () => {
+      const lib = uniq('lib');
+      const remote = uniq('remote');
+      const module = uniq('module');
+      const host = uniq('host');
+
+      runCLI(
+        `generate @nx/react:host ${host} --remotes=${remote} --no-interactive --projectNameAndRootFormat=as-provided`
+      );
+
+      runCLI(
+        `generate @nx/js:lib ${lib} --no-interactive --projectNameAndRootFormat=as-provided`
+      );
+
+      // Federate Module
+      runCLI(
+        `generate @nx/react:federate-module ${module} --remote=${remote} --path=${lib}/src/index.ts --no-interactive`
+      );
+
+      updateFile(
+        `${lib}/src/index.ts`,
+        `export { default } from './lib/${lib}';`
+      );
+      updateFile(
+        `${lib}/src/lib/${lib}.ts`,
+        `export default function lib() { return 'Hello from ${lib}'; };`
+      );
+
+      // Update Host to use the module
+      updateFile(
+        `${host}/src/app/app.tsx`,
+        `
+      import * as React from 'react';
+      import NxWelcome from './nx-welcome';
+      import { Link, Route, Routes } from 'react-router-dom';
+      
+      import myLib from '${remote}/${module}';
+
+      export function App() {
+        return (
+          <React.Suspense fallback={null}>
+            <div className='remote'>
+            My Remote Library:  { myLib() }
+            </div>
+            <ul>
+              <li>
+                <Link to="/">Home</Link>
+              </li>
+            </ul>
+            <Routes>
+              <Route path="/" element={<NxWelcome title="Host" />} />
+            </Routes>
+          </React.Suspense>
+        );
+      }
+
+      export default App;
+      `
+      );
+
+      // Update e2e test to check the module
+      updateFile(
+        `${host}-e2e/src/e2e/app.cy.ts`,
+        `
+      describe('${host}', () => {
+        beforeEach(() => cy.visit('/'));
+      
+        it('should display contain the remote library', () => {
+          expect(cy.get('div.remote')).to.exist;
+          expect(cy.get('div.remote').contains('My Remote Library: Hello from ${lib}'));
+        });
+      });
+      
+      `
+      );
+
+      // Build host and remote
+      const buildOutput = runCLI(`build ${host}`);
+      const remoteOutput = runCLI(`build ${remote}`);
+
+      expect(buildOutput).toContain('Successfully ran target build');
+      expect(remoteOutput).toContain('Successfully ran target build');
+
+      if (runE2ETests()) {
+        const hostE2eResults = runCLI(`e2e ${host}-e2e --no-watch --verbose`);
+
+        expect(hostE2eResults).toContain('All specs passed!');
+      }
+    }, 500_000);
+  });
+
   function readPort(appName: string): number {
     const config = readJson(join('apps', appName, 'project.json'));
     return config.targets.serve.options.port;
diff --git a/packages/react/generators.json b/packages/react/generators.json
index 857c124deeba1..c19b87080d1d4 100644
--- a/packages/react/generators.json
+++ b/packages/react/generators.json
@@ -102,6 +102,13 @@
       "schema": "./src/generators/setup-ssr/schema.json",
       "description": "Set up SSR configuration for a project.",
       "hidden": false
+    },
+
+    "federate-module": {
+      "factory": "./src/generators/federate-module/federate-module#federateModuleSchematic",
+      "schema": "./src/generators/federate-module/schema.json",
+      "description": "Federate a module.",
+      "hidden": false
     }
   },
   "generators": {
@@ -218,6 +225,13 @@
       "schema": "./src/generators/setup-ssr/schema.json",
       "description": "Set up SSR configuration for a project.",
       "hidden": false
+    },
+
+    "federate-module": {
+      "factory": "./src/generators/federate-module/federate-module#federateModuleGenerator",
+      "schema": "./src/generators/federate-module/schema.json",
+      "description": "Federate a module.",
+      "hidden": false
     }
   }
 }
diff --git a/packages/react/src/generators/federate-module/federate-module.spec.ts b/packages/react/src/generators/federate-module/federate-module.spec.ts
new file mode 100644
index 0000000000000..1ebce3f368103
--- /dev/null
+++ b/packages/react/src/generators/federate-module/federate-module.spec.ts
@@ -0,0 +1,101 @@
+import { Tree, getProjects } from '@nx/devkit';
+import { Schema } from './schema';
+import { Schema as remoteSchma } from '../remote/schema';
+import { federateModuleGenerator } from './federate-module';
+import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports';
+import { Linter } from '@nx/linter';
+import { remoteGeneratorInternal } from '../remote/remote';
+
+describe('federate-module', () => {
+  let tree: Tree;
+  let schema: Schema = {
+    name: 'my-federated-module',
+    remote: 'my-remote',
+    path: 'my-remote/src/my-federated-module.ts',
+    style: 'css',
+  };
+
+  beforeAll(() => {
+    tree = createTreeWithEmptyWorkspace();
+  });
+  describe('no remote', () => {
+    it('should generate a remote and e2e', async () => {
+      await federateModuleGenerator(tree, schema);
+
+      const projects = getProjects(tree);
+
+      expect(projects.get('my-remote').root).toEqual('my-remote');
+      expect(projects.get('my-remote-e2e').root).toEqual('my-remote-e2e');
+    });
+
+    it('should contain an entry for the new path for module federation', async () => {
+      await federateModuleGenerator(tree, schema);
+
+      expect(tree.exists('my-remote/module-federation.config.js')).toBe(true);
+
+      const content = tree.read(
+        'my-remote/module-federation.config.js',
+        'utf-8'
+      );
+      expect(content).toContain(
+        `'./my-federated-module': 'my-remote/src/my-federated-module.ts'`
+      );
+
+      const tsconfig = JSON.parse(tree.read('tsconfig.base.json', 'utf-8'));
+      expect(
+        tsconfig.compilerOptions.paths['my-remote/my-federated-module']
+      ).toEqual(['my-remote/src/my-federated-module.ts']);
+    });
+  });
+
+  describe('with remote', () => {
+    let remoteSchema: remoteSchma = {
+      name: 'my-remote',
+      e2eTestRunner: 'none',
+      skipFormat: false,
+      linter: Linter.EsLint,
+      style: 'css',
+      unitTestRunner: 'none',
+    };
+
+    beforeEach(async () => {
+      remoteSchema.name = uniq('remote');
+      await remoteGeneratorInternal(tree, remoteSchema);
+    });
+
+    it('should append the new path to the module federation config', async () => {
+      let content = tree.read(
+        `${remoteSchema.name}/module-federation.config.js`,
+        'utf-8'
+      );
+
+      expect(content).not.toContain(
+        `'./my-federated-module': 'my-remote/src/my-federated-module.ts'`
+      );
+
+      await federateModuleGenerator(tree, {
+        ...schema,
+        remote: remoteSchema.name,
+      });
+
+      content = tree.read(
+        `${remoteSchema.name}/module-federation.config.js`,
+        'utf-8'
+      );
+      expect(content).toContain(
+        `'./my-federated-module': 'my-remote/src/my-federated-module.ts'`
+      );
+
+      const tsconfig = JSON.parse(tree.read('tsconfig.base.json', 'utf-8'));
+      expect(
+        tsconfig.compilerOptions.paths[
+          `${remoteSchema.name}/my-federated-module`
+        ]
+      ).toEqual(['my-remote/src/my-federated-module.ts']);
+    });
+  });
+});
+
+function uniq(prefix: string) {
+  return `${prefix}${Math.floor(Math.random() * 10000000)}`;
+}
diff --git a/packages/react/src/generators/federate-module/federate-module.ts b/packages/react/src/generators/federate-module/federate-module.ts
new file mode 100644
index 0000000000000..9afb5672821c6
--- /dev/null
+++ b/packages/react/src/generators/federate-module/federate-module.ts
@@ -0,0 +1,79 @@
+import {
+  GeneratorCallback,
+  Tree,
+  convertNxGenerator,
+  formatFiles,
+  logger,
+  readJson,
+  runTasksInSerial,
+} from '@nx/devkit';
+import { Schema } from './schema';
+
+import { remoteGeneratorInternal } from '../remote/remote';
+import { addPathToExposes, checkRemoteExists } from './lib/utils';
+import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
+import { addTsConfigPath, getRootTsConfigPathInTree } from '@nx/js';
+
+export async function federateModuleGenerator(tree: Tree, schema: Schema) {
+  const tasks: GeneratorCallback[] = [];
+  // Check remote exists
+  const remote = checkRemoteExists(tree, schema.remote);
+  const { projectName, projectRoot: remoteRoot } =
+    await determineProjectNameAndRootOptions(tree, {
+      name: schema.remote,
+      projectType: 'application',
+      projectNameAndRootFormat: schema.projectNameAndRootFormat,
+      callingGenerator: '@nx/react:federate-module',
+    });
+
+  if (!remote) {
+    // create remote
+    const remoteGenerator = await remoteGeneratorInternal(tree, {
+      name: schema.remote,
+      e2eTestRunner: schema.e2eTestRunner,
+      skipFormat: schema.skipFormat,
+      linter: schema.linter,
+      style: schema.style,
+      unitTestRunner: schema.unitTestRunner,
+      host: schema.host,
+      projectNameAndRootFormat: schema.projectNameAndRootFormat ?? 'derived',
+    });
+
+    tasks.push(remoteGenerator);
+  }
+
+  const projectRoot = remote ? remote.root : remoteRoot;
+  const remoteName = remote ? remote.name : projectName;
+
+  // add path to exposes property
+  addPathToExposes(tree, projectRoot, schema.name, schema.path);
+
+  // Add new path to tsconfig
+  const rootJSON = readJson(tree, getRootTsConfigPathInTree(tree));
+  if (!rootJSON?.compilerOptions?.paths[`${remoteName}/${schema.name}`]) {
+    addTsConfigPath(tree, `${remoteName}/${schema.name}`, [schema.path]);
+  }
+
+  if (!schema.skipFormat) {
+    await formatFiles(tree);
+  }
+
+  logger.info(
+    `✅️ Updated module federation config.
+    Now you can use the module from your host app like this:
+
+    Static import:
+    import { MyComponent } from '${schema.name}/${remoteName}';
+    
+    Dynamic import:
+    import('${schema.name}/${remoteName}').then((m) => m.${remoteName});
+  `
+  );
+  return runTasksInSerial(...tasks);
+}
+
+export default federateModuleGenerator;
+
+export const federateModuleSchematic = convertNxGenerator(
+  federateModuleGenerator
+);
diff --git a/packages/react/src/generators/federate-module/lib/utils.spec.ts b/packages/react/src/generators/federate-module/lib/utils.spec.ts
new file mode 100644
index 0000000000000..5fbc63312cd80
--- /dev/null
+++ b/packages/react/src/generators/federate-module/lib/utils.spec.ts
@@ -0,0 +1,89 @@
+import * as ts from 'typescript';
+import { updateExposesProperty, createObjectEntry, findExposes } from './utils';
+import { Tree } from '@nx/devkit';
+import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports';
+
+describe('federate-module Utils', () => {
+  let tree: Tree = null;
+
+  beforeAll(() => {
+    tree = createTreeWithEmptyWorkspace();
+  });
+  describe('findExposes', () => {
+    it('should find the exposes object', () => {
+      const fileContent = `
+      module.exports = {
+        name: 'myremote',
+        exposes: {
+            './Module': './src/remote-entry.ts',
+        }
+    };
+    `;
+      const sourceFile = ts.createSourceFile(
+        'module-federation.config.js',
+        fileContent,
+        ts.ScriptTarget.ES2015,
+        true
+      );
+      const exposesObject = findExposes(sourceFile);
+      expect(exposesObject).toBeDefined();
+      expect(exposesObject?.properties.length).toEqual(1);
+    });
+  });
+
+  describe('createObjectEntry', () => {
+    it('should update the exposes object with a new entry', () => {
+      const newEntry = createObjectEntry(
+        'NewModule',
+        './src/new-remote-entry.ts'
+      );
+      expect(newEntry).toBeDefined();
+
+      // Creating a printer to convert AST nodes to string, for safer assertions.
+      const printer = ts.createPrinter();
+      const newEntryText = printer.printNode(
+        ts.EmitHint.Unspecified,
+        newEntry,
+        ts.createSourceFile('', '', ts.ScriptTarget.ES2015)
+      );
+
+      expect(newEntryText).toEqual(
+        `'./NewModule': './src/new-remote-entry.ts'`
+      );
+    });
+  });
+
+  describe('updateExposesProperty', () => {
+    it('should update the exposes object with a new entry', () => {
+      const moduleName = 'NewModule';
+      const modulePath = './src/new-remote-entry.ts';
+      const fileName = 'module-federation.config.js';
+
+      const fileContent = `
+      module.exports = {
+        name: 'myremote',
+        exposes: {
+            './Module': './src/remote-entry.ts',
+        }
+    };
+    `;
+
+      tree.write(fileName, fileContent);
+
+      updateExposesProperty(tree, fileName, moduleName, modulePath);
+      const printer = ts.createPrinter();
+
+      const updatedSource = ts.createSourceFile(
+        fileName,
+        tree.read(fileName).toString(),
+        ts.ScriptTarget.ES2015,
+        true
+      );
+
+      const updatedContent = printer.printFile(updatedSource);
+
+      expect(updatedContent).toContain(moduleName);
+      expect(updatedContent).toContain(modulePath);
+    });
+  });
+});
diff --git a/packages/react/src/generators/federate-module/lib/utils.ts b/packages/react/src/generators/federate-module/lib/utils.ts
new file mode 100644
index 0000000000000..758b9df0ae16c
--- /dev/null
+++ b/packages/react/src/generators/federate-module/lib/utils.ts
@@ -0,0 +1,164 @@
+import { Tree, getProjects, joinPathFragments } from '@nx/devkit';
+
+import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
+import type {
+  SourceFile,
+  ObjectLiteralExpression,
+  Node,
+  PropertyAssignment,
+  TransformerFactory,
+  Visitor,
+} from 'typescript';
+
+let tsModule: typeof import('typescript');
+
+if (!tsModule) {
+  tsModule = ensureTypescript();
+}
+
+/**
+ * Adds a Module Federation path to the exposes property of the module federation config
+ * The assumption here is made the we will only update a TypeScript Module Federation file namely 'module-federation.config.js'
+ * @param tree Tree for the workspace
+ * @param projectPath Project path relative to the workspace
+ * @param moduleName The name of the module to expose
+ * @param modulePath The path to the module to expose (e.g. './src/my-lib/my-lib.ts')
+ */
+export function addPathToExposes(
+  tree: Tree,
+  projectPath: string,
+  moduleName: string,
+  modulePath: string
+) {
+  const moduleFederationConfigPath = joinPathFragments(
+    projectPath,
+    'module-federation.config.js'
+  );
+
+  updateExposesProperty(
+    tree,
+    moduleFederationConfigPath,
+    moduleName,
+    modulePath
+  );
+}
+
+/**
+ * @param tree The workspace tree
+ * @param remoteName The name of the remote to check
+ * @returns Remote ProjectConfig if it exists, false otherwise
+ */
+export function checkRemoteExists(tree: Tree, remoteName: string) {
+  const remote = getRemote(tree, remoteName);
+  if (!remote) return false;
+  const hasModuleFederationConfig = tree.exists(
+    joinPathFragments(remote.root, 'module-federation.config.js')
+  );
+
+  return hasModuleFederationConfig ? remote : false;
+}
+
+export function getRemote(tree: Tree, remoteName: string) {
+  const projects = getProjects(tree);
+  const remote = projects.get(remoteName);
+  return remote;
+}
+
+// Check if the exposes property exists in the AST
+export function findExposes(sourceFile: SourceFile) {
+  let exposesObject: ObjectLiteralExpression | null = null;
+
+  const visit = (node: Node) => {
+    if (
+      tsModule.isPropertyAssignment(node) &&
+      tsModule.isIdentifier(node.name) &&
+      node.name.text === 'exposes' &&
+      tsModule.isObjectLiteralExpression(node.initializer)
+    ) {
+      exposesObject = node.initializer;
+    } else {
+      tsModule.forEachChild(node, visit);
+    }
+  };
+
+  tsModule.forEachChild(sourceFile, visit);
+
+  return exposesObject;
+}
+
+// Create a new property assignment
+export function createObjectEntry(
+  moduleName: string,
+  modulePath: string
+): PropertyAssignment {
+  return tsModule.factory.createPropertyAssignment(
+    tsModule.factory.createStringLiteral(`./${moduleName}`, true),
+    tsModule.factory.createStringLiteral(modulePath, true)
+  );
+}
+
+// Update the exposes property in the AST
+export function updateExposesPropertyinAST(
+  source: SourceFile,
+  exposesObject: ObjectLiteralExpression,
+  newEntry: PropertyAssignment
+) {
+  const updatedExposes = tsModule.factory.updateObjectLiteralExpression(
+    exposesObject,
+    [...exposesObject.properties, newEntry]
+  );
+
+  const transform: TransformerFactory<SourceFile> = (context) => {
+    const visit: Visitor = (node) => {
+      // Comparing nodes indirectly to ensure type compatibility. You must ensure that the nodes are identical.
+      return tsModule.isObjectLiteralExpression(node) && node === exposesObject
+        ? updatedExposes
+        : tsModule.visitEachChild(node, visit, context);
+    };
+    return (node) => tsModule.visitNode(node, visit) as SourceFile;
+  };
+
+  return tsModule.transform<SourceFile>(source, [transform]).transformed[0];
+}
+
+// Write the updated AST to the file (module-federation.config.js)
+export function writeToConfig(
+  tree: Tree,
+  filename: string,
+  source: SourceFile,
+  updatedSourceFile: SourceFile
+) {
+  const printer = tsModule.createPrinter();
+  const update = printer.printNode(
+    tsModule.EmitHint.Unspecified,
+    updatedSourceFile,
+    source
+  );
+  tree.write(filename, update);
+}
+
+export function updateExposesProperty(
+  tree: Tree,
+  filename: string,
+  moduleName: string,
+  modulePath: string
+) {
+  const fileContent = tree.read(filename).toString();
+  const source = tsModule.createSourceFile(
+    filename,
+    fileContent,
+    tsModule.ScriptTarget.ES2015,
+    true
+  );
+
+  const exposesObject = findExposes(source);
+  if (!exposesObject) return;
+
+  const newEntry = createObjectEntry(moduleName, modulePath);
+  const updatedSourceFile = updateExposesPropertyinAST(
+    source,
+    exposesObject,
+    newEntry
+  );
+  writeToConfig(tree, filename, source, updatedSourceFile);
+}
diff --git a/packages/react/src/generators/federate-module/schema.d.ts b/packages/react/src/generators/federate-module/schema.d.ts
new file mode 100644
index 0000000000000..2877e2891d9da
--- /dev/null
+++ b/packages/react/src/generators/federate-module/schema.d.ts
@@ -0,0 +1,12 @@
+export interface Schema {
+  name: string;
+  path: string;
+  remote: string;
+  projectNameAndRootFormat?: ProjectNameAndRootFormat;
+  e2eTestRunner?: 'cypress' | 'none';
+  host?: string;
+  linter?: Linter;
+  skipFormat?: boolean;
+  style?: SupportedStyles;
+  unitTestRunner?: 'jest' | 'vitest' | 'none';
+}
diff --git a/packages/react/src/generators/federate-module/schema.json b/packages/react/src/generators/federate-module/schema.json
new file mode 100644
index 0000000000000..ebeda7758cc60
--- /dev/null
+++ b/packages/react/src/generators/federate-module/schema.json
@@ -0,0 +1,116 @@
+{
+  "$schema": "http://json-schema.org/schema",
+  "cli": "nx",
+  "$id": "NxReactFederateModule",
+  "title": "Federate Module",
+  "description": "Create a federated module, which can be loaded by a remote host.",
+  "examples": [
+    {
+      "command": "nx g federate-module MyModule --path=./src/component/my-cmp.ts --remote=my-remote-app",
+      "description": "Create a federated module from my-remote-app, that exposes my-cmp from ./src/component/my-cmp.ts as MyModule."
+    }
+  ],
+  "type": "object",
+  "properties": {
+    "name": {
+      "description": "The name of the module.",
+      "type": "string",
+      "$default": {
+        "$source": "argv",
+        "index": 0
+      },
+      "x-prompt": "What name would you like to use for the module?",
+      "pattern": "^[a-zA-Z][^:]*$",
+      "x-priority": "important"
+    },
+    "path": {
+      "type": "string",
+      "description": "The path to locate the federated module.",
+      "x-prompt": "What is the path to the module to be federated?"
+    },
+    "remote": {
+      "type": "string",
+      "description": "The name of the remote.",
+      "x-prompt": "What is/should the remote be named?"
+    },
+    "projectNameAndRootFormat": {
+      "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
+      "type": "string",
+      "enum": ["as-provided", "derived"]
+    },
+    "style": {
+      "description": "The file extension to be used for style files.",
+      "type": "string",
+      "default": "css",
+      "alias": "s",
+      "x-prompt": {
+        "message": "Which stylesheet format would you like to use?",
+        "type": "list",
+        "items": [
+          {
+            "value": "css",
+            "label": "CSS"
+          },
+          {
+            "value": "scss",
+            "label": "SASS(.scss)       [ http://sass-lang.com                     ]"
+          },
+          {
+            "value": "less",
+            "label": "LESS              [ http://lesscss.org                       ]"
+          },
+          {
+            "value": "styled-components",
+            "label": "styled-components [ https://styled-components.com            ]"
+          },
+          {
+            "value": "@emotion/styled",
+            "label": "emotion           [ https://emotion.sh                       ]"
+          },
+          {
+            "value": "styled-jsx",
+            "label": "styled-jsx        [ https://www.npmjs.com/package/styled-jsx ]"
+          },
+          {
+            "value": "styl",
+            "label": "DEPRECATD: Stylus(.styl) [ http://stylus-lang.com            ]"
+          },
+          {
+            "value": "none",
+            "label": "None"
+          }
+        ]
+      }
+    },
+    "linter": {
+      "description": "The tool to use for running lint checks.",
+      "type": "string",
+      "enum": ["eslint"],
+      "default": "eslint"
+    },
+    "skipFormat": {
+      "description": "Skip formatting files.",
+      "type": "boolean",
+      "default": false,
+      "x-priority": "internal"
+    },
+    "unitTestRunner": {
+      "type": "string",
+      "enum": ["jest", "none"],
+      "description": "Test runner to use for unit tests.",
+      "default": "jest"
+    },
+    "e2eTestRunner": {
+      "type": "string",
+      "enum": ["cypress", "none"],
+      "description": "Test runner to use for end to end (e2e) tests.",
+      "default": "cypress"
+    },
+    "host": {
+      "type": "string",
+      "description": "The host / shell application for this remote."
+    }
+  },
+  "required": ["name", "path", "remote"],
+  "additionalProperties": false
+}
diff --git a/packages/react/src/generators/remote/remote.ts b/packages/react/src/generators/remote/remote.ts
index f3cd4c30dc64b..78b7dc935ec1e 100644
--- a/packages/react/src/generators/remote/remote.ts
+++ b/packages/react/src/generators/remote/remote.ts
@@ -71,7 +71,7 @@ export async function remoteGeneratorInternal(host: Tree, schema: Schema) {
   const tasks: GeneratorCallback[] = [];
   const options: NormalizedSchema<Schema> = {
     ...(await normalizeOptions<Schema>(host, schema, '@nx/react:remote')),
-    typescriptConfiguration: schema.typescriptConfiguration ?? true,
+    typescriptConfiguration: schema.typescriptConfiguration ?? false,
   };
   const initAppTask = await applicationGenerator(host, {
     ...options,
diff --git a/packages/react/src/module-federation/with-module-federation.ts b/packages/react/src/module-federation/with-module-federation.ts
index c58b05d1220a1..6c705cfe0a986 100644
--- a/packages/react/src/module-federation/with-module-federation.ts
+++ b/packages/react/src/module-federation/with-module-federation.ts
@@ -19,7 +19,6 @@ export async function withModuleFederation(
 
     if (options.library?.type === 'var') {
       config.output.scriptType = 'text/javascript';
-      config.experiments.outputModule = false;
     }
 
     config.optimization = {
@@ -28,7 +27,7 @@ export async function withModuleFederation(
 
     config.experiments = {
       ...config.experiments,
-      outputModule: true,
+      outputModule: !(options.library?.type === 'var'),
     };
 
     config.plugins.push(