Skip to content

Commit bdee1ac

Browse files
alan-agius4clydin
authored andcommitted
refactor(@angular-devkit/build-angular): refactor prerendering code to aid code reusability
This commit refactors the prerending code and makes it more reusable. This is needed to implement SSR support in the dev-server.
1 parent 304ec64 commit bdee1ac

File tree

8 files changed

+64
-60
lines changed

8 files changed

+64
-60
lines changed

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ import {
2828
} from '../../tools/esbuild/utils';
2929
import { copyAssets } from '../../utils/copy-assets';
3030
import { maxWorkers } from '../../utils/environment-options';
31+
import { prerenderPages } from '../../utils/server-rendering/prerender';
3132
import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker';
32-
import { prerenderPages } from '../../utils/ssg/render';
3333
import { getSupportedBrowsers } from '../../utils/supported-browsers';
3434
import { NormalizedApplicationBuildOptions } from './options';
3535

@@ -181,7 +181,6 @@ export async function executeBuild(
181181
);
182182

183183
const { output, warnings, errors } = await prerenderPages(
184-
workspaceRoot,
185184
options.tsconfig,
186185
appShellOptions,
187186
prerenderOptions,

packages/angular_devkit/build_angular/src/builders/application/tests/options/app-shell_spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
106106
harness.useTarget('build', {
107107
...BASE_OPTIONS,
108108
server: 'src/main.server.ts',
109+
polyfills: ['zone.js'],
109110
appShell: true,
110111
});
111112

@@ -122,6 +123,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
122123
harness.useTarget('build', {
123124
...BASE_OPTIONS,
124125
server: 'src/main.server.ts',
126+
polyfills: ['zone.js'],
125127
appShell: true,
126128
styles: ['src/styles.css'],
127129
optimization: {
@@ -150,6 +152,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
150152
harness.useTarget('build', {
151153
...BASE_OPTIONS,
152154
server: 'src/main.server.ts',
155+
polyfills: ['zone.js'],
153156
appShell: true,
154157
styles: ['src/styles.css'],
155158
optimization: {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export function execute(
4646
switchMap(({ builderName, normalizedOptions }) => {
4747
// Use vite-based development server for esbuild-based builds
4848
if (
49+
builderName === '@angular-devkit/build-angular:application' ||
4950
builderName === '@angular-devkit/build-angular:browser-esbuild' ||
5051
normalizedOptions.forceEsbuild
5152
) {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ export function createServerCodeBundleOptions(
177177
importAndExportDec.unshift(`import '@angular/compiler';`);
178178
}
179179

180+
if (options.polyfills?.includes('zone.js')) {
181+
importAndExportDec.unshift(`import 'zone.js/node';`);
182+
}
183+
180184
return {
181185
contents: importAndExportDec.join('\n'),
182186
loader: 'js',

packages/angular_devkit/build_angular/src/utils/ssg/render.ts renamed to packages/angular_devkit/build_angular/src/utils/server-rendering/prerender.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import { OutputFile } from 'esbuild';
1010
import { readFile } from 'node:fs/promises';
1111
import { extname, posix } from 'node:path';
1212
import Piscina from 'piscina';
13-
import type { RenderResult, ServerContext, WorkerData } from './render-worker';
13+
import type { RenderResult, ServerContext } from './render-page';
14+
import type { WorkerData } from './render-worker';
1415

15-
interface prerenderOptions {
16+
interface PrerenderOptions {
1617
routesFile?: string;
1718
discoverRoutes?: boolean;
1819
routes?: string[];
@@ -23,10 +24,9 @@ interface AppShellOptions {
2324
}
2425

2526
export async function prerenderPages(
26-
workspaceRoot: string,
2727
tsConfigPath: string,
2828
appShellOptions: AppShellOptions = {},
29-
prerenderOptions: prerenderOptions = {},
29+
prerenderOptions: PrerenderOptions = {},
3030
outputFiles: Readonly<OutputFile[]>,
3131
document: string,
3232
inlineCriticalCss?: boolean,
@@ -52,7 +52,6 @@ export async function prerenderPages(
5252
filename: require.resolve('./render-worker'),
5353
maxThreads: Math.min(allRoutes.size, maxThreads),
5454
workerData: {
55-
zonePackage: require.resolve('zone.js', { paths: [workspaceRoot] }),
5655
outputFiles: outputFilesForWorker,
5756
inlineCriticalCss,
5857
document,
@@ -109,7 +108,7 @@ export async function prerenderPages(
109108
async function getAllRoutes(
110109
tsConfigPath: string,
111110
appShellOptions: AppShellOptions,
112-
prerenderOptions: prerenderOptions,
111+
prerenderOptions: PrerenderOptions,
113112
): Promise<Set<string>> {
114113
const { routesFile, discoverRoutes, routes: existingRoutes } = prerenderOptions;
115114
const routes = new Set(existingRoutes);

packages/angular_devkit/build_angular/src/utils/ssg/render-worker.ts renamed to packages/angular_devkit/build_angular/src/utils/server-rendering/render-page.ts

Lines changed: 20 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,17 @@
88

99
import type { ApplicationRef, StaticProvider, Type } from '@angular/core';
1010
import type { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/platform-server';
11-
import assert from 'node:assert';
1211
import { basename } from 'node:path';
13-
import { workerData } from 'node:worker_threads';
1412
import { InlineCriticalCssProcessor } from '../index-file/inline-critical-css';
1513
import { loadEsmModule } from '../load-esm';
1614

1715
export interface RenderOptions {
1816
route: string;
1917
serverContext: ServerContext;
18+
outputFiles: Record<string, string>;
19+
document: string;
20+
inlineCriticalCss?: boolean;
21+
loadBundle?: (path: string) => Promise<MainServerBundleExports>;
2022
}
2123

2224
export interface RenderResult {
@@ -25,47 +27,39 @@ export interface RenderResult {
2527
content?: string;
2628
}
2729

28-
export type ServerContext = 'app-shell' | 'ssg';
30+
export type ServerContext = 'app-shell' | 'ssg' | 'ssr';
2931

30-
export interface WorkerData {
31-
zonePackage: string;
32-
outputFiles: Record<string, string>;
33-
document: string;
34-
inlineCriticalCss?: boolean;
35-
}
36-
37-
interface BundleExports {
32+
interface MainServerBundleExports {
3833
/** An internal token that allows providing extra information about the server context. */
39-
ɵSERVER_CONTEXT?: typeof ɵSERVER_CONTEXT;
34+
ɵSERVER_CONTEXT: typeof ɵSERVER_CONTEXT;
4035

4136
/** Render an NgModule application. */
42-
renderModule?: typeof renderModule;
37+
renderModule: typeof renderModule;
4338

4439
/** Method to render a standalone application. */
45-
renderApplication?: typeof renderApplication;
40+
renderApplication: typeof renderApplication;
4641

4742
/** Standalone application bootstrapping function. */
48-
default?: (() => Promise<ApplicationRef>) | Type<unknown>;
43+
default: (() => Promise<ApplicationRef>) | Type<unknown>;
4944
}
5045

51-
/**
52-
* The fully resolved path to the zone.js package that will be loaded during worker initialization.
53-
* This is passed as workerData when setting up the worker via the `piscina` package.
54-
*/
55-
const { zonePackage, outputFiles, document, inlineCriticalCss } = workerData as WorkerData;
56-
5746
/**
5847
* Renders each route in routes and writes them to <outputPath>/<route>/index.html.
5948
*/
60-
async function render({ route, serverContext }: RenderOptions): Promise<RenderResult> {
49+
export async function renderPage({
50+
route,
51+
serverContext,
52+
document,
53+
inlineCriticalCss,
54+
outputFiles,
55+
loadBundle = loadEsmModule,
56+
}: RenderOptions): Promise<RenderResult> {
6157
const {
6258
default: bootstrapAppFnOrModule,
6359
ɵSERVER_CONTEXT,
6460
renderModule,
6561
renderApplication,
66-
} = await loadEsmModule<BundleExports>('./main.server.mjs');
67-
68-
assert(ɵSERVER_CONTEXT, `ɵSERVER_CONTEXT was not exported.`);
62+
} = await loadBundle('./main.server.mjs');
6963

7064
const platformProviders: StaticProvider[] = [
7165
{
@@ -77,19 +71,12 @@ async function render({ route, serverContext }: RenderOptions): Promise<RenderRe
7771
let html: string | undefined;
7872

7973
if (isBootstrapFn(bootstrapAppFnOrModule)) {
80-
assert(renderApplication, `renderApplication was not exported.`);
8174
html = await renderApplication(bootstrapAppFnOrModule, {
8275
document,
8376
url: route,
8477
platformProviders,
8578
});
8679
} else {
87-
assert(renderModule, `renderModule was not exported.`);
88-
assert(
89-
bootstrapAppFnOrModule,
90-
`Neither an AppServerModule nor a bootstrapping function was exported.`,
91-
);
92-
9380
html = await renderModule(bootstrapAppFnOrModule, {
9481
document,
9582
url: route,
@@ -99,7 +86,7 @@ async function render({ route, serverContext }: RenderOptions): Promise<RenderRe
9986

10087
if (inlineCriticalCss) {
10188
const inlineCriticalCssProcessor = new InlineCriticalCssProcessor({
102-
minify: false, // CSS has already been minified during the build.
89+
minify: false,
10390
readAsset: async (filePath) => {
10491
filePath = basename(filePath);
10592
const content = outputFiles[filePath];
@@ -123,22 +110,3 @@ function isBootstrapFn(value: unknown): value is () => Promise<ApplicationRef> {
123110
// We can differentiate between a module and a bootstrap function by reading `cmp`:
124111
return typeof value === 'function' && !('ɵmod' in value);
125112
}
126-
127-
/**
128-
* Initializes the worker when it is first created by loading the Zone.js package
129-
* into the worker instance.
130-
*
131-
* @returns A promise resolving to the render function of the worker.
132-
*/
133-
async function initialize() {
134-
// Setup Zone.js
135-
await import(zonePackage);
136-
137-
return render;
138-
}
139-
140-
/**
141-
* The default export will be the promise returned by the initialize function.
142-
* This is awaited by piscina prior to using the Worker.
143-
*/
144-
export default initialize();
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { workerData } from 'node:worker_threads';
10+
import { RenderResult, ServerContext, renderPage } from './render-page';
11+
12+
export interface WorkerData {
13+
outputFiles: Record<string, string>;
14+
document: string;
15+
inlineCriticalCss?: boolean;
16+
}
17+
18+
export interface RenderOptions {
19+
route: string;
20+
serverContext: ServerContext;
21+
}
22+
23+
/**
24+
* This is passed as workerData when setting up the worker via the `piscina` package.
25+
*/
26+
const { outputFiles, document, inlineCriticalCss } = workerData as WorkerData;
27+
28+
export default function (options: RenderOptions): Promise<RenderResult> {
29+
return renderPage({ ...options, outputFiles, document, inlineCriticalCss });
30+
}

0 commit comments

Comments
 (0)