feat(react): add SSR suppoprt to host generator for module federation (#13416)
This commit is contained in:
parent
19acffa837
commit
6c59cbbb3b
@ -1099,6 +1099,11 @@
|
|||||||
"devServerPort": {
|
"devServerPort": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "The port for the dev server of the remote app."
|
"description": "The port for the dev server of the remote app."
|
||||||
|
},
|
||||||
|
"ssr": {
|
||||||
|
"description": "Whether to configure SSR for the host application",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name"],
|
"required": ["name"],
|
||||||
@ -1260,6 +1265,11 @@
|
|||||||
"devServerPort": {
|
"devServerPort": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "The port for the dev server of the remote app."
|
"description": "The port for the dev server of the remote app."
|
||||||
|
},
|
||||||
|
"ssr": {
|
||||||
|
"description": "Whether to configure SSR for the host application",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name"],
|
"required": ["name"],
|
||||||
@ -1447,6 +1457,13 @@
|
|||||||
"skipFormat": {
|
"skipFormat": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Skip formatting the workspace after the generator completes."
|
"description": "Skip formatting the workspace after the generator completes."
|
||||||
|
},
|
||||||
|
"extraInclude": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"hidden": true,
|
||||||
|
"description": "Extra include entries in tsconfig.",
|
||||||
|
"default": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["project"],
|
"required": ["project"],
|
||||||
|
|||||||
@ -13,10 +13,10 @@ export function normalizeProjectName(options: Schema) {
|
|||||||
return normalizeDirectory(options).replace(new RegExp('/', 'g'), '-');
|
return normalizeDirectory(options).replace(new RegExp('/', 'g'), '-');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeOptions(
|
export function normalizeOptions<T extends Schema = Schema>(
|
||||||
host: Tree,
|
host: Tree,
|
||||||
options: Schema
|
options: Schema
|
||||||
): NormalizedSchema {
|
): NormalizedSchema<T> {
|
||||||
const appDirectory = normalizeDirectory(options);
|
const appDirectory = normalizeDirectory(options);
|
||||||
const appProjectName = normalizeProjectName(options);
|
const appProjectName = normalizeProjectName(options);
|
||||||
const e2eProjectName = options.rootProject
|
const e2eProjectName = options.rootProject
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { SupportedStyles } from '../../../typings/style';
|
|||||||
export interface Schema {
|
export interface Schema {
|
||||||
name: string;
|
name: string;
|
||||||
style: SupportedStyles;
|
style: SupportedStyles;
|
||||||
skipFormat: boolean;
|
skipFormat?: boolean;
|
||||||
directory?: string;
|
directory?: string;
|
||||||
tags?: string;
|
tags?: string;
|
||||||
unitTestRunner?: 'jest' | 'vitest' | 'none';
|
unitTestRunner?: 'jest' | 'vitest' | 'none';
|
||||||
@ -34,7 +34,7 @@ export interface Schema {
|
|||||||
minimal?: boolean;
|
minimal?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NormalizedSchema extends Schema {
|
export interface NormalizedSchema<T extends Schema = Schema> extends T {
|
||||||
projectName: string;
|
projectName: string;
|
||||||
appProjectRoot: string;
|
appProjectRoot: string;
|
||||||
e2eProjectName: string;
|
e2eProjectName: string;
|
||||||
|
|||||||
@ -0,0 +1,13 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import('@nrwl/devkit').ModuleFederationConfig}
|
||||||
|
**/
|
||||||
|
const moduleFederationConfig = {
|
||||||
|
name: '<%= projectName %>',
|
||||||
|
remotes: [
|
||||||
|
<% remotes.forEach(function(r) {%> "<%= r.fileName %>", <% }); %>
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = moduleFederationConfig;
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
const { withModuleFederationForSSR } = require('@nrwl/react/module-federation');
|
||||||
|
const baseConfig = require("./module-federation.server.config");
|
||||||
|
|
||||||
|
const defaultConfig = {
|
||||||
|
...baseConfig
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = withModuleFederationForSSR(defaultConfig);
|
||||||
60
packages/react/src/generators/host/host.spec.ts
Normal file
60
packages/react/src/generators/host/host.spec.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import type { Tree } from '@nrwl/devkit';
|
||||||
|
import { readJson } from '@nrwl/devkit';
|
||||||
|
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
|
||||||
|
import hostGenerator from './host';
|
||||||
|
import { Linter } from '@nrwl/linter';
|
||||||
|
|
||||||
|
describe('hostGenerator', () => {
|
||||||
|
let tree: Tree;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate host files and configs', async () => {
|
||||||
|
await hostGenerator(tree, {
|
||||||
|
name: 'test',
|
||||||
|
style: 'css',
|
||||||
|
linter: Linter.None,
|
||||||
|
unitTestRunner: 'none',
|
||||||
|
e2eTestRunner: 'none',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tree.exists('apps/test/tsconfig.json'));
|
||||||
|
expect(tree.exists('apps/test/webpack.config.prod.js'));
|
||||||
|
expect(tree.exists('apps/test/webpack.config.js'));
|
||||||
|
expect(tree.exists('apps/test/src/bootstrap.tsx'));
|
||||||
|
expect(tree.exists('apps/test/src/main.ts'));
|
||||||
|
expect(tree.exists('apps/test/src/remotes.d.ts'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate host files and configs for SSR', async () => {
|
||||||
|
await hostGenerator(tree, {
|
||||||
|
name: 'test',
|
||||||
|
ssr: true,
|
||||||
|
style: 'css',
|
||||||
|
linter: Linter.None,
|
||||||
|
unitTestRunner: 'none',
|
||||||
|
e2eTestRunner: 'none',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tree.exists('apps/test/tsconfig.json'));
|
||||||
|
expect(tree.exists('apps/test/webpack.config.prod.js'));
|
||||||
|
expect(tree.exists('apps/test/webpack.config.server.js'));
|
||||||
|
expect(tree.exists('apps/test/webpack.config.js'));
|
||||||
|
expect(tree.exists('apps/test/src/main.server.tsx'));
|
||||||
|
expect(tree.exists('apps/test/src/bootstrap.tsx'));
|
||||||
|
expect(tree.exists('apps/test/src/main.ts'));
|
||||||
|
expect(tree.exists('apps/test/src/remotes.d.ts'));
|
||||||
|
|
||||||
|
expect(readJson(tree, 'apps/test/tsconfig.server.json')).toEqual({
|
||||||
|
compilerOptions: {
|
||||||
|
outDir: '../../out-tsc/server',
|
||||||
|
target: 'es2019',
|
||||||
|
types: ['node'],
|
||||||
|
},
|
||||||
|
extends: './tsconfig.app.json',
|
||||||
|
include: ['src/main.server.tsx', 'server.ts'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,15 +1,27 @@
|
|||||||
import { formatFiles, Tree } from '@nrwl/devkit';
|
import {
|
||||||
|
formatFiles,
|
||||||
|
GeneratorCallback,
|
||||||
|
joinPathFragments,
|
||||||
|
readProjectConfiguration,
|
||||||
|
Tree,
|
||||||
|
updateProjectConfiguration,
|
||||||
|
} from '@nrwl/devkit';
|
||||||
|
|
||||||
import applicationGenerator from '../application/application';
|
import applicationGenerator from '../application/application';
|
||||||
import { normalizeOptions } from '../application/lib/normalize-options';
|
import { normalizeOptions } from '../application/lib/normalize-options';
|
||||||
import { updateModuleFederationProject } from '../../rules/update-module-federation-project';
|
import { updateModuleFederationProject } from '../../rules/update-module-federation-project';
|
||||||
import { addModuleFederationFiles } from './lib/add-module-federation-files';
|
import { addModuleFederationFiles } from './lib/add-module-federation-files';
|
||||||
import { updateModuleFederationE2eProject } from './lib/update-module-federation-e2e-project';
|
import { updateModuleFederationE2eProject } from './lib/update-module-federation-e2e-project';
|
||||||
|
|
||||||
import { Schema } from './schema';
|
import { Schema } from './schema';
|
||||||
import remoteGenerator from '../remote/remote';
|
import remoteGenerator from '../remote/remote';
|
||||||
|
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
|
||||||
|
import setupSsrGenerator from '../setup-ssr/setup-ssr';
|
||||||
|
import { setupSsrForHost } from './lib/setup-ssr-for-host';
|
||||||
|
|
||||||
export async function hostGenerator(host: Tree, schema: Schema) {
|
export async function hostGenerator(host: Tree, schema: Schema) {
|
||||||
const options = normalizeOptions(host, schema);
|
const tasks: GeneratorCallback[] = [];
|
||||||
|
const options = normalizeOptions<Schema>(host, schema);
|
||||||
|
|
||||||
const initTask = await applicationGenerator(host, {
|
const initTask = await applicationGenerator(host, {
|
||||||
...options,
|
...options,
|
||||||
@ -18,6 +30,7 @@ export async function hostGenerator(host: Tree, schema: Schema) {
|
|||||||
// Only webpack works with module federation for now.
|
// Only webpack works with module federation for now.
|
||||||
bundler: 'webpack',
|
bundler: 'webpack',
|
||||||
});
|
});
|
||||||
|
tasks.push(initTask);
|
||||||
|
|
||||||
const remotesWithPorts: { name: string; port: number }[] = [];
|
const remotesWithPorts: { name: string; port: number }[] = [];
|
||||||
|
|
||||||
@ -34,6 +47,7 @@ export async function hostGenerator(host: Tree, schema: Schema) {
|
|||||||
e2eTestRunner: options.e2eTestRunner,
|
e2eTestRunner: options.e2eTestRunner,
|
||||||
linter: options.linter,
|
linter: options.linter,
|
||||||
devServerPort: remotePort,
|
devServerPort: remotePort,
|
||||||
|
ssr: options.ssr,
|
||||||
});
|
});
|
||||||
remotePort++;
|
remotePort++;
|
||||||
}
|
}
|
||||||
@ -43,11 +57,33 @@ export async function hostGenerator(host: Tree, schema: Schema) {
|
|||||||
updateModuleFederationProject(host, options);
|
updateModuleFederationProject(host, options);
|
||||||
updateModuleFederationE2eProject(host, options);
|
updateModuleFederationE2eProject(host, options);
|
||||||
|
|
||||||
|
if (options.ssr) {
|
||||||
|
const setupSsrTask = await setupSsrGenerator(host, {
|
||||||
|
project: options.projectName,
|
||||||
|
});
|
||||||
|
tasks.push(setupSsrTask);
|
||||||
|
|
||||||
|
const setupSsrForHostTask = await setupSsrForHost(
|
||||||
|
host,
|
||||||
|
options,
|
||||||
|
options.projectName,
|
||||||
|
remotesWithPorts
|
||||||
|
);
|
||||||
|
tasks.push(setupSsrForHostTask);
|
||||||
|
|
||||||
|
const projectConfig = readProjectConfiguration(host, options.projectName);
|
||||||
|
projectConfig.targets.server.options.webpackConfig = joinPathFragments(
|
||||||
|
projectConfig.root,
|
||||||
|
'webpack.server.config.js'
|
||||||
|
);
|
||||||
|
updateProjectConfiguration(host, options.projectName, projectConfig);
|
||||||
|
}
|
||||||
|
|
||||||
if (!options.skipFormat) {
|
if (!options.skipFormat) {
|
||||||
await formatFiles(host);
|
await formatFiles(host);
|
||||||
}
|
}
|
||||||
|
|
||||||
return initTask;
|
return runTasksInSerial(...tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default hostGenerator;
|
export default hostGenerator;
|
||||||
|
|||||||
@ -29,6 +29,13 @@ export function addModuleFederationFiles(
|
|||||||
join(options.appProjectRoot, 'src/bootstrap.tsx')
|
join(options.appProjectRoot, 'src/bootstrap.tsx')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
generateFiles(
|
||||||
|
host,
|
||||||
|
join(__dirname, `../files/common`),
|
||||||
|
options.appProjectRoot,
|
||||||
|
templateVariables
|
||||||
|
);
|
||||||
|
|
||||||
// New entry file is created here.
|
// New entry file is created here.
|
||||||
generateFiles(
|
generateFiles(
|
||||||
host,
|
host,
|
||||||
@ -36,11 +43,4 @@ export function addModuleFederationFiles(
|
|||||||
options.appProjectRoot,
|
options.appProjectRoot,
|
||||||
templateVariables
|
templateVariables
|
||||||
);
|
);
|
||||||
|
|
||||||
generateFiles(
|
|
||||||
host,
|
|
||||||
join(__dirname, `../files/common`),
|
|
||||||
options.appProjectRoot,
|
|
||||||
templateVariables
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
52
packages/react/src/generators/host/lib/setup-ssr-for-host.ts
Normal file
52
packages/react/src/generators/host/lib/setup-ssr-for-host.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import type { GeneratorCallback, Tree } from '@nrwl/devkit';
|
||||||
|
import {
|
||||||
|
addDependenciesToPackageJson,
|
||||||
|
generateFiles,
|
||||||
|
joinPathFragments,
|
||||||
|
names,
|
||||||
|
readProjectConfiguration,
|
||||||
|
} from '@nrwl/devkit';
|
||||||
|
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
|
||||||
|
|
||||||
|
import type { Schema } from '../schema';
|
||||||
|
import { moduleFederationNodeVersion } from '../../../utils/versions';
|
||||||
|
import { normalizeProjectName } from '../../application/lib/normalize-options';
|
||||||
|
|
||||||
|
export async function setupSsrForHost(
|
||||||
|
tree: Tree,
|
||||||
|
options: Schema,
|
||||||
|
appName: string,
|
||||||
|
defaultRemoteManifest: { name: string; port: number }[]
|
||||||
|
) {
|
||||||
|
const tasks: GeneratorCallback[] = [];
|
||||||
|
const project = readProjectConfiguration(tree, appName);
|
||||||
|
|
||||||
|
generateFiles(
|
||||||
|
tree,
|
||||||
|
joinPathFragments(__dirname, '../files/module-federation-ssr'),
|
||||||
|
project.root,
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
remotes: defaultRemoteManifest.map(({ name, port }) => {
|
||||||
|
const remote = normalizeProjectName({ ...options, name });
|
||||||
|
return {
|
||||||
|
...names(remote),
|
||||||
|
port,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
appName,
|
||||||
|
tmpl: '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const installTask = addDependenciesToPackageJson(
|
||||||
|
tree,
|
||||||
|
{
|
||||||
|
'@module-federation/node': moduleFederationNodeVersion,
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
tasks.push(installTask);
|
||||||
|
|
||||||
|
return runTasksInSerial(...tasks);
|
||||||
|
}
|
||||||
33
packages/react/src/generators/host/schema.d.ts
vendored
33
packages/react/src/generators/host/schema.d.ts
vendored
@ -2,29 +2,30 @@ import { Linter } from '@nrwl/linter';
|
|||||||
import { SupportedStyles } from '../../../typings';
|
import { SupportedStyles } from '../../../typings';
|
||||||
|
|
||||||
export interface Schema {
|
export interface Schema {
|
||||||
name: string;
|
|
||||||
style: SupportedStyles;
|
|
||||||
skipFormat: boolean;
|
|
||||||
directory?: string;
|
|
||||||
tags?: string;
|
|
||||||
unitTestRunner: 'jest' | 'vitest' | 'none';
|
|
||||||
e2eTestRunner: 'cypress' | 'none';
|
|
||||||
linter: Linter;
|
|
||||||
pascalCaseFiles?: boolean;
|
|
||||||
classComponent?: boolean;
|
classComponent?: boolean;
|
||||||
skipWorkspaceJson?: boolean;
|
|
||||||
js?: boolean;
|
|
||||||
globalCss?: boolean;
|
|
||||||
strict?: boolean;
|
|
||||||
setParserOptionsProject?: boolean;
|
|
||||||
standaloneConfig?: boolean;
|
|
||||||
compiler?: 'babel' | 'swc';
|
compiler?: 'babel' | 'swc';
|
||||||
devServerPort?: number;
|
devServerPort?: number;
|
||||||
|
directory?: string;
|
||||||
|
e2eTestRunner: 'cypress' | 'none';
|
||||||
|
globalCss?: boolean;
|
||||||
|
js?: boolean;
|
||||||
|
linter: Linter;
|
||||||
|
name: string;
|
||||||
|
pascalCaseFiles?: boolean;
|
||||||
remotes?: string[];
|
remotes?: string[];
|
||||||
|
setParserOptionsProject?: boolean;
|
||||||
|
skipFormat?: boolean;
|
||||||
|
skipWorkspaceJson?: boolean;
|
||||||
|
ssr?: boolean;
|
||||||
|
standaloneConfig?: boolean;
|
||||||
|
strict?: boolean;
|
||||||
|
style: SupportedStyles;
|
||||||
|
tags?: string;
|
||||||
|
unitTestRunner: 'jest' | 'vitest' | 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NormalizedSchema extends Schema {
|
export interface NormalizedSchema extends Schema {
|
||||||
projectName: string;
|
|
||||||
appProjectRoot: string;
|
appProjectRoot: string;
|
||||||
e2eProjectName: string;
|
e2eProjectName: string;
|
||||||
|
projectName: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -148,6 +148,11 @@
|
|||||||
"devServerPort": {
|
"devServerPort": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "The port for the dev server of the remote app."
|
"description": "The port for the dev server of the remote app."
|
||||||
|
},
|
||||||
|
"ssr": {
|
||||||
|
"description": "Whether to configure SSR for the host application",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name"],
|
"required": ["name"],
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { readJson, Tree } from '@nrwl/devkit';
|
import { readJson, Tree } from '@nrwl/devkit';
|
||||||
import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing';
|
import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing';
|
||||||
import { applicationGenerator, libraryGenerator } from '@nrwl/react';
|
|
||||||
import { Linter } from '@nrwl/linter';
|
import { Linter } from '@nrwl/linter';
|
||||||
|
import { applicationGenerator } from '../application/application';
|
||||||
|
import { libraryGenerator } from '../library/library';
|
||||||
import { reduxGenerator } from './redux';
|
import { reduxGenerator } from './redux';
|
||||||
|
|
||||||
describe('redux', () => {
|
describe('redux', () => {
|
||||||
|
|||||||
@ -0,0 +1,13 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import('@nrwl/devkit').ModuleFederationConfig}
|
||||||
|
**/
|
||||||
|
const moduleFederationConfig = {
|
||||||
|
name: '<%= projectName %>',
|
||||||
|
exposes: {
|
||||||
|
'./Module': '<%= appProjectRoot %>/src/remote-entry.ts',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = moduleFederationConfig;
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
const { withModuleFederationForSSR } = require('@nrwl/react/module-federation');
|
||||||
|
const baseConfig = require("./module-federation.server.config");
|
||||||
|
|
||||||
|
const defaultConfig = {
|
||||||
|
...baseConfig
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = withModuleFederationForSSR(defaultConfig);
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
import type { GeneratorCallback, Tree } from '@nrwl/devkit';
|
||||||
|
import {
|
||||||
|
addDependenciesToPackageJson,
|
||||||
|
generateFiles,
|
||||||
|
joinPathFragments,
|
||||||
|
readProjectConfiguration,
|
||||||
|
} from '@nrwl/devkit';
|
||||||
|
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
|
||||||
|
|
||||||
|
import { NormalizedSchema } from '../../application/schema';
|
||||||
|
import type { Schema } from '../schema';
|
||||||
|
import { moduleFederationNodeVersion } from '../../../utils/versions';
|
||||||
|
|
||||||
|
export async function setupSsrForRemote(
|
||||||
|
tree: Tree,
|
||||||
|
options: NormalizedSchema<Schema>,
|
||||||
|
appName: string
|
||||||
|
) {
|
||||||
|
const tasks: GeneratorCallback[] = [];
|
||||||
|
const project = readProjectConfiguration(tree, appName);
|
||||||
|
|
||||||
|
generateFiles(
|
||||||
|
tree,
|
||||||
|
joinPathFragments(__dirname, '../files/module-federation-ssr'),
|
||||||
|
project.root,
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
appName,
|
||||||
|
tmpl: '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const installTask = addDependenciesToPackageJson(
|
||||||
|
tree,
|
||||||
|
{
|
||||||
|
'@module-federation/node': moduleFederationNodeVersion,
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
tasks.push(installTask);
|
||||||
|
|
||||||
|
return runTasksInSerial(...tasks);
|
||||||
|
}
|
||||||
@ -1,5 +1,14 @@
|
|||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { formatFiles, generateFiles, names, Tree } from '@nrwl/devkit';
|
import {
|
||||||
|
formatFiles,
|
||||||
|
generateFiles,
|
||||||
|
GeneratorCallback,
|
||||||
|
joinPathFragments,
|
||||||
|
names,
|
||||||
|
readProjectConfiguration,
|
||||||
|
Tree,
|
||||||
|
updateProjectConfiguration,
|
||||||
|
} from '@nrwl/devkit';
|
||||||
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
|
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
|
||||||
|
|
||||||
import { normalizeOptions } from '../application/lib/normalize-options';
|
import { normalizeOptions } from '../application/lib/normalize-options';
|
||||||
@ -8,6 +17,8 @@ import { NormalizedSchema } from '../application/schema';
|
|||||||
import { updateHostWithRemote } from './lib/update-host-with-remote';
|
import { updateHostWithRemote } from './lib/update-host-with-remote';
|
||||||
import { updateModuleFederationProject } from '../../rules/update-module-federation-project';
|
import { updateModuleFederationProject } from '../../rules/update-module-federation-project';
|
||||||
import { Schema } from './schema';
|
import { Schema } from './schema';
|
||||||
|
import setupSsrGenerator from '../setup-ssr/setup-ssr';
|
||||||
|
import { setupSsrForRemote } from './lib/setup-ssr-for-remote';
|
||||||
|
|
||||||
export function addModuleFederationFiles(
|
export function addModuleFederationFiles(
|
||||||
host: Tree,
|
host: Tree,
|
||||||
@ -28,13 +39,15 @@ export function addModuleFederationFiles(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function remoteGenerator(host: Tree, schema: Schema) {
|
export async function remoteGenerator(host: Tree, schema: Schema) {
|
||||||
const options = normalizeOptions(host, schema);
|
const tasks: GeneratorCallback[] = [];
|
||||||
const initApp = await applicationGenerator(host, {
|
const options = normalizeOptions<Schema>(host, schema);
|
||||||
|
const initAppTask = await applicationGenerator(host, {
|
||||||
...options,
|
...options,
|
||||||
skipDefaultProject: true,
|
skipDefaultProject: true,
|
||||||
// Only webpack works with module federation for now.
|
// Only webpack works with module federation for now.
|
||||||
bundler: 'webpack',
|
bundler: 'webpack',
|
||||||
});
|
});
|
||||||
|
tasks.push(initAppTask);
|
||||||
|
|
||||||
if (schema.host) {
|
if (schema.host) {
|
||||||
updateHostWithRemote(host, schema.host, options.name);
|
updateHostWithRemote(host, schema.host, options.name);
|
||||||
@ -51,11 +64,32 @@ export async function remoteGenerator(host: Tree, schema: Schema) {
|
|||||||
addModuleFederationFiles(host, options);
|
addModuleFederationFiles(host, options);
|
||||||
updateModuleFederationProject(host, options);
|
updateModuleFederationProject(host, options);
|
||||||
|
|
||||||
|
if (options.ssr) {
|
||||||
|
const setupSsrTask = await setupSsrGenerator(host, {
|
||||||
|
project: options.projectName,
|
||||||
|
});
|
||||||
|
tasks.push(setupSsrTask);
|
||||||
|
|
||||||
|
const setupSsrForRemoteTask = await setupSsrForRemote(
|
||||||
|
host,
|
||||||
|
options,
|
||||||
|
options.projectName
|
||||||
|
);
|
||||||
|
tasks.push(setupSsrForRemoteTask);
|
||||||
|
|
||||||
|
const projectConfig = readProjectConfiguration(host, options.projectName);
|
||||||
|
projectConfig.targets.server.options.webpackConfig = joinPathFragments(
|
||||||
|
projectConfig.root,
|
||||||
|
'webpack.server.config.js'
|
||||||
|
);
|
||||||
|
updateProjectConfiguration(host, options.projectName, projectConfig);
|
||||||
|
}
|
||||||
|
|
||||||
if (!options.skipFormat) {
|
if (!options.skipFormat) {
|
||||||
await formatFiles(host);
|
await formatFiles(host);
|
||||||
}
|
}
|
||||||
|
|
||||||
return runTasksInSerial(initApp);
|
return runTasksInSerial(...tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default remoteGenerator;
|
export default remoteGenerator;
|
||||||
|
|||||||
35
packages/react/src/generators/remote/schema.d.ts
vendored
35
packages/react/src/generators/remote/schema.d.ts
vendored
@ -3,24 +3,25 @@ import { Linter } from '@nrwl/linter';
|
|||||||
import { SupportedStyles } from '../../../typings';
|
import { SupportedStyles } from '../../../typings';
|
||||||
|
|
||||||
export interface Schema {
|
export interface Schema {
|
||||||
name: string;
|
classComponent?: boolean;
|
||||||
style: SupportedStyles;
|
compiler?: 'babel' | 'swc';
|
||||||
skipFormat: boolean;
|
devServerPort?: number;
|
||||||
directory?: string;
|
directory?: string;
|
||||||
|
e2eTestRunner: 'cypress' | 'none';
|
||||||
|
globalCss?: boolean;
|
||||||
|
host?: string;
|
||||||
|
js?: boolean;
|
||||||
|
linter: Linter;
|
||||||
|
name: string;
|
||||||
|
pascalCaseFiles?: boolean;
|
||||||
|
routing?: boolean;
|
||||||
|
setParserOptionsProject?: boolean;
|
||||||
|
skipFormat: boolean;
|
||||||
|
skipWorkspaceJson?: boolean;
|
||||||
|
ssr?: boolean;
|
||||||
|
standaloneConfig?: boolean;
|
||||||
|
strict?: boolean;
|
||||||
|
style: SupportedStyles;
|
||||||
tags?: string;
|
tags?: string;
|
||||||
unitTestRunner: 'jest' | 'vitest' | 'none';
|
unitTestRunner: 'jest' | 'vitest' | 'none';
|
||||||
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';
|
|
||||||
host?: string;
|
|
||||||
devServerPort?: number;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -152,6 +152,11 @@
|
|||||||
"devServerPort": {
|
"devServerPort": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "The port for the dev server of the remote app."
|
"description": "The port for the dev server of the remote app."
|
||||||
|
},
|
||||||
|
"ssr": {
|
||||||
|
"description": "Whether to configure SSR for the host application",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name"],
|
"required": ["name"],
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import express from 'express';
|
|
||||||
import { handleRequest } from './src/main.server';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
|
||||||
|
import { handleRequest } from './src/main.server';
|
||||||
|
|
||||||
const port = process.env['port'] || 4200;
|
const port = process.env['port'] || 4200;
|
||||||
const app = express();
|
const app = express();
|
||||||
@ -8,12 +10,14 @@ const app = express();
|
|||||||
const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
|
const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
|
||||||
const indexPath =path.join(browserDist, 'index.html');
|
const indexPath =path.join(browserDist, 'index.html');
|
||||||
|
|
||||||
app.get(
|
app.use(cors());
|
||||||
'*.*',
|
|
||||||
express.static(browserDist, {
|
app.get(
|
||||||
maxAge: '1y',
|
'*.*',
|
||||||
})
|
express.static(browserDist, {
|
||||||
);
|
maxAge: '1y',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
app.use('*', handleRequest(indexPath));
|
app.use('*', handleRequest(indexPath));
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,8 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
<%- extraInclude %>
|
||||||
"src/main.server.tsx",
|
"src/main.server.tsx",
|
||||||
"server.ts",
|
"server.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
export interface Schema {
|
export interface Schema {
|
||||||
project: string;
|
project: string;
|
||||||
appComponentImportPath: string;
|
appComponentImportPath?: string;
|
||||||
serverPort?: number;
|
serverPort?: number;
|
||||||
skipFormat?: boolean;
|
skipFormat?: boolean;
|
||||||
|
extraInclude?: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,15 @@
|
|||||||
"skipFormat": {
|
"skipFormat": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Skip formatting the workspace after the generator completes."
|
"description": "Skip formatting the workspace after the generator completes."
|
||||||
|
},
|
||||||
|
"extraInclude": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"hidden": true,
|
||||||
|
"description": "Extra include entries in tsconfig.",
|
||||||
|
"default": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["project"],
|
"required": ["project"],
|
||||||
|
|||||||
42
packages/react/src/generators/setup-ssr/setup-ssr.spec.ts
Normal file
42
packages/react/src/generators/setup-ssr/setup-ssr.spec.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
|
||||||
|
import { readJson, Tree } from '@nrwl/devkit';
|
||||||
|
import applicationGenerator from '../application/application';
|
||||||
|
import setupSsrGenerator from './setup-ssr';
|
||||||
|
import { Linter } from '@nrwl/linter';
|
||||||
|
|
||||||
|
describe('setupSsrGenerator', () => {
|
||||||
|
let tree: Tree;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tree = createTreeWithEmptyWorkspace();
|
||||||
|
|
||||||
|
applicationGenerator(tree, {
|
||||||
|
name: 'my-app',
|
||||||
|
style: 'css',
|
||||||
|
linter: Linter.None,
|
||||||
|
unitTestRunner: 'none',
|
||||||
|
e2eTestRunner: 'none',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add SSR files', async () => {
|
||||||
|
await setupSsrGenerator(tree, {
|
||||||
|
project: 'my-app',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tree.exists(`my-app/server.ts`)).toBeTruthy();
|
||||||
|
expect(tree.exists(`my-app/tsconfig.server.json`)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support adding additional include files', async () => {
|
||||||
|
await setupSsrGenerator(tree, {
|
||||||
|
project: 'my-app',
|
||||||
|
extraInclude: ['src/remote.d.ts'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tree.exists(`my-app/server.ts`)).toBeTruthy();
|
||||||
|
expect(readJson(tree, `my-app/tsconfig.server.json`)).toMatchObject({
|
||||||
|
include: ['src/remote.d.ts', 'src/main.server.tsx', 'server.ts'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,5 +1,7 @@
|
|||||||
|
import * as ts from 'typescript';
|
||||||
import {
|
import {
|
||||||
addDependenciesToPackageJson,
|
addDependenciesToPackageJson,
|
||||||
|
applyChangesToString,
|
||||||
convertNxGenerator,
|
convertNxGenerator,
|
||||||
formatFiles,
|
formatFiles,
|
||||||
generateFiles,
|
generateFiles,
|
||||||
@ -10,32 +12,59 @@ import {
|
|||||||
updateProjectConfiguration,
|
updateProjectConfiguration,
|
||||||
updateWorkspaceConfiguration,
|
updateWorkspaceConfiguration,
|
||||||
} from '@nrwl/devkit';
|
} from '@nrwl/devkit';
|
||||||
|
import initGenerator from '../init/init';
|
||||||
|
|
||||||
import type { Schema } from './schema';
|
import type { Schema } from './schema';
|
||||||
import {
|
import {
|
||||||
|
corsVersion,
|
||||||
expressVersion,
|
expressVersion,
|
||||||
isbotVersion,
|
isbotVersion,
|
||||||
|
typesCorsVersion,
|
||||||
typesExpressVersion,
|
typesExpressVersion,
|
||||||
} from '../../utils/versions';
|
} from '../../utils/versions';
|
||||||
|
import { addStaticRouter } from '../../utils/ast-utils';
|
||||||
|
|
||||||
|
function readEntryFile(
|
||||||
|
host: Tree,
|
||||||
|
path: string
|
||||||
|
): { content: string; source: ts.SourceFile } {
|
||||||
|
const content = host.read(path, 'utf-8');
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
source: ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppComponentInfo {
|
||||||
|
importPath: string;
|
||||||
|
filePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
export async function setupSsrGenerator(tree: Tree, options: Schema) {
|
export async function setupSsrGenerator(tree: Tree, options: Schema) {
|
||||||
|
await initGenerator(tree, {});
|
||||||
const projectConfig = readProjectConfiguration(tree, options.project);
|
const projectConfig = readProjectConfiguration(tree, options.project);
|
||||||
const projectRoot = projectConfig.root;
|
const projectRoot = projectConfig.root;
|
||||||
const appImportCandidates = [
|
const appImportCandidates: AppComponentInfo[] = [
|
||||||
options.appComponentImportPath,
|
options.appComponentImportPath ?? 'app/app',
|
||||||
'app',
|
'app',
|
||||||
'App',
|
'App',
|
||||||
'app/App',
|
'app/App',
|
||||||
'App/App',
|
'App/App',
|
||||||
];
|
].map((importPath) => {
|
||||||
const appComponentImport = appImportCandidates.find(
|
return {
|
||||||
(app) =>
|
importPath,
|
||||||
tree.exists(joinPathFragments(projectConfig.sourceRoot, `${app}.tsx`)) ||
|
filePath: joinPathFragments(
|
||||||
tree.exists(joinPathFragments(projectConfig.sourceRoot, `${app}.jsx`)) ||
|
projectConfig.sourceRoot || projectConfig.root,
|
||||||
tree.exists(joinPathFragments(projectConfig.sourceRoot, `${app}.js`))
|
`${importPath}.tsx`
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const appComponentInfo = appImportCandidates.find((candidate) =>
|
||||||
|
tree.exists(candidate.filePath)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!appComponentImport) {
|
if (!appComponentInfo) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Cannot find an import path for <App/> component. Try passing setting --appComponentImportPath option.`
|
`Cannot find an import path for <App/> component. Try passing setting --appComponentImportPath option.`
|
||||||
);
|
);
|
||||||
@ -148,10 +177,28 @@ export async function setupSsrGenerator(tree: Tree, options: Schema) {
|
|||||||
|
|
||||||
generateFiles(tree, joinPathFragments(__dirname, 'files'), projectRoot, {
|
generateFiles(tree, joinPathFragments(__dirname, 'files'), projectRoot, {
|
||||||
tmpl: '',
|
tmpl: '',
|
||||||
appComponentImport,
|
extraInclude:
|
||||||
|
options.extraInclude?.length > 0
|
||||||
|
? `"${options.extraInclude.join('", "')}",`
|
||||||
|
: '',
|
||||||
|
appComponentImport: appComponentInfo.importPath,
|
||||||
browserBuildOutputPath: projectConfig.targets.build.options.outputPath,
|
browserBuildOutputPath: projectConfig.targets.build.options.outputPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add <StaticRouter> to server main if needed.
|
||||||
|
// TODO: need to read main.server.tsx not main.tsx.
|
||||||
|
const appContent = tree.read(appComponentInfo.filePath, 'utf-8');
|
||||||
|
const isRouterPresent = appContent.match(/react-router-dom/);
|
||||||
|
if (isRouterPresent) {
|
||||||
|
const serverEntry = joinPathFragments(projectRoot, 'src/main.server.tsx');
|
||||||
|
const { content, source } = readEntryFile(tree, serverEntry);
|
||||||
|
const changes = applyChangesToString(
|
||||||
|
content,
|
||||||
|
addStaticRouter(serverEntry, source)
|
||||||
|
);
|
||||||
|
tree.write(serverEntry, changes);
|
||||||
|
}
|
||||||
|
|
||||||
updateWorkspaceConfiguration(tree, workspace);
|
updateWorkspaceConfiguration(tree, workspace);
|
||||||
|
|
||||||
const installTask = addDependenciesToPackageJson(
|
const installTask = addDependenciesToPackageJson(
|
||||||
@ -159,9 +206,11 @@ export async function setupSsrGenerator(tree: Tree, options: Schema) {
|
|||||||
{
|
{
|
||||||
express: expressVersion,
|
express: expressVersion,
|
||||||
isbot: isbotVersion,
|
isbot: isbotVersion,
|
||||||
|
cors: corsVersion,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@types/express': typesExpressVersion,
|
'@types/express': typesExpressVersion,
|
||||||
|
'@types/cors': typesCorsVersion,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -452,6 +452,36 @@ export function addBrowserRouter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addStaticRouter(
|
||||||
|
sourcePath: string,
|
||||||
|
source: ts.SourceFile
|
||||||
|
): StringChange[] {
|
||||||
|
const app = findElements(source, 'App')[0];
|
||||||
|
if (app) {
|
||||||
|
return [
|
||||||
|
...addImport(
|
||||||
|
source,
|
||||||
|
`import { StaticRouter } from 'react-router-dom/server';`
|
||||||
|
),
|
||||||
|
{
|
||||||
|
type: ChangeType.Insert,
|
||||||
|
index: app.getStart(),
|
||||||
|
text: `<StaticRouter location={req.url}>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ChangeType.Insert,
|
||||||
|
index: app.getEnd(),
|
||||||
|
text: `</StaticRouter>`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
`Could not find App component in ${sourcePath}; Skipping add <StaticRouter>`
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function addReduxStoreToMain(
|
export function addReduxStoreToMain(
|
||||||
sourcePath: string,
|
sourcePath: string,
|
||||||
source: ts.SourceFile
|
source: ts.SourceFile
|
||||||
|
|||||||
@ -41,6 +41,10 @@ export const postcssVersion = '8.4.19';
|
|||||||
export const tailwindcssVersion = '3.2.4';
|
export const tailwindcssVersion = '3.2.4';
|
||||||
export const autoprefixerVersion = '10.4.13';
|
export const autoprefixerVersion = '10.4.13';
|
||||||
|
|
||||||
export const expressVersion = '^4.18.1';
|
// SSR and Module Federation
|
||||||
|
export const expressVersion = '~4.18.2';
|
||||||
export const typesExpressVersion = '4.17.14';
|
export const typesExpressVersion = '4.17.14';
|
||||||
export const isbotVersion = '^3.6.5';
|
export const isbotVersion = '^3.6.5';
|
||||||
|
export const corsVersion = '~2.8.5';
|
||||||
|
export const typesCorsVersion = '~2.8.12';
|
||||||
|
export const moduleFederationNodeVersion = '~0.9.6';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user