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": {
|
||||
"type": "number",
|
||||
"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"],
|
||||
@ -1260,6 +1265,11 @@
|
||||
"devServerPort": {
|
||||
"type": "number",
|
||||
"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"],
|
||||
@ -1447,6 +1457,13 @@
|
||||
"skipFormat": {
|
||||
"type": "boolean",
|
||||
"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"],
|
||||
|
||||
@ -13,10 +13,10 @@ export function normalizeProjectName(options: Schema) {
|
||||
return normalizeDirectory(options).replace(new RegExp('/', 'g'), '-');
|
||||
}
|
||||
|
||||
export function normalizeOptions(
|
||||
export function normalizeOptions<T extends Schema = Schema>(
|
||||
host: Tree,
|
||||
options: Schema
|
||||
): NormalizedSchema {
|
||||
): NormalizedSchema<T> {
|
||||
const appDirectory = normalizeDirectory(options);
|
||||
const appProjectName = normalizeProjectName(options);
|
||||
const e2eProjectName = options.rootProject
|
||||
|
||||
@ -4,7 +4,7 @@ import { SupportedStyles } from '../../../typings/style';
|
||||
export interface Schema {
|
||||
name: string;
|
||||
style: SupportedStyles;
|
||||
skipFormat: boolean;
|
||||
skipFormat?: boolean;
|
||||
directory?: string;
|
||||
tags?: string;
|
||||
unitTestRunner?: 'jest' | 'vitest' | 'none';
|
||||
@ -34,7 +34,7 @@ export interface Schema {
|
||||
minimal?: boolean;
|
||||
}
|
||||
|
||||
export interface NormalizedSchema extends Schema {
|
||||
export interface NormalizedSchema<T extends Schema = Schema> extends T {
|
||||
projectName: string;
|
||||
appProjectRoot: 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 { normalizeOptions } from '../application/lib/normalize-options';
|
||||
import { updateModuleFederationProject } from '../../rules/update-module-federation-project';
|
||||
import { addModuleFederationFiles } from './lib/add-module-federation-files';
|
||||
import { updateModuleFederationE2eProject } from './lib/update-module-federation-e2e-project';
|
||||
|
||||
import { Schema } from './schema';
|
||||
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) {
|
||||
const options = normalizeOptions(host, schema);
|
||||
const tasks: GeneratorCallback[] = [];
|
||||
const options = normalizeOptions<Schema>(host, schema);
|
||||
|
||||
const initTask = await applicationGenerator(host, {
|
||||
...options,
|
||||
@ -18,6 +30,7 @@ export async function hostGenerator(host: Tree, schema: Schema) {
|
||||
// Only webpack works with module federation for now.
|
||||
bundler: 'webpack',
|
||||
});
|
||||
tasks.push(initTask);
|
||||
|
||||
const remotesWithPorts: { name: string; port: number }[] = [];
|
||||
|
||||
@ -34,6 +47,7 @@ export async function hostGenerator(host: Tree, schema: Schema) {
|
||||
e2eTestRunner: options.e2eTestRunner,
|
||||
linter: options.linter,
|
||||
devServerPort: remotePort,
|
||||
ssr: options.ssr,
|
||||
});
|
||||
remotePort++;
|
||||
}
|
||||
@ -43,11 +57,33 @@ export async function hostGenerator(host: Tree, schema: Schema) {
|
||||
updateModuleFederationProject(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) {
|
||||
await formatFiles(host);
|
||||
}
|
||||
|
||||
return initTask;
|
||||
return runTasksInSerial(...tasks);
|
||||
}
|
||||
|
||||
export default hostGenerator;
|
||||
|
||||
@ -29,6 +29,13 @@ export function addModuleFederationFiles(
|
||||
join(options.appProjectRoot, 'src/bootstrap.tsx')
|
||||
);
|
||||
|
||||
generateFiles(
|
||||
host,
|
||||
join(__dirname, `../files/common`),
|
||||
options.appProjectRoot,
|
||||
templateVariables
|
||||
);
|
||||
|
||||
// New entry file is created here.
|
||||
generateFiles(
|
||||
host,
|
||||
@ -36,11 +43,4 @@ export function addModuleFederationFiles(
|
||||
options.appProjectRoot,
|
||||
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';
|
||||
|
||||
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;
|
||||
skipWorkspaceJson?: boolean;
|
||||
js?: boolean;
|
||||
globalCss?: boolean;
|
||||
strict?: boolean;
|
||||
setParserOptionsProject?: boolean;
|
||||
standaloneConfig?: boolean;
|
||||
compiler?: 'babel' | 'swc';
|
||||
devServerPort?: number;
|
||||
directory?: string;
|
||||
e2eTestRunner: 'cypress' | 'none';
|
||||
globalCss?: boolean;
|
||||
js?: boolean;
|
||||
linter: Linter;
|
||||
name: string;
|
||||
pascalCaseFiles?: boolean;
|
||||
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 {
|
||||
projectName: string;
|
||||
appProjectRoot: string;
|
||||
e2eProjectName: string;
|
||||
projectName: string;
|
||||
}
|
||||
|
||||
@ -148,6 +148,11 @@
|
||||
"devServerPort": {
|
||||
"type": "number",
|
||||
"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"],
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { readJson, Tree } from '@nrwl/devkit';
|
||||
import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing';
|
||||
import { applicationGenerator, libraryGenerator } from '@nrwl/react';
|
||||
import { Linter } from '@nrwl/linter';
|
||||
import { applicationGenerator } from '../application/application';
|
||||
import { libraryGenerator } from '../library/library';
|
||||
import { reduxGenerator } from './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 { 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 { normalizeOptions } from '../application/lib/normalize-options';
|
||||
@ -8,6 +17,8 @@ import { NormalizedSchema } from '../application/schema';
|
||||
import { updateHostWithRemote } from './lib/update-host-with-remote';
|
||||
import { updateModuleFederationProject } from '../../rules/update-module-federation-project';
|
||||
import { Schema } from './schema';
|
||||
import setupSsrGenerator from '../setup-ssr/setup-ssr';
|
||||
import { setupSsrForRemote } from './lib/setup-ssr-for-remote';
|
||||
|
||||
export function addModuleFederationFiles(
|
||||
host: Tree,
|
||||
@ -28,13 +39,15 @@ export function addModuleFederationFiles(
|
||||
}
|
||||
|
||||
export async function remoteGenerator(host: Tree, schema: Schema) {
|
||||
const options = normalizeOptions(host, schema);
|
||||
const initApp = await applicationGenerator(host, {
|
||||
const tasks: GeneratorCallback[] = [];
|
||||
const options = normalizeOptions<Schema>(host, schema);
|
||||
const initAppTask = await applicationGenerator(host, {
|
||||
...options,
|
||||
skipDefaultProject: true,
|
||||
// Only webpack works with module federation for now.
|
||||
bundler: 'webpack',
|
||||
});
|
||||
tasks.push(initAppTask);
|
||||
|
||||
if (schema.host) {
|
||||
updateHostWithRemote(host, schema.host, options.name);
|
||||
@ -51,11 +64,32 @@ export async function remoteGenerator(host: Tree, schema: Schema) {
|
||||
addModuleFederationFiles(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) {
|
||||
await formatFiles(host);
|
||||
}
|
||||
|
||||
return runTasksInSerial(initApp);
|
||||
return runTasksInSerial(...tasks);
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
export interface Schema {
|
||||
name: string;
|
||||
style: SupportedStyles;
|
||||
skipFormat: boolean;
|
||||
classComponent?: boolean;
|
||||
compiler?: 'babel' | 'swc';
|
||||
devServerPort?: number;
|
||||
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;
|
||||
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": {
|
||||
"type": "number",
|
||||
"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"],
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import express from 'express';
|
||||
import { handleRequest } from './src/main.server';
|
||||
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 app = express();
|
||||
@ -8,12 +10,14 @@ const app = express();
|
||||
const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
|
||||
const indexPath =path.join(browserDist, 'index.html');
|
||||
|
||||
app.get(
|
||||
app.use(cors());
|
||||
|
||||
app.get(
|
||||
'*.*',
|
||||
express.static(browserDist, {
|
||||
maxAge: '1y',
|
||||
})
|
||||
);
|
||||
);
|
||||
|
||||
app.use('*', handleRequest(indexPath));
|
||||
|
||||
|
||||
@ -8,7 +8,8 @@
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
<%- extraInclude %>
|
||||
"src/main.server.tsx",
|
||||
"server.ts",
|
||||
"server.ts"
|
||||
]
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
export interface Schema {
|
||||
project: string;
|
||||
appComponentImportPath: string;
|
||||
appComponentImportPath?: string;
|
||||
serverPort?: number;
|
||||
skipFormat?: boolean;
|
||||
extraInclude?: string[];
|
||||
}
|
||||
|
||||
@ -29,6 +29,15 @@
|
||||
"skipFormat": {
|
||||
"type": "boolean",
|
||||
"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"],
|
||||
|
||||
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 {
|
||||
addDependenciesToPackageJson,
|
||||
applyChangesToString,
|
||||
convertNxGenerator,
|
||||
formatFiles,
|
||||
generateFiles,
|
||||
@ -10,32 +12,59 @@ import {
|
||||
updateProjectConfiguration,
|
||||
updateWorkspaceConfiguration,
|
||||
} from '@nrwl/devkit';
|
||||
import initGenerator from '../init/init';
|
||||
|
||||
import type { Schema } from './schema';
|
||||
import {
|
||||
corsVersion,
|
||||
expressVersion,
|
||||
isbotVersion,
|
||||
typesCorsVersion,
|
||||
typesExpressVersion,
|
||||
} 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) {
|
||||
await initGenerator(tree, {});
|
||||
const projectConfig = readProjectConfiguration(tree, options.project);
|
||||
const projectRoot = projectConfig.root;
|
||||
const appImportCandidates = [
|
||||
options.appComponentImportPath,
|
||||
const appImportCandidates: AppComponentInfo[] = [
|
||||
options.appComponentImportPath ?? 'app/app',
|
||||
'app',
|
||||
'App',
|
||||
'app/App',
|
||||
'App/App',
|
||||
];
|
||||
const appComponentImport = appImportCandidates.find(
|
||||
(app) =>
|
||||
tree.exists(joinPathFragments(projectConfig.sourceRoot, `${app}.tsx`)) ||
|
||||
tree.exists(joinPathFragments(projectConfig.sourceRoot, `${app}.jsx`)) ||
|
||||
tree.exists(joinPathFragments(projectConfig.sourceRoot, `${app}.js`))
|
||||
].map((importPath) => {
|
||||
return {
|
||||
importPath,
|
||||
filePath: joinPathFragments(
|
||||
projectConfig.sourceRoot || projectConfig.root,
|
||||
`${importPath}.tsx`
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const appComponentInfo = appImportCandidates.find((candidate) =>
|
||||
tree.exists(candidate.filePath)
|
||||
);
|
||||
|
||||
if (!appComponentImport) {
|
||||
if (!appComponentInfo) {
|
||||
throw new Error(
|
||||
`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, {
|
||||
tmpl: '',
|
||||
appComponentImport,
|
||||
extraInclude:
|
||||
options.extraInclude?.length > 0
|
||||
? `"${options.extraInclude.join('", "')}",`
|
||||
: '',
|
||||
appComponentImport: appComponentInfo.importPath,
|
||||
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);
|
||||
|
||||
const installTask = addDependenciesToPackageJson(
|
||||
@ -159,9 +206,11 @@ export async function setupSsrGenerator(tree: Tree, options: Schema) {
|
||||
{
|
||||
express: expressVersion,
|
||||
isbot: isbotVersion,
|
||||
cors: corsVersion,
|
||||
},
|
||||
{
|
||||
'@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(
|
||||
sourcePath: string,
|
||||
source: ts.SourceFile
|
||||
|
||||
@ -41,6 +41,10 @@ export const postcssVersion = '8.4.19';
|
||||
export const tailwindcssVersion = '3.2.4';
|
||||
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 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