From ecaca466a2f2af56a4befd6f238f295d4f6ae9e7 Mon Sep 17 00:00:00 2001 From: Nicholas Cunningham Date: Sat, 19 Mar 2022 12:55:45 -0600 Subject: [PATCH] feat(react): add Generator for MFE Host --- packages/react/generators.json | 6 + packages/react/index.ts | 1 + .../plugins/with-module-federation/index.ts | 139 +++++++++++++++ .../mfe-host/files/common/.babelrc__tmpl__ | 13 ++ .../files/common/.browserslistrc__tmpl__ | 16 ++ .../src/app/__fileName__.spec.tsx__tmpl__ | 26 +++ .../mfe-host/files/common/src/assets/.gitkeep | 0 .../environments/environment.prod.ts__tmpl__ | 3 + .../src/environments/environment.ts__tmpl__ | 6 + .../mfe-host/files/common/src/favicon.ico | Bin 0 -> 15086 bytes .../mfe-host/files/common/src/index.html | 14 ++ .../files/common/src/polyfills.ts__tmpl__ | 7 + .../files/common/tsconfig.app.json__tmpl__ | 14 ++ .../files/common/tsconfig.json__tmpl__ | 17 ++ .../mfe-host/files/mfe/mfe.config.js__tmpl__ | 6 + .../files/mfe/src/bootstrap.tsx__tmpl__ | 7 + .../mfe-host/files/mfe/src/main.tsx__tmpl__ | 1 + .../files/mfe/src/remotes.d.ts__tmpl__ | 4 + .../files/mfe/webpack.config.js__tmpl__ | 6 + .../files/mfe/webpack.config.prod.js__tmpl__ | 1 + .../src/generators/mfe-host/lib/add-mfe.ts | 25 +++ .../generators/mfe-host/mfe-host.compact.ts | 0 .../react/src/generators/mfe-host/mfe-host.ts | 35 ++++ .../react/src/generators/mfe-host/schema.d.ts | 29 ++++ .../react/src/generators/mfe-host/schema.json | 161 ++++++++++++++++++ 25 files changed, 537 insertions(+) create mode 100644 packages/react/plugins/with-module-federation/index.ts create mode 100644 packages/react/src/generators/mfe-host/files/common/.babelrc__tmpl__ create mode 100644 packages/react/src/generators/mfe-host/files/common/.browserslistrc__tmpl__ create mode 100644 packages/react/src/generators/mfe-host/files/common/src/app/__fileName__.spec.tsx__tmpl__ create mode 100644 packages/react/src/generators/mfe-host/files/common/src/assets/.gitkeep create mode 100644 packages/react/src/generators/mfe-host/files/common/src/environments/environment.prod.ts__tmpl__ create mode 100644 packages/react/src/generators/mfe-host/files/common/src/environments/environment.ts__tmpl__ create mode 100644 packages/react/src/generators/mfe-host/files/common/src/favicon.ico create mode 100644 packages/react/src/generators/mfe-host/files/common/src/index.html create mode 100644 packages/react/src/generators/mfe-host/files/common/src/polyfills.ts__tmpl__ create mode 100644 packages/react/src/generators/mfe-host/files/common/tsconfig.app.json__tmpl__ create mode 100644 packages/react/src/generators/mfe-host/files/common/tsconfig.json__tmpl__ create mode 100644 packages/react/src/generators/mfe-host/files/mfe/mfe.config.js__tmpl__ create mode 100644 packages/react/src/generators/mfe-host/files/mfe/src/bootstrap.tsx__tmpl__ create mode 100644 packages/react/src/generators/mfe-host/files/mfe/src/main.tsx__tmpl__ create mode 100644 packages/react/src/generators/mfe-host/files/mfe/src/remotes.d.ts__tmpl__ create mode 100644 packages/react/src/generators/mfe-host/files/mfe/webpack.config.js__tmpl__ create mode 100644 packages/react/src/generators/mfe-host/files/mfe/webpack.config.prod.js__tmpl__ create mode 100644 packages/react/src/generators/mfe-host/lib/add-mfe.ts create mode 100644 packages/react/src/generators/mfe-host/mfe-host.compact.ts create mode 100644 packages/react/src/generators/mfe-host/mfe-host.ts create mode 100644 packages/react/src/generators/mfe-host/schema.d.ts create mode 100644 packages/react/src/generators/mfe-host/schema.json diff --git a/packages/react/generators.json b/packages/react/generators.json index 2d766fd3b8e85d..9d364fd4f97cd3 100644 --- a/packages/react/generators.json +++ b/packages/react/generators.json @@ -162,6 +162,12 @@ "schema": "./src/generators/hook/schema.json", "description": "Create a hook", "aliases": "c" + }, + + "mfe-host": { + "factory": "./src/generators/mfe-host/mfe-host#mfeHostGenerator", + "schema": "./src/generators/mfe-host/schema.json", + "description": "Generate a Host React Application" } } } diff --git a/packages/react/index.ts b/packages/react/index.ts index 2aec4830f43f99..e9c6a07620353b 100644 --- a/packages/react/index.ts +++ b/packages/react/index.ts @@ -16,4 +16,5 @@ export { reduxGenerator } from './src/generators/redux/redux'; export { storiesGenerator } from './src/generators/stories/stories'; export { storybookConfigurationGenerator } from './src/generators/storybook-configuration/configuration'; export { storybookMigration5to6Generator } from './src/generators/storybook-migrate-defaults-5-to-6/migrate-defaults-5-to-6'; +export { mfeHostGenerator } from './src/generators/mfe-host/mfe-host'; export type { SupportedStyles } from './typings/style'; diff --git a/packages/react/plugins/with-module-federation/index.ts b/packages/react/plugins/with-module-federation/index.ts new file mode 100644 index 00000000000000..92f9aa1657a1f1 --- /dev/null +++ b/packages/react/plugins/with-module-federation/index.ts @@ -0,0 +1,139 @@ +import { readCachedProjectGraph } from '@nrwl/workspace/src/core/project-graph'; +import { readWorkspaceJson } from '@nrwl/workspace/src/core/file-utils'; +import { + ProjectGraph, + ProjectGraphDependency, +} from '@nrwl/tao/src/shared/project-graph'; +import { WorkspaceJsonConfiguration } from '@nrwl/tao/src/shared/workspace'; + +const { ModuleFederationPlugin } = require('webpack').container; +const reactWebpackConfig = require('../webpack'); + +export type MFERemotes = string[] | [remoteName: string, remoteUrl: string][]; + +export type MFELibrary = { type: string; name: string }; + +export interface SharedLibraryConfig { + singleton?: boolean; + strictVersion?: boolean; + requiredVersion?: string; + eager?: boolean; +} + +export interface MfeOptions { + name: string; + filename: string; + remotes?: string[]; + library?: MFELibrary; + exposes?: Record; + shared?: ( + library: string, + config: SharedLibraryConfig + ) => undefined | false | SharedLibraryConfig; +} + +function getSharedDependencies(graph: ProjectGraph, options: MfeOptions) { + const dependencies = graph.dependencies[options.name]; + const sharedDependencies = dependencies.reduce((acc, dep) => { + //npm libs + if (graph.externalNodes[dep.target]) { + const externalNode = graph.externalNodes[dep.target]; + acc[externalNode.data.packageName] = { + singleton: true, + requiredVersion: externalNode.data.version, + }; + } + + // workspace libs + if (graph.nodes[dep.target]) { + const node = graph.nodes[dep.target]; + if (node.data.projectType !== 'application') { + acc[node.name] = { + requiredVersion: false, + }; + } + } + return acc; + }, {}); + + if (options.shared) { + for (const [library, config] of Object.entries(sharedDependencies)) { + const shouldKeepDependency = options.shared(library, config); + if (shouldKeepDependency == false) { + delete sharedDependencies[library]; + continue; + } + sharedDependencies[library] = shouldKeepDependency ?? config; + } + } + return sharedDependencies; +} + +function getRemotes(options: MfeOptions, ws: WorkspaceJsonConfiguration) { + return options.remotes.reduce((acc, name) => { + const project = ws.projects[name]; + + if (!project) { + throw Error( + `Cannot find remote project "${options.name}". Check that the name is correct in mfe.config.js` + ); + } + const serveOptions = project?.targets?.serve.options; + if (serveOptions) { + acc[name] = `${name}@//${serveOptions.host ?? 'localhost'}:${ + serveOptions.port ?? 4200 + }/remoteEntry.js`; + } + return acc; + }, {}); +} + +// This is probably something linked to a Schema +function withModuleFederation(options: MfeOptions) { + const ws = readWorkspaceJson(); + const graph = readCachedProjectGraph(); + const project = ws.projects[options.name]; + + if (!project) { + throw Error( + `Cannot find project "${options.name}". Check that the name is correct in mfe.config.js` + ); + } + + const mfeOptions = { + name: options.name, + filename: 'remoteEntry.js', + exposes: {}, + } as any; + + if (options.library) { + mfeOptions.library = options.library; + } + + mfeOptions.shared = getSharedDependencies(graph, options); + + if (options.remotes) { + mfeOptions.remotes = getRemotes(options, ws); + } + + if (options.exposes) { + mfeOptions.exposes = options.exposes; + } + + return (config, options) => { + config = reactWebpackConfig(config); + config.output.uniqueName = options.name; + config.output.publicPath = 'auto'; + + config.optimization = { + runtimeChunk: false, + minimize: false, + }; + + config.plugins.push(new ModuleFederationPlugin(mfeOptions)); + + return config; + }; +} + +module.exports = withModuleFederation; diff --git a/packages/react/src/generators/mfe-host/files/common/.babelrc__tmpl__ b/packages/react/src/generators/mfe-host/files/common/.babelrc__tmpl__ new file mode 100644 index 00000000000000..626dc3ed79947d --- /dev/null +++ b/packages/react/src/generators/mfe-host/files/common/.babelrc__tmpl__ @@ -0,0 +1,13 @@ +{ + "presets": [ + [ + "@nrwl/react/babel", { + "runtime": "automatic"<% if (style === '@emotion/styled') { %>,<% } %> + <% if (style === '@emotion/styled') { %>"importSource": "@emotion/react" <% } %> + } + ] + ], + "plugins": [ + <% if (style === 'styled-components') { %>["styled-components", { "pure": true, "ssr": true }]<% } %><% if (style === 'styled-jsx') { %>"styled-jsx/babel"<% } %><% if (style === '@emotion/styled') { %>"@emotion/babel-plugin"<% } %> + ] +} diff --git a/packages/react/src/generators/mfe-host/files/common/.browserslistrc__tmpl__ b/packages/react/src/generators/mfe-host/files/common/.browserslistrc__tmpl__ new file mode 100644 index 00000000000000..f1d12df4faa25a --- /dev/null +++ b/packages/react/src/generators/mfe-host/files/common/.browserslistrc__tmpl__ @@ -0,0 +1,16 @@ +# This file is used by: +# 1. autoprefixer to adjust CSS to support the below specified browsers +# 2. babel preset-env to adjust included polyfills +# +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries +# +# If you need to support different browsers in production, you may tweak the list below. + +last 1 Chrome version +last 1 Firefox version +last 2 Edge major versions +last 2 Safari major version +last 2 iOS major versions +Firefox ESR +not IE 9-11 # For IE 9-11 support, remove 'not'. \ No newline at end of file diff --git a/packages/react/src/generators/mfe-host/files/common/src/app/__fileName__.spec.tsx__tmpl__ b/packages/react/src/generators/mfe-host/files/common/src/app/__fileName__.spec.tsx__tmpl__ new file mode 100644 index 00000000000000..cb810d67d8c663 --- /dev/null +++ b/packages/react/src/generators/mfe-host/files/common/src/app/__fileName__.spec.tsx__tmpl__ @@ -0,0 +1,26 @@ +import { render } from '@testing-library/react'; +<% if (routing) { %> +import { BrowserRouter } from 'react-router-dom'; +<% } %> + +import App from './app'; + +describe('App', () => { + it('should render successfully', () => { + <% if (routing) { %> + const { baseElement } = render(); + <% } else { %> + const { baseElement } = render(); + <% } %> + expect(baseElement).toBeTruthy(); + }); + + it('should have a greeting as the title', () => { + <% if (routing) { %> + const { getByText } = render(); + <% } else { %> + const { getByText } = render(); + <% } %> + expect(getByText(/Welcome <%= projectName %>/gi)).toBeTruthy(); + }); +}); diff --git a/packages/react/src/generators/mfe-host/files/common/src/assets/.gitkeep b/packages/react/src/generators/mfe-host/files/common/src/assets/.gitkeep new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/packages/react/src/generators/mfe-host/files/common/src/environments/environment.prod.ts__tmpl__ b/packages/react/src/generators/mfe-host/files/common/src/environments/environment.prod.ts__tmpl__ new file mode 100644 index 00000000000000..3612073bc31cd4 --- /dev/null +++ b/packages/react/src/generators/mfe-host/files/common/src/environments/environment.prod.ts__tmpl__ @@ -0,0 +1,3 @@ +export const environment = { + production: true +}; diff --git a/packages/react/src/generators/mfe-host/files/common/src/environments/environment.ts__tmpl__ b/packages/react/src/generators/mfe-host/files/common/src/environments/environment.ts__tmpl__ new file mode 100644 index 00000000000000..d9370e924b51bc --- /dev/null +++ b/packages/react/src/generators/mfe-host/files/common/src/environments/environment.ts__tmpl__ @@ -0,0 +1,6 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// When building for production, this file is replaced with `environment.prod.ts`. + +export const environment = { + production: false +}; diff --git a/packages/react/src/generators/mfe-host/files/common/src/favicon.ico b/packages/react/src/generators/mfe-host/files/common/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..317ebcb2336e0833a22dddf0ab287849f26fda57 GIT binary patch literal 15086 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 + + + + <%= className %> + + + + + + +
+ + diff --git a/packages/react/src/generators/mfe-host/files/common/src/polyfills.ts__tmpl__ b/packages/react/src/generators/mfe-host/files/common/src/polyfills.ts__tmpl__ new file mode 100644 index 00000000000000..2adf3d05b6fcf4 --- /dev/null +++ b/packages/react/src/generators/mfe-host/files/common/src/polyfills.ts__tmpl__ @@ -0,0 +1,7 @@ +/** + * Polyfill stable language features. These imports will be optimized by `@babel/preset-env`. + * + * See: https://github.com/zloirock/core-js#babel + */ +import 'core-js/stable'; +import 'regenerator-runtime/runtime'; diff --git a/packages/react/src/generators/mfe-host/files/common/tsconfig.app.json__tmpl__ b/packages/react/src/generators/mfe-host/files/common/tsconfig.app.json__tmpl__ new file mode 100644 index 00000000000000..1e4a14aded7d2a --- /dev/null +++ b/packages/react/src/generators/mfe-host/files/common/tsconfig.app.json__tmpl__ @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "<%= offsetFromRoot %>dist/out-tsc", + "types": ["node"] + }, + "files": [ + <% if (style === 'styled-jsx') { %>"<%= offsetFromRoot %>node_modules/@nrwl/react/typings/styled-jsx.d.ts",<% } %> + "<%= offsetFromRoot %>node_modules/@nrwl/react/typings/cssmodule.d.ts", + "<%= offsetFromRoot %>node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": ["**/*.spec.ts", "**/*.test.ts", "**/*.spec.tsx", "**/*.test.tsx", "**/*.spec.js", "**/*.test.js", "**/*.spec.jsx", "**/*.test.jsx"], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/packages/react/src/generators/mfe-host/files/common/tsconfig.json__tmpl__ b/packages/react/src/generators/mfe-host/files/common/tsconfig.json__tmpl__ new file mode 100644 index 00000000000000..877486407b8df1 --- /dev/null +++ b/packages/react/src/generators/mfe-host/files/common/tsconfig.json__tmpl__ @@ -0,0 +1,17 @@ +{ + "extends": "<%= offsetFromRoot %>tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + <% if (style === '@emotion/styled') { %>"jsxImportSource": "@emotion/react",<% } %> + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/packages/react/src/generators/mfe-host/files/mfe/mfe.config.js__tmpl__ b/packages/react/src/generators/mfe-host/files/mfe/mfe.config.js__tmpl__ new file mode 100644 index 00000000000000..02954642b2e6e1 --- /dev/null +++ b/packages/react/src/generators/mfe-host/files/mfe/mfe.config.js__tmpl__ @@ -0,0 +1,6 @@ +module.exports = { + name: '<%= projectName %>', + remotes: [ + <% remotes.forEach(function(remote) {%> "<%= remote %>", <% }); %> + ], +} \ No newline at end of file diff --git a/packages/react/src/generators/mfe-host/files/mfe/src/bootstrap.tsx__tmpl__ b/packages/react/src/generators/mfe-host/files/mfe/src/bootstrap.tsx__tmpl__ new file mode 100644 index 00000000000000..564a677ba41441 --- /dev/null +++ b/packages/react/src/generators/mfe-host/files/mfe/src/bootstrap.tsx__tmpl__ @@ -0,0 +1,7 @@ +<% if (strict) { %>import { StrictMode } from 'react';<% } %> +import * as ReactDOM from 'react-dom'; +<% if (routing) { %>import { BrowserRouter } from 'react-router-dom';<% } %> + +import App from './app/<%= fileName %>'; + +ReactDOM.render(<% if (strict) { %><% } %><% if (routing) { %><% } %><% if (routing) { %><% } %><% if (strict) { %><% } %>, document.getElementById('root')); diff --git a/packages/react/src/generators/mfe-host/files/mfe/src/main.tsx__tmpl__ b/packages/react/src/generators/mfe-host/files/mfe/src/main.tsx__tmpl__ new file mode 100644 index 00000000000000..137c64f9f44756 --- /dev/null +++ b/packages/react/src/generators/mfe-host/files/mfe/src/main.tsx__tmpl__ @@ -0,0 +1 @@ +import('./bootstrap'); \ No newline at end of file diff --git a/packages/react/src/generators/mfe-host/files/mfe/src/remotes.d.ts__tmpl__ b/packages/react/src/generators/mfe-host/files/mfe/src/remotes.d.ts__tmpl__ new file mode 100644 index 00000000000000..0782e2e2e11c01 --- /dev/null +++ b/packages/react/src/generators/mfe-host/files/mfe/src/remotes.d.ts__tmpl__ @@ -0,0 +1,4 @@ + // Declare your remote Modules here + // Example declare module 'about/Module'; + + <% remotes.forEach(function(remote) { %>declare module '<%= remote %>/Module';<% }); %> \ No newline at end of file diff --git a/packages/react/src/generators/mfe-host/files/mfe/webpack.config.js__tmpl__ b/packages/react/src/generators/mfe-host/files/mfe/webpack.config.js__tmpl__ new file mode 100644 index 00000000000000..c8782b572c3170 --- /dev/null +++ b/packages/react/src/generators/mfe-host/files/mfe/webpack.config.js__tmpl__ @@ -0,0 +1,6 @@ +const withModuleFederation = require('@nrwl/react/plugins/with-module-federation'); +const mfeConfig = require('./mfe.config'); + +module.exports = withModuleFederation({ + ...mfeConfig, +}); \ No newline at end of file diff --git a/packages/react/src/generators/mfe-host/files/mfe/webpack.config.prod.js__tmpl__ b/packages/react/src/generators/mfe-host/files/mfe/webpack.config.prod.js__tmpl__ new file mode 100644 index 00000000000000..bbf8e1f9d4c69b --- /dev/null +++ b/packages/react/src/generators/mfe-host/files/mfe/webpack.config.prod.js__tmpl__ @@ -0,0 +1 @@ +module.exports = require('./webpack.config'); \ No newline at end of file diff --git a/packages/react/src/generators/mfe-host/lib/add-mfe.ts b/packages/react/src/generators/mfe-host/lib/add-mfe.ts new file mode 100644 index 00000000000000..cebaac8e3f2cf5 --- /dev/null +++ b/packages/react/src/generators/mfe-host/lib/add-mfe.ts @@ -0,0 +1,25 @@ +import { NormalizedSchema } from '@nrwl/react/src/generators/application/schema'; +import { generateFiles, names } from '@nrwl/devkit'; +import { join } from 'path'; + +export function addMFEFiles(host, options: NormalizedSchema) { + if (host.exists(`apps/${options.projectName}/src/main.tsx`)) { + host.rename( + `apps/${options.projectName}/src/main.tsx`, + `apps/${options.projectName}/src/bootstrap.tsx` + ); + } + + const templateVariables = { + ...names(options.name), + ...options, + tmpl: '', + }; + + generateFiles( + host, + join(__dirname, `../files/mfe`), + options.appProjectRoot, + templateVariables + ); +} diff --git a/packages/react/src/generators/mfe-host/mfe-host.compact.ts b/packages/react/src/generators/mfe-host/mfe-host.compact.ts new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/packages/react/src/generators/mfe-host/mfe-host.ts b/packages/react/src/generators/mfe-host/mfe-host.ts new file mode 100644 index 00000000000000..7fbd0643b131a0 --- /dev/null +++ b/packages/react/src/generators/mfe-host/mfe-host.ts @@ -0,0 +1,35 @@ +import { + formatFiles, + generateFiles, + names, + Tree, + updateJson, +} from '@nrwl/devkit'; +import { Schema } from './schema'; +import applicationGenerator from '../application/application'; +import { normalizeOptions } from '@nrwl/react/src/generators/application/lib/normalize-options'; +import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; +import { addMFEFiles } from '@nrwl/react/src/generators/mfe-host/lib/add-mfe'; +import { NormalizedSchema } from '@nrwl/react/src/generators/application/schema'; + +export function updateProjectJson(host: Tree, options: NormalizedSchema) { + updateJson(host, `${options.appProjectRoot}/project.json`, (json) => { + json.targets.build.options[ + 'webpackConfig' + ] = `${options.appProjectRoot}/webpack.config.js`; + return json; + }); +} + +export async function mfeHostGenerator(host: Tree, schema: Schema) { + const options = normalizeOptions(host, schema); + const initApp = await applicationGenerator(host, options); + addMFEFiles(host, options); + updateProjectJson(host, options); + + if (!options.skipFormat) { + await formatFiles(host); + } + + return runTasksInSerial(initApp); +} diff --git a/packages/react/src/generators/mfe-host/schema.d.ts b/packages/react/src/generators/mfe-host/schema.d.ts new file mode 100644 index 00000000000000..c158e0db7138f8 --- /dev/null +++ b/packages/react/src/generators/mfe-host/schema.d.ts @@ -0,0 +1,29 @@ +import { SupportedStyles } from '@nrwl/react'; +import { Linter } from '@nrwl/linter'; + +export interface Schema { + name: string; + style: SupportedStyles; + skipFormat: boolean; + directory?: string; + tags?: string; + unitTestRunner: 'jest' | 'none'; + /** + * @deprecated + */ + babelJest?: boolean; + e2eTestRunner: 'cypress' | 'none'; + linter: Linter; + pascalCaseFiles?: boolean; + classComponent?: boolean; + routing?: boolean; + skipWorkspaceJson?: boolean; + js?: boolean; + globalCss?: boolean; + strict?: boolean; + setParserOptionsProject?: boolean; + standaloneConfig?: boolean; + compiler?: 'babel' | 'swc'; + port?: number; + remotes?: string[]; +} diff --git a/packages/react/src/generators/mfe-host/schema.json b/packages/react/src/generators/mfe-host/schema.json new file mode 100644 index 00000000000000..3c48b477d71084 --- /dev/null +++ b/packages/react/src/generators/mfe-host/schema.json @@ -0,0 +1,161 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "GeneratorReactMFEHost", + "cli": "nx", + "title": "Generate Module Federation Setup for React Host App", + "description": "Create Module Federation configuration files for given React Host Application.", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the host application to generate the Module Federation configuration", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What name would you like to use as the host application?", + "pattern": "^[a-zA-Z].*$" + }, + "directory": { + "description": "The directory of the new application.", + "type": "string", + "alias": "dir" + }, + "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": "styl", + "label": "Stylus(.styl) [ http://stylus-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": "none", + "label": "None" + } + ] + } + }, + "linter": { + "description": "The tool to use for running lint checks.", + "type": "string", + "enum": ["eslint", "tslint"], + "default": "eslint" + }, + "routing": { + "type": "boolean", + "description": "Generate application with routes.", + "x-prompt": "Would you like to add React Router to this application?", + "default": false + }, + "skipFormat": { + "description": "Skip formatting files.", + "type": "boolean", + "default": false + }, + "skipWorkspaceJson": { + "description": "Skip updating workspace.json with default options based on values provided to this app (e.g. babel, style).", + "type": "boolean", + "default": false + }, + "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" + }, + "tags": { + "type": "string", + "description": "Add tags to the application (used for linting).", + "alias": "t" + }, + "pascalCaseFiles": { + "type": "boolean", + "description": "Use pascal case component file name (e.g. App.tsx).", + "alias": "P", + "default": false + }, + "classComponent": { + "type": "boolean", + "description": "Use class components instead of functional component.", + "alias": "C", + "default": false + }, + "js": { + "type": "boolean", + "description": "Generate JavaScript files rather than TypeScript files.", + "default": false + }, + "globalCss": { + "type": "boolean", + "description": "Default is false. When true, the component is generated with *.css/*.scss instead of *.module.css/*.module.scss", + "default": false + }, + "strict": { + "type": "boolean", + "description": "Creates an application with strict mode and strict type checking", + "default": true + }, + "setParserOptionsProject": { + "type": "boolean", + "description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.", + "default": false + }, + "standaloneConfig": { + "description": "Split the project configuration into /project.json rather than including it inside workspace.json", + "type": "boolean" + }, + "compiler": { + "type": "string", + "description": "The compiler to use", + "enum": ["babel", "swc"], + "default": "babel" + }, + "port": { + "type": "number", + "description": "The port at which the remote application should be served." + }, + "remotes": { + "type": "array", + "description": "A list of remote application names that the host application should consume.", + "default": [] + } + }, + "required": ["name"], + "additionalProperties": false +}