fix(angular): handle ssr with convert-to-rspack (#30752)
## Current Behavior The `convert-to-rspack` generator for `@nx/angular` does not currently handle SSR Webpack applications correctly. ## Expected Behavior Ensure that the `convert-to-rspack` generator handles SSR correctly.
This commit is contained in:
parent
4f8b407a75
commit
c37007ec6c
@ -1,5 +1,154 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`app --minimal should generate a correct setup when --bundler=rspack and ssr 1`] = `
|
||||||
|
"
|
||||||
|
import { createConfig }from '@nx/angular-rspack';
|
||||||
|
|
||||||
|
|
||||||
|
export default createConfig({
|
||||||
|
options: {
|
||||||
|
root: __dirname,
|
||||||
|
|
||||||
|
"outputPath": {
|
||||||
|
"base": "../dist/app2"
|
||||||
|
},
|
||||||
|
"index": "./src/index.html",
|
||||||
|
"browser": "./src/main.ts",
|
||||||
|
"polyfills": [
|
||||||
|
"./zone.js"
|
||||||
|
],
|
||||||
|
"tsConfig": "./tsconfig.app.json",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "./public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"./src/styles.css"
|
||||||
|
],
|
||||||
|
"scripts": [],
|
||||||
|
"devServer": {},
|
||||||
|
"ssr": {
|
||||||
|
"entry": "./src/server.ts"
|
||||||
|
},
|
||||||
|
"server": "./src/main.server.ts"
|
||||||
|
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
production: {
|
||||||
|
options: {
|
||||||
|
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kb",
|
||||||
|
"maximumError": "1mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "4kb",
|
||||||
|
"maximumError": "8kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all",
|
||||||
|
"devServer": {}
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
development: {
|
||||||
|
options: {
|
||||||
|
|
||||||
|
"optimization": false,
|
||||||
|
"vendorChunk": true,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"namedChunks": true,
|
||||||
|
"devServer": {}
|
||||||
|
|
||||||
|
}
|
||||||
|
}});
|
||||||
|
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`app --minimal should generate a correct setup when --bundler=rspack and ssr 2`] = `
|
||||||
|
"import 'zone.js/node';
|
||||||
|
|
||||||
|
import { APP_BASE_HREF } from '@angular/common';
|
||||||
|
import { CommonEngine } from '@angular/ssr/node';
|
||||||
|
import * as express from 'express';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import bootstrap from './main.server';
|
||||||
|
|
||||||
|
// The Express app is exported so that it can be used by serverless Functions.
|
||||||
|
export function app(): express.Express {
|
||||||
|
const server = express();
|
||||||
|
const distFolder = join(process.cwd(), "../dist/app2/browser");
|
||||||
|
const indexHtml = existsSync(join(distFolder, 'index.original.html'))
|
||||||
|
? join(distFolder, 'index.original.html')
|
||||||
|
: join(distFolder, 'index.html');
|
||||||
|
|
||||||
|
const commonEngine = new CommonEngine();
|
||||||
|
|
||||||
|
server.set('view engine', 'html');
|
||||||
|
server.set('views', distFolder);
|
||||||
|
|
||||||
|
// Example Express Rest API endpoints
|
||||||
|
// server.get('/api/**', (req, res) => { });
|
||||||
|
// Serve static files from /browser
|
||||||
|
server.get(
|
||||||
|
'*.*',
|
||||||
|
express.static(distFolder, {
|
||||||
|
maxAge: '1y',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// All regular routes use the Angular engine
|
||||||
|
server.get('*', (req, res, next) => {
|
||||||
|
const { protocol, originalUrl, baseUrl, headers } = req;
|
||||||
|
|
||||||
|
commonEngine
|
||||||
|
.render({
|
||||||
|
bootstrap,
|
||||||
|
documentFilePath: indexHtml,
|
||||||
|
url: \`\${protocol}://\${headers.host}\${originalUrl}\`,
|
||||||
|
publicPath: distFolder,
|
||||||
|
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
|
||||||
|
})
|
||||||
|
.then((html) => res.send(html))
|
||||||
|
.catch((err) => next(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(): void {
|
||||||
|
const port = process.env['PORT'] || 4000;
|
||||||
|
|
||||||
|
// Start up the Node server
|
||||||
|
const server = app();
|
||||||
|
server.listen(port, () => {
|
||||||
|
console.log(\`Node Express server listening on http://localhost:\${port}\`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Webpack will replace 'require' with '__webpack_require__'
|
||||||
|
// '__non_webpack_require__' is a proxy to Node 'require'
|
||||||
|
// The below code is to ensure that the server is run only when not requiring the bundle.
|
||||||
|
declare const __non_webpack_require__: NodeRequire;
|
||||||
|
const mainModule = __non_webpack_require__.main;
|
||||||
|
const moduleFilename = (mainModule && mainModule.filename) || '';
|
||||||
|
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default bootstrap;
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`app --minimal should generate a correct setup when --bundler=rspack including a correct config file and no build target 1`] = `
|
exports[`app --minimal should generate a correct setup when --bundler=rspack including a correct config file and no build target 1`] = `
|
||||||
"
|
"
|
||||||
import { createConfig }from '@nx/angular-rspack';
|
import { createConfig }from '@nx/angular-rspack';
|
||||||
|
|||||||
@ -1258,6 +1258,18 @@ describe('app', () => {
|
|||||||
expect(appTree.read('app1/rspack.config.ts', 'utf-8')).toMatchSnapshot();
|
expect(appTree.read('app1/rspack.config.ts', 'utf-8')).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should generate a correct setup when --bundler=rspack and ssr', async () => {
|
||||||
|
await generateApp(appTree, 'app2', {
|
||||||
|
bundler: 'rspack',
|
||||||
|
ssr: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const project = readProjectConfiguration(appTree, 'app2');
|
||||||
|
expect(appTree.exists('app2/rspack.config.ts')).toBeTruthy();
|
||||||
|
expect(appTree.read('app2/rspack.config.ts', 'utf-8')).toMatchSnapshot();
|
||||||
|
expect(appTree.read('app2/src/server.ts', 'utf-8')).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
it('should generate use crystal jest when --bundler=rspack', async () => {
|
it('should generate use crystal jest when --bundler=rspack', async () => {
|
||||||
await generateApp(appTree, 'app1', {
|
await generateApp(appTree, 'app1', {
|
||||||
bundler: 'rspack',
|
bundler: 'rspack',
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
addDependenciesToPackageJson,
|
|
||||||
formatFiles,
|
formatFiles,
|
||||||
|
generateFiles,
|
||||||
GeneratorCallback,
|
GeneratorCallback,
|
||||||
installPackagesTask,
|
installPackagesTask,
|
||||||
|
joinPathFragments,
|
||||||
offsetFromRoot,
|
offsetFromRoot,
|
||||||
readNxJson,
|
readNxJson,
|
||||||
Tree,
|
Tree,
|
||||||
@ -115,6 +116,22 @@ export async function applicationGenerator(
|
|||||||
skipInstall: options.skipPackageJson,
|
skipInstall: options.skipPackageJson,
|
||||||
skipFormat: true,
|
skipFormat: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (options.ssr) {
|
||||||
|
generateFiles(
|
||||||
|
tree,
|
||||||
|
joinPathFragments(__dirname, './files/rspack-ssr'),
|
||||||
|
options.appProjectSourceRoot,
|
||||||
|
{
|
||||||
|
pathToDistFolder: joinPathFragments(
|
||||||
|
offsetFromRoot(options.appProjectRoot),
|
||||||
|
options.outputPath,
|
||||||
|
'browser'
|
||||||
|
),
|
||||||
|
tmpl: '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.skipFormat) {
|
if (!options.skipFormat) {
|
||||||
|
|||||||
@ -0,0 +1,72 @@
|
|||||||
|
import 'zone.js/node';
|
||||||
|
|
||||||
|
import { APP_BASE_HREF } from '@angular/common';
|
||||||
|
import { CommonEngine } from '@angular/ssr/node';
|
||||||
|
import * as express from 'express';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import bootstrap from './main.server';
|
||||||
|
|
||||||
|
// The Express app is exported so that it can be used by serverless Functions.
|
||||||
|
export function app(): express.Express {
|
||||||
|
const server = express();
|
||||||
|
const distFolder = join(process.cwd(), "<%= pathToDistFolder %>");
|
||||||
|
const indexHtml = existsSync(join(distFolder, 'index.original.html'))
|
||||||
|
? join(distFolder, 'index.original.html')
|
||||||
|
: join(distFolder, 'index.html');
|
||||||
|
|
||||||
|
const commonEngine = new CommonEngine();
|
||||||
|
|
||||||
|
server.set('view engine', 'html');
|
||||||
|
server.set('views', distFolder);
|
||||||
|
|
||||||
|
// Example Express Rest API endpoints
|
||||||
|
// server.get('/api/**', (req, res) => { });
|
||||||
|
// Serve static files from /browser
|
||||||
|
server.get(
|
||||||
|
'*.*',
|
||||||
|
express.static(distFolder, {
|
||||||
|
maxAge: '1y',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// All regular routes use the Angular engine
|
||||||
|
server.get('*', (req, res, next) => {
|
||||||
|
const { protocol, originalUrl, baseUrl, headers } = req;
|
||||||
|
|
||||||
|
commonEngine
|
||||||
|
.render({
|
||||||
|
bootstrap,
|
||||||
|
documentFilePath: indexHtml,
|
||||||
|
url: `${protocol}://${headers.host}${originalUrl}`,
|
||||||
|
publicPath: distFolder,
|
||||||
|
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
|
||||||
|
})
|
||||||
|
.then((html) => res.send(html))
|
||||||
|
.catch((err) => next(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(): void {
|
||||||
|
const port = process.env['PORT'] || 4000;
|
||||||
|
|
||||||
|
// Start up the Node server
|
||||||
|
const server = app();
|
||||||
|
server.listen(port, () => {
|
||||||
|
console.log(`Node Express server listening on http://localhost:${port}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Webpack will replace 'require' with '__webpack_require__'
|
||||||
|
// '__non_webpack_require__' is a proxy to Node 'require'
|
||||||
|
// The below code is to ensure that the server is run only when not requiring the bundle.
|
||||||
|
declare const __non_webpack_require__: NodeRequire;
|
||||||
|
const mainModule = __non_webpack_require__.main;
|
||||||
|
const moduleFilename = (mainModule && mainModule.filename) || '';
|
||||||
|
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default bootstrap;
|
||||||
@ -102,6 +102,99 @@ describe('convert-to-rspack', () => {
|
|||||||
expect(updatedProject.targets.serve).not.toBeDefined();
|
expect(updatedProject.targets.serve).not.toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should convert a ssr angular webpack application to rspack', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const tree = createTreeWithEmptyWorkspace();
|
||||||
|
|
||||||
|
addProjectConfiguration(tree, 'app', {
|
||||||
|
root: 'apps/app',
|
||||||
|
sourceRoot: 'apps/app/src',
|
||||||
|
projectType: 'application',
|
||||||
|
targets: {
|
||||||
|
build: {
|
||||||
|
executor: '@angular-devkit/build-angular:browser',
|
||||||
|
options: {
|
||||||
|
outputPath: 'dist/apps/app',
|
||||||
|
index: 'apps/app/src/index.html',
|
||||||
|
main: 'apps/app/src/main.ts',
|
||||||
|
polyfills: ['tslib'], // zone.js is not in nx repo's node_modules so simulating it with a package that is
|
||||||
|
tsConfig: 'apps/app/tsconfig.app.json',
|
||||||
|
assets: [
|
||||||
|
'apps/app/src/favicon.ico',
|
||||||
|
'apps/app/src/assets',
|
||||||
|
{ input: 'apps/app/public', glob: '**/*' },
|
||||||
|
],
|
||||||
|
styles: ['apps/app/src/styles.scss'],
|
||||||
|
scripts: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
executor: '@angular-devkit/build-angular:server',
|
||||||
|
options: {
|
||||||
|
main: 'apps/app/src/server.ts',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
writeJson(tree, 'apps/app/tsconfig.json', {});
|
||||||
|
updateJson(tree, 'package.json', (json) => {
|
||||||
|
json.scripts ??= {};
|
||||||
|
json.scripts.build = 'nx build';
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await convertToRspack(tree, { project: 'app' });
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
const updatedProject = readProjectConfiguration(tree, 'app');
|
||||||
|
const pkgJson = readJson(tree, 'package.json');
|
||||||
|
const nxJson = readNxJson(tree);
|
||||||
|
expect(tree.read('apps/app/rspack.config.ts', 'utf-8'))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"import { createConfig } from '@nx/angular-rspack';
|
||||||
|
|
||||||
|
export default createConfig({
|
||||||
|
options: {
|
||||||
|
root: __dirname,
|
||||||
|
|
||||||
|
outputPath: {
|
||||||
|
base: '../../dist/apps/app',
|
||||||
|
},
|
||||||
|
index: './src/index.html',
|
||||||
|
browser: './src/main.ts',
|
||||||
|
polyfills: ['tslib'],
|
||||||
|
tsConfig: './tsconfig.app.json',
|
||||||
|
assets: [
|
||||||
|
'./src/favicon.ico',
|
||||||
|
'./src/assets',
|
||||||
|
{
|
||||||
|
input: './public',
|
||||||
|
glob: '**/*',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
styles: ['./src/styles.scss'],
|
||||||
|
scripts: [],
|
||||||
|
ssr: {
|
||||||
|
entry: './src/server.ts',
|
||||||
|
},
|
||||||
|
server: './src/main.server.ts',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
expect(pkgJson.devDependencies['@nx/angular-rspack']).toBeDefined();
|
||||||
|
expect(
|
||||||
|
nxJson.plugins.find((p) =>
|
||||||
|
typeof p === 'string' ? false : p.plugin === '@nx/rspack/plugin'
|
||||||
|
)
|
||||||
|
).toBeDefined();
|
||||||
|
expect(pkgJson.scripts?.build).toBeUndefined();
|
||||||
|
expect(updatedProject.targets.build).not.toBeDefined();
|
||||||
|
expect(updatedProject.targets.serve).not.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('should normalize paths to libs in workspace correctly', async () => {
|
it('should normalize paths to libs in workspace correctly', async () => {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
const tree = createTreeWithEmptyWorkspace();
|
const tree = createTreeWithEmptyWorkspace();
|
||||||
|
|||||||
@ -31,7 +31,9 @@ import { prompt } from 'enquirer';
|
|||||||
const SUPPORTED_EXECUTORS = [
|
const SUPPORTED_EXECUTORS = [
|
||||||
'@angular-devkit/build-angular:browser',
|
'@angular-devkit/build-angular:browser',
|
||||||
'@angular-devkit/build-angular:dev-server',
|
'@angular-devkit/build-angular:dev-server',
|
||||||
|
'@angular-devkit/build-angular:server',
|
||||||
'@nx/angular:webpack-browser',
|
'@nx/angular:webpack-browser',
|
||||||
|
'@nx/angular:webpack-server',
|
||||||
'@nx/angular:dev-server',
|
'@nx/angular:dev-server',
|
||||||
'@nx/angular:module-federation-dev-server',
|
'@nx/angular:module-federation-dev-server',
|
||||||
];
|
];
|
||||||
@ -41,21 +43,7 @@ const RENAMED_OPTIONS = {
|
|||||||
ngswConfigPath: 'serviceWorker',
|
ngswConfigPath: 'serviceWorker',
|
||||||
};
|
};
|
||||||
|
|
||||||
const REMOVED_OPTIONS = [
|
const REMOVED_OPTIONS = ['buildOptimizer', 'buildTarget', 'browserTarget'];
|
||||||
'publicHost',
|
|
||||||
'disableHostCheck',
|
|
||||||
'resourcesOutputPath',
|
|
||||||
'routesFile',
|
|
||||||
'routes',
|
|
||||||
'discoverRoutes',
|
|
||||||
'appModuleBundle',
|
|
||||||
'inputIndexPath',
|
|
||||||
'outputIndexPath',
|
|
||||||
'buildOptimizer',
|
|
||||||
'deployUrl',
|
|
||||||
'buildTarget',
|
|
||||||
'browserTarget',
|
|
||||||
];
|
|
||||||
|
|
||||||
function normalizeFromProjectRoot(
|
function normalizeFromProjectRoot(
|
||||||
tree: Tree,
|
tree: Tree,
|
||||||
@ -382,6 +370,18 @@ export async function convertToRspack(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
buildTargetNames.push(targetName);
|
buildTargetNames.push(targetName);
|
||||||
|
} else if (
|
||||||
|
target.executor === '@angular-devkit/build-angular:server' ||
|
||||||
|
target.executor === '@nx/angular:webpack-server'
|
||||||
|
) {
|
||||||
|
createConfigOptions.ssr ??= {};
|
||||||
|
createConfigOptions.ssr.entry ??= normalizeFromProjectRoot(
|
||||||
|
tree,
|
||||||
|
target.options.main,
|
||||||
|
project.root
|
||||||
|
);
|
||||||
|
createConfigOptions.server = './src/main.server.ts';
|
||||||
|
buildTargetNames.push(targetName);
|
||||||
} else if (
|
} else if (
|
||||||
target.executor === '@angular-devkit/build-angular:dev-server' ||
|
target.executor === '@angular-devkit/build-angular:dev-server' ||
|
||||||
target.executor === '@nx/angular:dev-server' ||
|
target.executor === '@nx/angular:dev-server' ||
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user