Skip to content

Commit c37007e

Browse files
authored
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.
1 parent 4f8b407 commit c37007e

File tree

6 files changed

+359
-16
lines changed

6 files changed

+359
-16
lines changed

packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,154 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`app --minimal should generate a correct setup when --bundler=rspack and ssr 1`] = `
4+
"
5+
import { createConfig }from '@nx/angular-rspack';
6+
7+
8+
export default createConfig({
9+
options: {
10+
root: __dirname,
11+
12+
"outputPath": {
13+
"base": "../dist/app2"
14+
},
15+
"index": "./src/index.html",
16+
"browser": "./src/main.ts",
17+
"polyfills": [
18+
"./zone.js"
19+
],
20+
"tsConfig": "./tsconfig.app.json",
21+
"assets": [
22+
{
23+
"glob": "**/*",
24+
"input": "./public"
25+
}
26+
],
27+
"styles": [
28+
"./src/styles.css"
29+
],
30+
"scripts": [],
31+
"devServer": {},
32+
"ssr": {
33+
"entry": "./src/server.ts"
34+
},
35+
"server": "./src/main.server.ts"
36+
37+
}
38+
}, {
39+
production: {
40+
options: {
41+
42+
"budgets": [
43+
{
44+
"type": "initial",
45+
"maximumWarning": "500kb",
46+
"maximumError": "1mb"
47+
},
48+
{
49+
"type": "anyComponentStyle",
50+
"maximumWarning": "4kb",
51+
"maximumError": "8kb"
52+
}
53+
],
54+
"outputHashing": "all",
55+
"devServer": {}
56+
57+
}
58+
},
59+
60+
development: {
61+
options: {
62+
63+
"optimization": false,
64+
"vendorChunk": true,
65+
"extractLicenses": false,
66+
"sourceMap": true,
67+
"namedChunks": true,
68+
"devServer": {}
69+
70+
}
71+
}});
72+
73+
"
74+
`;
75+
76+
exports[`app --minimal should generate a correct setup when --bundler=rspack and ssr 2`] = `
77+
"import 'zone.js/node';
78+
79+
import { APP_BASE_HREF } from '@angular/common';
80+
import { CommonEngine } from '@angular/ssr/node';
81+
import * as express from 'express';
82+
import { existsSync } from 'node:fs';
83+
import { join } from 'node:path';
84+
import bootstrap from './main.server';
85+
86+
// The Express app is exported so that it can be used by serverless Functions.
87+
export function app(): express.Express {
88+
const server = express();
89+
const distFolder = join(process.cwd(), "../dist/app2/browser");
90+
const indexHtml = existsSync(join(distFolder, 'index.original.html'))
91+
? join(distFolder, 'index.original.html')
92+
: join(distFolder, 'index.html');
93+
94+
const commonEngine = new CommonEngine();
95+
96+
server.set('view engine', 'html');
97+
server.set('views', distFolder);
98+
99+
// Example Express Rest API endpoints
100+
// server.get('/api/**', (req, res) => { });
101+
// Serve static files from /browser
102+
server.get(
103+
'*.*',
104+
express.static(distFolder, {
105+
maxAge: '1y',
106+
})
107+
);
108+
109+
// All regular routes use the Angular engine
110+
server.get('*', (req, res, next) => {
111+
const { protocol, originalUrl, baseUrl, headers } = req;
112+
113+
commonEngine
114+
.render({
115+
bootstrap,
116+
documentFilePath: indexHtml,
117+
url: \`\${protocol}://\${headers.host}\${originalUrl}\`,
118+
publicPath: distFolder,
119+
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
120+
})
121+
.then((html) => res.send(html))
122+
.catch((err) => next(err));
123+
});
124+
125+
return server;
126+
}
127+
128+
function run(): void {
129+
const port = process.env['PORT'] || 4000;
130+
131+
// Start up the Node server
132+
const server = app();
133+
server.listen(port, () => {
134+
console.log(\`Node Express server listening on http://localhost:\${port}\`);
135+
});
136+
}
137+
138+
// Webpack will replace 'require' with '__webpack_require__'
139+
// '__non_webpack_require__' is a proxy to Node 'require'
140+
// The below code is to ensure that the server is run only when not requiring the bundle.
141+
declare const __non_webpack_require__: NodeRequire;
142+
const mainModule = __non_webpack_require__.main;
143+
const moduleFilename = (mainModule && mainModule.filename) || '';
144+
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
145+
run();
146+
}
147+
148+
export default bootstrap;
149+
"
150+
`;
151+
3152
exports[`app --minimal should generate a correct setup when --bundler=rspack including a correct config file and no build target 1`] = `
4153
"
5154
import { createConfig }from '@nx/angular-rspack';

packages/angular/src/generators/application/application.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,6 +1258,18 @@ describe('app', () => {
12581258
expect(appTree.read('app1/rspack.config.ts', 'utf-8')).toMatchSnapshot();
12591259
});
12601260

1261+
it('should generate a correct setup when --bundler=rspack and ssr', async () => {
1262+
await generateApp(appTree, 'app2', {
1263+
bundler: 'rspack',
1264+
ssr: true,
1265+
});
1266+
1267+
const project = readProjectConfiguration(appTree, 'app2');
1268+
expect(appTree.exists('app2/rspack.config.ts')).toBeTruthy();
1269+
expect(appTree.read('app2/rspack.config.ts', 'utf-8')).toMatchSnapshot();
1270+
expect(appTree.read('app2/src/server.ts', 'utf-8')).toMatchSnapshot();
1271+
});
1272+
12611273
it('should generate use crystal jest when --bundler=rspack', async () => {
12621274
await generateApp(appTree, 'app1', {
12631275
bundler: 'rspack',

packages/angular/src/generators/application/application.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import {
2-
addDependenciesToPackageJson,
32
formatFiles,
3+
generateFiles,
44
GeneratorCallback,
55
installPackagesTask,
6+
joinPathFragments,
67
offsetFromRoot,
78
readNxJson,
89
Tree,
@@ -115,6 +116,22 @@ export async function applicationGenerator(
115116
skipInstall: options.skipPackageJson,
116117
skipFormat: true,
117118
});
119+
120+
if (options.ssr) {
121+
generateFiles(
122+
tree,
123+
joinPathFragments(__dirname, './files/rspack-ssr'),
124+
options.appProjectSourceRoot,
125+
{
126+
pathToDistFolder: joinPathFragments(
127+
offsetFromRoot(options.appProjectRoot),
128+
options.outputPath,
129+
'browser'
130+
),
131+
tmpl: '',
132+
}
133+
);
134+
}
118135
}
119136

120137
if (!options.skipFormat) {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import 'zone.js/node';
2+
3+
import { APP_BASE_HREF } from '@angular/common';
4+
import { CommonEngine } from '@angular/ssr/node';
5+
import * as express from 'express';
6+
import { existsSync } from 'node:fs';
7+
import { join } from 'node:path';
8+
import bootstrap from './main.server';
9+
10+
// The Express app is exported so that it can be used by serverless Functions.
11+
export function app(): express.Express {
12+
const server = express();
13+
const distFolder = join(process.cwd(), "<%= pathToDistFolder %>");
14+
const indexHtml = existsSync(join(distFolder, 'index.original.html'))
15+
? join(distFolder, 'index.original.html')
16+
: join(distFolder, 'index.html');
17+
18+
const commonEngine = new CommonEngine();
19+
20+
server.set('view engine', 'html');
21+
server.set('views', distFolder);
22+
23+
// Example Express Rest API endpoints
24+
// server.get('/api/**', (req, res) => { });
25+
// Serve static files from /browser
26+
server.get(
27+
'*.*',
28+
express.static(distFolder, {
29+
maxAge: '1y',
30+
})
31+
);
32+
33+
// All regular routes use the Angular engine
34+
server.get('*', (req, res, next) => {
35+
const { protocol, originalUrl, baseUrl, headers } = req;
36+
37+
commonEngine
38+
.render({
39+
bootstrap,
40+
documentFilePath: indexHtml,
41+
url: `${protocol}://${headers.host}${originalUrl}`,
42+
publicPath: distFolder,
43+
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
44+
})
45+
.then((html) => res.send(html))
46+
.catch((err) => next(err));
47+
});
48+
49+
return server;
50+
}
51+
52+
function run(): void {
53+
const port = process.env['PORT'] || 4000;
54+
55+
// Start up the Node server
56+
const server = app();
57+
server.listen(port, () => {
58+
console.log(`Node Express server listening on http://localhost:${port}`);
59+
});
60+
}
61+
62+
// Webpack will replace 'require' with '__webpack_require__'
63+
// '__non_webpack_require__' is a proxy to Node 'require'
64+
// The below code is to ensure that the server is run only when not requiring the bundle.
65+
declare const __non_webpack_require__: NodeRequire;
66+
const mainModule = __non_webpack_require__.main;
67+
const moduleFilename = (mainModule && mainModule.filename) || '';
68+
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
69+
run();
70+
}
71+
72+
export default bootstrap;

packages/angular/src/generators/convert-to-rspack/convert-to-rspack.spec.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,99 @@ describe('convert-to-rspack', () => {
102102
expect(updatedProject.targets.serve).not.toBeDefined();
103103
});
104104

105+
it('should convert a ssr angular webpack application to rspack', async () => {
106+
// ARRANGE
107+
const tree = createTreeWithEmptyWorkspace();
108+
109+
addProjectConfiguration(tree, 'app', {
110+
root: 'apps/app',
111+
sourceRoot: 'apps/app/src',
112+
projectType: 'application',
113+
targets: {
114+
build: {
115+
executor: '@angular-devkit/build-angular:browser',
116+
options: {
117+
outputPath: 'dist/apps/app',
118+
index: 'apps/app/src/index.html',
119+
main: 'apps/app/src/main.ts',
120+
polyfills: ['tslib'], // zone.js is not in nx repo's node_modules so simulating it with a package that is
121+
tsConfig: 'apps/app/tsconfig.app.json',
122+
assets: [
123+
'apps/app/src/favicon.ico',
124+
'apps/app/src/assets',
125+
{ input: 'apps/app/public', glob: '**/*' },
126+
],
127+
styles: ['apps/app/src/styles.scss'],
128+
scripts: [],
129+
},
130+
},
131+
server: {
132+
executor: '@angular-devkit/build-angular:server',
133+
options: {
134+
main: 'apps/app/src/server.ts',
135+
},
136+
},
137+
},
138+
});
139+
140+
writeJson(tree, 'apps/app/tsconfig.json', {});
141+
updateJson(tree, 'package.json', (json) => {
142+
json.scripts ??= {};
143+
json.scripts.build = 'nx build';
144+
return json;
145+
});
146+
147+
// ACT
148+
await convertToRspack(tree, { project: 'app' });
149+
150+
// ASSERT
151+
const updatedProject = readProjectConfiguration(tree, 'app');
152+
const pkgJson = readJson(tree, 'package.json');
153+
const nxJson = readNxJson(tree);
154+
expect(tree.read('apps/app/rspack.config.ts', 'utf-8'))
155+
.toMatchInlineSnapshot(`
156+
"import { createConfig } from '@nx/angular-rspack';
157+
158+
export default createConfig({
159+
options: {
160+
root: __dirname,
161+
162+
outputPath: {
163+
base: '../../dist/apps/app',
164+
},
165+
index: './src/index.html',
166+
browser: './src/main.ts',
167+
polyfills: ['tslib'],
168+
tsConfig: './tsconfig.app.json',
169+
assets: [
170+
'./src/favicon.ico',
171+
'./src/assets',
172+
{
173+
input: './public',
174+
glob: '**/*',
175+
},
176+
],
177+
styles: ['./src/styles.scss'],
178+
scripts: [],
179+
ssr: {
180+
entry: './src/server.ts',
181+
},
182+
server: './src/main.server.ts',
183+
},
184+
});
185+
"
186+
`);
187+
expect(pkgJson.devDependencies['@nx/angular-rspack']).toBeDefined();
188+
expect(
189+
nxJson.plugins.find((p) =>
190+
typeof p === 'string' ? false : p.plugin === '@nx/rspack/plugin'
191+
)
192+
).toBeDefined();
193+
expect(pkgJson.scripts?.build).toBeUndefined();
194+
expect(updatedProject.targets.build).not.toBeDefined();
195+
expect(updatedProject.targets.serve).not.toBeDefined();
196+
});
197+
105198
it('should normalize paths to libs in workspace correctly', async () => {
106199
// ARRANGE
107200
const tree = createTreeWithEmptyWorkspace();

0 commit comments

Comments
 (0)