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
|
||||
|
||||
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`] = `
|
||||
"
|
||||
import { createConfig }from '@nx/angular-rspack';
|
||||
|
||||
@ -1258,6 +1258,18 @@ describe('app', () => {
|
||||
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 () => {
|
||||
await generateApp(appTree, 'app1', {
|
||||
bundler: 'rspack',
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import {
|
||||
addDependenciesToPackageJson,
|
||||
formatFiles,
|
||||
generateFiles,
|
||||
GeneratorCallback,
|
||||
installPackagesTask,
|
||||
joinPathFragments,
|
||||
offsetFromRoot,
|
||||
readNxJson,
|
||||
Tree,
|
||||
@ -115,6 +116,22 @@ export async function applicationGenerator(
|
||||
skipInstall: options.skipPackageJson,
|
||||
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) {
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
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 () => {
|
||||
// ARRANGE
|
||||
const tree = createTreeWithEmptyWorkspace();
|
||||
|
||||
@ -31,7 +31,9 @@ import { prompt } from 'enquirer';
|
||||
const SUPPORTED_EXECUTORS = [
|
||||
'@angular-devkit/build-angular:browser',
|
||||
'@angular-devkit/build-angular:dev-server',
|
||||
'@angular-devkit/build-angular:server',
|
||||
'@nx/angular:webpack-browser',
|
||||
'@nx/angular:webpack-server',
|
||||
'@nx/angular:dev-server',
|
||||
'@nx/angular:module-federation-dev-server',
|
||||
];
|
||||
@ -41,21 +43,7 @@ const RENAMED_OPTIONS = {
|
||||
ngswConfigPath: 'serviceWorker',
|
||||
};
|
||||
|
||||
const REMOVED_OPTIONS = [
|
||||
'publicHost',
|
||||
'disableHostCheck',
|
||||
'resourcesOutputPath',
|
||||
'routesFile',
|
||||
'routes',
|
||||
'discoverRoutes',
|
||||
'appModuleBundle',
|
||||
'inputIndexPath',
|
||||
'outputIndexPath',
|
||||
'buildOptimizer',
|
||||
'deployUrl',
|
||||
'buildTarget',
|
||||
'browserTarget',
|
||||
];
|
||||
const REMOVED_OPTIONS = ['buildOptimizer', 'buildTarget', 'browserTarget'];
|
||||
|
||||
function normalizeFromProjectRoot(
|
||||
tree: Tree,
|
||||
@ -382,6 +370,18 @@ export async function convertToRspack(
|
||||
}
|
||||
}
|
||||
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 (
|
||||
target.executor === '@angular-devkit/build-angular:dev-server' ||
|
||||
target.executor === '@nx/angular:dev-server' ||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user