feat(react): Add federate-module generator (#19286)
This commit is contained in:
parent
bda907ca77
commit
8ccd88cfcb
@ -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,
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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",
|
||||
|
||||
118
docs/generated/packages/react/generators/federate-module.json
Normal file
118
docs/generated/packages/react/generators/federate-module.json
Normal file
@ -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"
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)}`;
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
164
packages/react/src/generators/federate-module/lib/utils.ts
Normal file
164
packages/react/src/generators/federate-module/lib/utils.ts
Normal file
@ -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);
|
||||
}
|
||||
12
packages/react/src/generators/federate-module/schema.d.ts
vendored
Normal file
12
packages/react/src/generators/federate-module/schema.d.ts
vendored
Normal file
@ -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';
|
||||
}
|
||||
116
packages/react/src/generators/federate-module/schema.json
Normal file
116
packages/react/src/generators/federate-module/schema.json
Normal file
@ -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
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user