From d19ffdec07c313f2485ec70f573143a09600f4e7 Mon Sep 17 00:00:00 2001 From: Nicholas Cunningham 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 | 157 ++++++++++++++++++ .../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, 815 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 cc9b078576a043..f21b8b619b59b5 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -8074,6 +8074,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 65548e9928ede4..0b62ce3f011407 100644 --- a/docs/generated/manifests/nx-api.json +++ b/docs/generated/manifests/nx-api.json @@ -2182,6 +2182,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 3075e1de3e9936..9e82dcaa9f9927 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -2158,6 +2158,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 00000000000000..259edc833a1273 --- /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/draft-04/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 called my-remote-app, that exposes my-cmp from ./src/component/my-cmp.ts. called 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][^:]*$" + }, + "path": { + "type": "string", + "description": "The relative path from the remote root to the module to be federated.", + "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.", + "x-priority": "important" + } + }, + "required": ["name"], + "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 ee92c033b03448..a4e65a91787402 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -559,6 +559,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 75f87080b6c8bd..922e9b215525af 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 ( + +
+ My Remote Library: { myLib() } +
+ + + } /> + +
+ ); + } + + 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 857c124deeba1e..c19b87080d1d45 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 00000000000000..1ebce3f3681031 --- /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 00000000000000..9afb5672821c62 --- /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 00000000000000..5fbc63312cd806 --- /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 00000000000000..eda58b50f43f22 --- /dev/null +++ b/packages/react/src/generators/federate-module/lib/utils.ts @@ -0,0 +1,157 @@ +import { Tree, getProjects, joinPathFragments } from '@nx/devkit'; + +import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; +import * as ts 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: ts.SourceFile) { + let exposesObject: ts.ObjectLiteralExpression | null = null; + + const visit = (node: ts.Node) => { + if ( + ts.isPropertyAssignment(node) && + ts.isIdentifier(node.name) && + node.name.text === 'exposes' && + ts.isObjectLiteralExpression(node.initializer) + ) { + exposesObject = node.initializer; + } else { + ts.forEachChild(node, visit); + } + }; + + ts.forEachChild(sourceFile, visit); + + return exposesObject; +} + +// Create a new property assignment +export function createObjectEntry( + moduleName: string, + modulePath: string +): ts.PropertyAssignment { + return ts.factory.createPropertyAssignment( + ts.factory.createStringLiteral(`./${moduleName}`, true), + ts.factory.createStringLiteral(modulePath, true) + ); +} + +// Update the exposes property in the AST +export function updateExposesPropertyinAST( + source: ts.SourceFile, + exposesObject: ts.ObjectLiteralExpression, + newEntry: ts.PropertyAssignment +) { + const updatedExposes = ts.factory.updateObjectLiteralExpression( + exposesObject, + [...exposesObject.properties, newEntry] + ); + + const transform: ts.TransformerFactory = (context) => { + const visit: ts.Visitor = (node) => { + // Comparing nodes indirectly to ensure type compatibility. You must ensure that the nodes are identical. + return ts.isObjectLiteralExpression(node) && node === exposesObject + ? updatedExposes + : ts.visitEachChild(node, visit, context); + }; + return (node) => ts.visitNode(node, visit) as ts.SourceFile; + }; + + return ts.transform(source, [transform]).transformed[0]; +} + +// Write the updated AST to the file (module-federation.config.js) +export function writeToConfig( + tree: Tree, + filename: string, + source: ts.SourceFile, + updatedSourceFile: ts.SourceFile +) { + const printer = tsModule.createPrinter(); + const update = printer.printNode( + ts.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 00000000000000..2877e2891d9da6 --- /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 00000000000000..d97e60809f436d --- /dev/null +++ b/packages/react/src/generators/federate-module/schema.json @@ -0,0 +1,116 @@ +{ + "$schema": "http://json-schema.org/draft-04/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 called my-remote-app, that exposes my-cmp from ./src/component/my-cmp.ts. called 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][^:]*$" + }, + "path": { + "type": "string", + "description": "The relative path from the remote root to the module to be federated.", + "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.", + "x-priority": "important" + } + }, + "required": ["name"], + "additionalProperties": false +} diff --git a/packages/react/src/generators/remote/remote.ts b/packages/react/src/generators/remote/remote.ts index f3cd4c30dc64bb..78b7dc935ec1eb 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 = { ...(await normalizeOptions(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 c58b05d1220a1a..6c705cfe0a9869 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(