feat(react): add SSR suppoprt to host generator for module federation (#13416)

This commit is contained in:
Jack Hsu 2022-11-29 14:49:15 -05:00 committed by GitHub
parent 19acffa837
commit 6c59cbbb3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 510 additions and 73 deletions

View File

@ -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"],

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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);

View 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'],
});
});
});

View File

@ -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;

View File

@ -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
);
} }

View 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);
}

View File

@ -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;
} }

View File

@ -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"],

View File

@ -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', () => {

View File

@ -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;

View File

@ -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);

View File

@ -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);
}

View File

@ -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;

View File

@ -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;
} }

View File

@ -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"],

View File

@ -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));

View File

@ -8,7 +8,8 @@
] ]
}, },
"include": [ "include": [
<%- extraInclude %>
"src/main.server.tsx", "src/main.server.tsx",
"server.ts", "server.ts"
] ]
} }

View File

@ -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[];
} }

View File

@ -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"],

View 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'],
});
});
});

View File

@ -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,
} }
); );

View File

@ -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

View File

@ -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';