Skip to content

Commit 05fe63d

Browse files
committed
fix(@angular-devkit/build-angular): display warning when using resourcesOutputPath with esbuild builder
`resourcesOutputPath` option is not supported when using the esbuild based builder. Closes angular#25658
1 parent 340ae3c commit 05fe63d

File tree

6 files changed

+111
-52
lines changed

6 files changed

+111
-52
lines changed

packages/angular_devkit/build_angular/src/builders/application/execute-build.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ export async function executeBuild(
181181
);
182182

183183
const { output, warnings, errors } = await prerenderPages(
184+
workspaceRoot,
184185
options.tsconfig,
185186
appShellOptions,
186187
prerenderOptions,

packages/angular_devkit/build_angular/src/builders/browser-esbuild/builder-status-warnings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const UNSUPPORTED_OPTIONS: Array<keyof BrowserBuilderOptions> = [
2727
// * Unused by builder and will be removed in a future release
2828
'namedChunks',
2929
'vendorChunk',
30+
'resourcesOutputPath',
3031

3132
// * Currently unsupported by esbuild
3233
'webWorkerTsConfig',

packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts

Lines changed: 52 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ export async function setupServer(
335335
next: Connect.NextFunction,
336336
) {
337337
const url = req.originalUrl;
338-
if (!url) {
338+
if (!url || url.endsWith('.html')) {
339339
next();
340340

341341
return;
@@ -348,37 +348,23 @@ export async function setupServer(
348348
return;
349349
}
350350

351-
server
352-
.transformIndexHtml(url, Buffer.from(rawHtml).toString('utf-8'))
353-
.then(async (html) => {
354-
const { content } = await renderPage({
355-
document: html,
356-
route: pathnameWithoutServePath(url, serverOptions),
357-
serverContext: 'ssr',
358-
loadBundle: (path: string) =>
359-
server.ssrLoadModule(path.slice(1)) as ReturnType<
360-
NonNullable<RenderOptions['loadBundle']>
361-
>,
362-
// Files here are only needed for critical CSS inlining.
363-
outputFiles: {},
364-
// TODO: add support for critical css inlining.
365-
inlineCriticalCss: false,
366-
});
367-
368-
if (content) {
369-
res.setHeader('Content-Type', 'text/html');
370-
res.setHeader('Cache-Control', 'no-cache');
371-
if (serverOptions.headers) {
372-
Object.entries(serverOptions.headers).forEach(([name, value]) =>
373-
res.setHeader(name, value),
374-
);
375-
}
376-
res.end(content);
377-
} else {
378-
next();
379-
}
380-
})
381-
.catch((error) => next(error));
351+
transformIndexHtmlAndAddHeaders(url, rawHtml, res, next, async (html) => {
352+
const { content } = await renderPage({
353+
document: html,
354+
route: pathnameWithoutServePath(url, serverOptions),
355+
serverContext: 'ssr',
356+
loadBundle: (path: string) =>
357+
server.ssrLoadModule(path.slice(1)) as ReturnType<
358+
NonNullable<RenderOptions['loadBundle']>
359+
>,
360+
// Files here are only needed for critical CSS inlining.
361+
outputFiles: {},
362+
// TODO: add support for critical css inlining.
363+
inlineCriticalCss: false,
364+
});
365+
366+
return content;
367+
});
382368
}
383369

384370
if (ssr) {
@@ -399,19 +385,7 @@ export async function setupServer(
399385
if (pathname === '/' || pathname === `/index.html`) {
400386
const rawHtml = outputFiles.get('/index.html')?.contents;
401387
if (rawHtml) {
402-
server
403-
.transformIndexHtml(req.url, Buffer.from(rawHtml).toString('utf-8'))
404-
.then((processedHtml) => {
405-
res.setHeader('Content-Type', 'text/html');
406-
res.setHeader('Cache-Control', 'no-cache');
407-
if (serverOptions.headers) {
408-
Object.entries(serverOptions.headers).forEach(([name, value]) =>
409-
res.setHeader(name, value),
410-
);
411-
}
412-
res.end(processedHtml);
413-
})
414-
.catch((error) => next(error));
388+
transformIndexHtmlAndAddHeaders(req.url, rawHtml, res, next);
415389

416390
return;
417391
}
@@ -420,6 +394,39 @@ export async function setupServer(
420394
next();
421395
});
422396
};
397+
398+
function transformIndexHtmlAndAddHeaders(
399+
url: string,
400+
rawHtml: Uint8Array,
401+
res: ServerResponse<import('http').IncomingMessage>,
402+
next: Connect.NextFunction,
403+
additionalTransformer?: (html: string) => Promise<string | undefined>,
404+
) {
405+
server
406+
.transformIndexHtml(url, Buffer.from(rawHtml).toString('utf-8'))
407+
.then(async (processedHtml) => {
408+
if (additionalTransformer) {
409+
const content = await additionalTransformer(processedHtml);
410+
if (!content) {
411+
next();
412+
413+
return;
414+
}
415+
416+
processedHtml = content;
417+
}
418+
419+
res.setHeader('Content-Type', 'text/html');
420+
res.setHeader('Cache-Control', 'no-cache');
421+
if (serverOptions.headers) {
422+
Object.entries(serverOptions.headers).forEach(([name, value]) =>
423+
res.setHeader(name, value),
424+
);
425+
}
426+
res.end(processedHtml);
427+
})
428+
.catch((error) => next(error));
429+
}
423430
},
424431
},
425432
],

packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ export function createServerCodeBundleOptions(
164164
const polyfills = [`import '@angular/platform-server/init';`];
165165

166166
if (options.polyfills?.includes('zone.js')) {
167-
polyfills.push(`import 'zone.js/node';`);
167+
polyfills.push(`import 'zone.js/fesm2015/zone-node.js';`);
168168
}
169169

170170
if (jit) {

packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-file-loader.ts

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,38 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import { join } from 'node:path';
910
import { workerData } from 'node:worker_threads';
1011
import { fileURLToPath } from 'url';
12+
import { JavaScriptTransformer } from '../../tools/esbuild/javascript-transformer';
1113

1214
/**
1315
* Node.js ESM loader to redirect imports to in memory files.
1416
* @see: https://nodejs.org/api/esm.html#loaders for more information about loaders.
1517
*/
1618

17-
const { outputFiles } = workerData as {
19+
const { outputFiles, workspaceRoot } = workerData as {
1820
outputFiles: Record<string, string>;
21+
workspaceRoot: string;
1922
};
2023

21-
export function resolve(specifier: string, context: {}, nextResolve: Function) {
24+
const TRANSFORMED_FILES: Record<string, string> = {};
25+
const CHUNKS_REGEXP = /file:\/\/\/(main\.server|chunk-\w+)\.mjs/;
26+
const WORKSPACE_ROOT_FILE = new URL(join(workspaceRoot, 'index.mjs'), 'file:').href;
27+
28+
const JAVASCRIPT_TRANSFORMER = new JavaScriptTransformer(
29+
// Always enable JIT linking to support applications built with and without AOT.
30+
// In a development environment the additional scope information does not
31+
// have a negative effect unlike production where final output size is relevant.
32+
{ sourcemap: true, jit: true },
33+
1,
34+
);
35+
36+
export function resolve(
37+
specifier: string,
38+
context: { parentURL: undefined | string },
39+
nextResolve: Function,
40+
) {
2241
if (!isFileProtocol(specifier)) {
2342
const normalizedSpecifier = specifier.replace(/^\.\//, '');
2443
if (normalizedSpecifier in outputFiles) {
@@ -32,12 +51,24 @@ export function resolve(specifier: string, context: {}, nextResolve: Function) {
3251

3352
// Defer to the next hook in the chain, which would be the
3453
// Node.js default resolve if this is the last user-specified loader.
35-
return nextResolve(specifier);
54+
return nextResolve(
55+
specifier,
56+
isBundleEntryPointOrChunk(context) ? { ...context, parentURL: WORKSPACE_ROOT_FILE } : context,
57+
);
3658
}
3759

38-
export function load(url: string, context: { format?: string | null }, nextLoad: Function) {
60+
export async function load(url: string, context: { format?: string | null }, nextLoad: Function) {
3961
if (isFileProtocol(url)) {
40-
const source = outputFiles[fileURLToPath(url).slice(1)]; // Remove leading slash
62+
const filePath = fileURLToPath(url);
63+
let source =
64+
outputFiles[filePath.slice(1)] /* Remove leading slash */ ?? TRANSFORMED_FILES[filePath];
65+
66+
if (source === undefined) {
67+
source = TRANSFORMED_FILES[filePath] = Buffer.from(
68+
await JAVASCRIPT_TRANSFORMER.transformFile(filePath),
69+
).toString('utf-8');
70+
}
71+
4172
if (source !== undefined) {
4273
const { format } = context;
4374

@@ -56,3 +87,15 @@ export function load(url: string, context: { format?: string | null }, nextLoad:
5687
function isFileProtocol(url: string): boolean {
5788
return url.startsWith('file://');
5889
}
90+
91+
function handleProcessExit(): void {
92+
void JAVASCRIPT_TRANSFORMER.close();
93+
}
94+
95+
function isBundleEntryPointOrChunk(context: { parentURL: undefined | string }): boolean {
96+
return !!context.parentURL && CHUNKS_REGEXP.test(context.parentURL);
97+
}
98+
99+
process.once('exit', handleProcessExit);
100+
process.once('SIGINT', handleProcessExit);
101+
process.once('uncaughtException', handleProcessExit);

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface AppShellOptions {
2424
}
2525

2626
export async function prerenderPages(
27+
workspaceRoot: string,
2728
tsConfigPath: string,
2829
appShellOptions: AppShellOptions = {},
2930
prerenderOptions: PrerenderOptions = {},
@@ -52,6 +53,7 @@ export async function prerenderPages(
5253
filename: require.resolve('./render-worker'),
5354
maxThreads: Math.min(allRoutes.size, maxThreads),
5455
workerData: {
56+
workspaceRoot,
5557
outputFiles: outputFilesForWorker,
5658
inlineCriticalCss,
5759
document,
@@ -77,7 +79,12 @@ export async function prerenderPages(
7779
const render: Promise<RenderResult> = renderWorker.run({ route, serverContext });
7880
const renderResult: Promise<void> = render.then(({ content, warnings, errors }) => {
7981
if (content !== undefined) {
80-
const outPath = isAppShellRoute ? 'index.html' : posix.join(route, 'index.html');
82+
const outPath = isAppShellRoute
83+
? 'index.html'
84+
: posix.join(
85+
route.startsWith('/') ? route.slice(1) /* Remove leading slash */ : route,
86+
'index.html',
87+
);
8188
output[outPath] = content;
8289
}
8390

0 commit comments

Comments
 (0)