Skip to content

Commit 0f45e21

Browse files
committed
fix(@angular-devkit/build-angular): resolve and load sourcemaps during prerendering to provide better stacktraces
With this commit when running a build with sourcemaps enabled we enable Node.js Sourcemap support for stack traces. See: https://nodejs.org/dist/latest/docs/api/cli.html#--enable-source-maps for more information about this feature. Before ``` ERROR ReferenceError: window is not defined at new _AppComponent (file:///chunk-HFZN2HE2.mjs:41631:19) at NodeInjectorFactory.AppComponent_Factory [as factory] (file:///chunk-HFZN2HE2.mjs:41635:12) at getNodeInjectable (file:///chunk-HFZN2HE2.mjs:6104:38) at createRootComponent (file:///chunk-HFZN2HE2.mjs:10446:31) at ComponentFactory.create (file:///chunk-HFZN2HE2.mjs:10348:19) at _ApplicationRef.bootstrap (file:///chunk-HFZN2HE2.mjs:13247:40) at file:///chunk-HFZN2HE2.mjs:12938:20 at _ZoneDelegate.invoke (file:///chunk-HFZN2HE2.mjs:320:158) at Object.onInvoke (file:///chunk-HFZN2HE2.mjs:8562:25) at _ZoneDelegate.invoke (file:///chunk-HFZN2HE2.mjs:320:46) ``` Now ``` ERROR ReferenceError: window is not defined at constructor (/usr/xxxx/src/app/app.component.ts:16:17) at NodeInjectorFactory.AppComponent_Factory (/usr/xxxx/src/app/app.component.ts:17:3) at getNodeInjectable (/usr/xxxx/node_modules/@angular/core/fesm2022/core.mjs:4166:38) at createRootComponent (/usr/xxxx/node_modules/@angular/core/fesm2022/core.mjs:14064:31) at ComponentFactory.create (/usr/xxxx/node_modules/@angular/core/fesm2022/core.mjs:13931:19) at _ApplicationRef.bootstrap (/usr/xxxx/node_modules/@angular/core/fesm2022/core.mjs:30650:40) at <anonymous> (/usr/xxxx/node_modules/@angular/core/fesm2022/core.mjs:30163:20) at _ZoneDelegate.invoke (/usr/xxxx/node_modules/zone.js/fesm2015/zone-node.js:344:158) at Object.onInvoke (/usr/xxxx/node_modules/@angular/core/fesm2022/core.mjs:10964:25) at _ZoneDelegate.invoke (/usr/xxxx/node_modules/zone.js/fesm2015/zone-node.js:344:46) ``` Closes #26013
1 parent a136f2e commit 0f45e21

File tree

3 files changed

+82
-6
lines changed

3 files changed

+82
-6
lines changed

packages/angular_devkit/build_angular/src/builders/application/execute-post-bundle.ts

+2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export async function executePostBundleSteps(
4949
serviceWorker,
5050
indexHtmlOptions,
5151
optimizationOptions,
52+
sourcemapOptions,
5253
ssrOptions,
5354
prerenderOptions,
5455
appShellOptions,
@@ -110,6 +111,7 @@ export async function executePostBundleSteps(
110111
prerenderOptions,
111112
outputFiles,
112113
indexContentOutputNoCssInlining,
114+
sourcemapOptions.scripts,
113115
optimizationOptions.styles.inlineCritical,
114116
maxWorkers,
115117
verbose,

packages/angular_devkit/build_angular/src/utils/server-rendering/prerender.ts

+36-6
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import { readFile } from 'node:fs/promises';
10-
import { extname, posix } from 'node:path';
10+
import { extname, join, posix } from 'node:path';
1111
import Piscina from 'piscina';
1212
import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context';
1313
import { getESMLoaderArgs } from './esm-in-memory-loader/node-18-utils';
@@ -33,7 +33,8 @@ export async function prerenderPages(
3333
prerenderOptions: PrerenderOptions = {},
3434
outputFiles: Readonly<BuildOutputFile[]>,
3535
document: string,
36-
inlineCriticalCss?: boolean,
36+
sourcemap = false,
37+
inlineCriticalCss = false,
3738
maxThreads = 1,
3839
verbose = false,
3940
): Promise<{
@@ -45,22 +46,40 @@ export async function prerenderPages(
4546
const warnings: string[] = [];
4647
const errors: string[] = [];
4748
const outputFilesForWorker: Record<string, string> = {};
49+
const serverBundlesSourceMaps = new Map<string, string>();
4850

4951
for (const { text, path, type } of outputFiles) {
50-
if (
52+
const fileExt = extname(path);
53+
if (type === BuildOutputFileType.Server && fileExt === '.map') {
54+
serverBundlesSourceMaps.set(path.slice(0, -4), text);
55+
} else if (
5156
type === BuildOutputFileType.Server || // Contains the server runnable application code
52-
(type === BuildOutputFileType.Browser && extname(path) === '.css') // Global styles for critical CSS inlining.
57+
(type === BuildOutputFileType.Browser && fileExt === '.css') // Global styles for critical CSS inlining.
5358
) {
5459
outputFilesForWorker[path] = text;
5560
}
5661
}
5762

63+
// Inline sourcemap into JS file. This is needed to make Node.js resolve sourcemaps
64+
// when using `--enable-source-maps` when using in memory files.
65+
for (const [filePath, map] of serverBundlesSourceMaps) {
66+
const jsContent = outputFilesForWorker[filePath];
67+
if (jsContent) {
68+
outputFilesForWorker[filePath] =
69+
jsContent +
70+
`\n//# sourceMappingURL=` +
71+
`data:application/json;base64,${Buffer.from(map).toString('base64')}`;
72+
}
73+
}
74+
serverBundlesSourceMaps.clear();
75+
5876
const { routes: allRoutes, warnings: routesWarnings } = await getAllRoutes(
5977
workspaceRoot,
6078
outputFilesForWorker,
6179
document,
6280
appShellOptions,
6381
prerenderOptions,
82+
sourcemap,
6483
verbose,
6584
);
6685

@@ -76,6 +95,11 @@ export async function prerenderPages(
7695
};
7796
}
7897

98+
const workerExecArgv = getESMLoaderArgs();
99+
if (sourcemap) {
100+
workerExecArgv.push('--enable-source-maps');
101+
}
102+
79103
const renderWorker = new Piscina({
80104
filename: require.resolve('./render-worker'),
81105
maxThreads: Math.min(allRoutes.size, maxThreads),
@@ -85,7 +109,7 @@ export async function prerenderPages(
85109
inlineCriticalCss,
86110
document,
87111
} as RenderWorkerData,
88-
execArgv: getESMLoaderArgs(),
112+
execArgv: workerExecArgv,
89113
});
90114

91115
try {
@@ -139,6 +163,7 @@ async function getAllRoutes(
139163
document: string,
140164
appShellOptions: AppShellOptions,
141165
prerenderOptions: PrerenderOptions,
166+
sourcemap: boolean,
142167
verbose: boolean,
143168
): Promise<{ routes: Set<string>; warnings?: string[] }> {
144169
const { routesFile, discoverRoutes } = prerenderOptions;
@@ -160,6 +185,11 @@ async function getAllRoutes(
160185
return { routes };
161186
}
162187

188+
const workerExecArgv = getESMLoaderArgs();
189+
if (sourcemap) {
190+
workerExecArgv.push('--enable-source-maps');
191+
}
192+
163193
const renderWorker = new Piscina({
164194
filename: require.resolve('./routes-extractor-worker'),
165195
maxThreads: 1,
@@ -169,7 +199,7 @@ async function getAllRoutes(
169199
document,
170200
verbose,
171201
} as RoutesExtractorWorkerData,
172-
execArgv: getESMLoaderArgs(),
202+
execArgv: workerExecArgv,
173203
});
174204

175205
const { routes: extractedRoutes, warnings }: RoutersExtractorWorkerResult = await renderWorker
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { ng } from '../../../utils/process';
2+
import { getGlobalVariable } from '../../../utils/env';
3+
import { rimraf, writeMultipleFiles } from '../../../utils/fs';
4+
import { match } from 'node:assert';
5+
import { expectToFail } from '../../../utils/utils';
6+
7+
export default async function () {
8+
const useWebpackBuilder = !getGlobalVariable('argv')['esbuild'];
9+
if (useWebpackBuilder) {
10+
return;
11+
}
12+
13+
// Forcibly remove in case another test doesn't clean itself up.
14+
await rimraf('node_modules/@angular/ssr');
15+
await ng('add', '@angular/ssr', '--skip-confirmation');
16+
17+
await writeMultipleFiles({
18+
'src/app/app.component.ts': `
19+
import { Component } from '@angular/core';
20+
import { CommonModule } from '@angular/common';
21+
import { RouterOutlet } from '@angular/router';
22+
23+
@Component({
24+
selector: 'app-root',
25+
standalone: true,
26+
imports: [CommonModule, RouterOutlet],
27+
templateUrl: './app.component.html',
28+
styleUrls: ['./app.component.css']
29+
})
30+
export class AppComponent {
31+
title = 'test-ssr';
32+
33+
constructor() {
34+
console.log(window)
35+
}
36+
}
37+
`,
38+
});
39+
40+
const { message } = await expectToFail(() =>
41+
ng('build', '--configuration', 'development', '--prerender'),
42+
);
43+
match(message, /window is not defined[.\s\S]*constructor \(.*app\.component\.ts\:\d+:\d+\)/);
44+
}

0 commit comments

Comments
 (0)