Skip to content

Commit 2908020

Browse files
alan-agius4angular-robot[bot]
authored andcommitted
feat(@angular-devkit/build-angular): support standalone app-shell generation
This commit adds support for generating an app-shell for a standalone application. The `main.server.ts`, will need to export a bootstrapping function that returns a `Promise<ApplicationRef>`. Example ```ts export default () => bootstrapApplication(AppComponent, { providers: [ importProvidersFrom(ServerModule), provideRouter([{ path: 'shell', component: AppShellComponent }]), ], }); ```
1 parent 04274af commit 2908020

File tree

3 files changed

+136
-19
lines changed

3 files changed

+136
-19
lines changed

‎packages/angular_devkit/build_angular/src/builders/app-shell/render-worker.ts

+51-18
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import type { Type } from '@angular/core';
10-
import type * as platformServer from '@angular/platform-server';
9+
import type { ApplicationRef, StaticProvider, Type } from '@angular/core';
10+
import type { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/platform-server';
1111
import assert from 'node:assert';
1212
import { workerData } from 'node:worker_threads';
1313

@@ -19,6 +19,23 @@ const { zonePackage } = workerData as {
1919
zonePackage: string;
2020
};
2121

22+
interface ServerBundleExports {
23+
/** An internal token that allows providing extra information about the server context. */
24+
ɵSERVER_CONTEXT?: typeof ɵSERVER_CONTEXT;
25+
26+
/** Render an NgModule application. */
27+
renderModule?: typeof renderModule;
28+
29+
/** NgModule to render. */
30+
AppServerModule?: Type<unknown>;
31+
32+
/** Method to render a standalone application. */
33+
renderApplication?: typeof renderApplication;
34+
35+
/** Standalone application bootstrapping function. */
36+
default?: () => Promise<ApplicationRef>;
37+
}
38+
2239
/**
2340
* A request to render a Server bundle generate by the universal server builder.
2441
*/
@@ -43,29 +60,45 @@ interface RenderRequest {
4360
* @returns A promise that resolves to the render HTML document for the application.
4461
*/
4562
async function render({ serverBundlePath, document, url }: RenderRequest): Promise<string> {
46-
const { AppServerModule, renderModule, ɵSERVER_CONTEXT } = (await import(serverBundlePath)) as {
47-
renderModule: typeof platformServer.renderModule | undefined;
48-
ɵSERVER_CONTEXT: typeof platformServer.ɵSERVER_CONTEXT | undefined;
49-
AppServerModule: Type<unknown> | undefined;
50-
};
63+
const {
64+
ɵSERVER_CONTEXT,
65+
AppServerModule,
66+
renderModule,
67+
renderApplication,
68+
default: bootstrapAppFn,
69+
} = (await import(serverBundlePath)) as ServerBundleExports;
5170

52-
assert(renderModule, `renderModule was not exported from: ${serverBundlePath}.`);
53-
assert(AppServerModule, `AppServerModule was not exported from: ${serverBundlePath}.`);
5471
assert(ɵSERVER_CONTEXT, `ɵSERVER_CONTEXT was not exported from: ${serverBundlePath}.`);
5572

73+
const platformProviders: StaticProvider[] = [
74+
{
75+
provide: ɵSERVER_CONTEXT,
76+
useValue: 'app-shell',
77+
},
78+
];
79+
5680
// Render platform server module
57-
const html = await renderModule(AppServerModule, {
81+
if (bootstrapAppFn) {
82+
assert(renderApplication, `renderApplication was not exported from: ${serverBundlePath}.`);
83+
84+
return renderApplication(bootstrapAppFn, {
85+
document,
86+
url,
87+
platformProviders,
88+
});
89+
}
90+
91+
assert(
92+
AppServerModule,
93+
`Neither an AppServerModule nor a bootstrapping function was exported from: ${serverBundlePath}.`,
94+
);
95+
assert(renderModule, `renderModule was not exported from: ${serverBundlePath}.`);
96+
97+
return renderModule(AppServerModule, {
5898
document,
5999
url,
60-
extraProviders: [
61-
{
62-
provide: ɵSERVER_CONTEXT,
63-
useValue: 'app-shell',
64-
},
65-
],
100+
extraProviders: platformProviders,
66101
});
67-
68-
return html;
69102
}
70103

71104
/**

‎packages/angular_devkit/build_angular/src/builders/server/platform-server-exports-loader.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default function (
1919
const source = `${content}
2020
2121
// EXPORTS added by @angular-devkit/build-angular
22-
export { renderModule, ɵSERVER_CONTEXT } from '@angular/platform-server';
22+
export { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/platform-server';
2323
`;
2424

2525
this.callback(null, source, map);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { getGlobalVariable } from '../../../utils/env';
2+
import { appendToFile, expectFileToMatch, writeMultipleFiles } from '../../../utils/fs';
3+
import { installPackage } from '../../../utils/packages';
4+
import { ng } from '../../../utils/process';
5+
import { updateJsonFile } from '../../../utils/project';
6+
7+
const snapshots = require('../../../ng-snapshot/package.json');
8+
9+
export default async function () {
10+
await appendToFile('src/app/app.component.html', '<router-outlet></router-outlet>');
11+
await ng('generate', 'app-shell', '--project', 'test-project');
12+
13+
const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots'];
14+
if (isSnapshotBuild) {
15+
const packagesToInstall: string[] = [];
16+
await updateJsonFile('package.json', (packageJson) => {
17+
const dependencies = packageJson['dependencies'];
18+
// Iterate over all of the packages to update them to the snapshot version.
19+
for (const [name, version] of Object.entries(
20+
snapshots.dependencies as { [p: string]: string },
21+
)) {
22+
if (name in dependencies && dependencies[name] !== version) {
23+
packagesToInstall.push(version);
24+
}
25+
}
26+
});
27+
28+
for (const pkg of packagesToInstall) {
29+
await installPackage(pkg);
30+
}
31+
}
32+
33+
// TODO(alanagius): update the below once we have a standalone schematic.
34+
await writeMultipleFiles({
35+
'src/app/app.component.ts': `
36+
import { Component } from '@angular/core';
37+
import { RouterOutlet } from '@angular/router';
38+
39+
@Component({
40+
selector: 'app-root',
41+
standalone: true,
42+
template: '<router-outlet></router-outlet>',
43+
imports: [RouterOutlet],
44+
})
45+
export class AppComponent {}
46+
`,
47+
'src/main.ts': `
48+
import { bootstrapApplication } from '@angular/platform-browser';
49+
import { provideRouter } from '@angular/router';
50+
51+
import { AppComponent } from './app/app.component';
52+
53+
bootstrapApplication(AppComponent, {
54+
providers: [
55+
provideRouter([]),
56+
],
57+
});
58+
`,
59+
'src/main.server.ts': `
60+
import { importProvidersFrom } from '@angular/core';
61+
import { BrowserModule, bootstrapApplication } from '@angular/platform-browser';
62+
import { ServerModule } from '@angular/platform-server';
63+
64+
import { provideRouter } from '@angular/router';
65+
66+
import { AppShellComponent } from './app/app-shell/app-shell.component';
67+
import { AppComponent } from './app/app.component';
68+
69+
export default () => bootstrapApplication(AppComponent, {
70+
providers: [
71+
importProvidersFrom(BrowserModule.withServerTransition({ appId: 'app' })),
72+
importProvidersFrom(ServerModule),
73+
provideRouter([{ path: 'shell', component: AppShellComponent }]),
74+
],
75+
});
76+
`,
77+
});
78+
79+
await ng('run', 'test-project:app-shell:development');
80+
await expectFileToMatch('dist/test-project/browser/index.html', /app-shell works!/);
81+
82+
await ng('run', 'test-project:app-shell');
83+
await expectFileToMatch('dist/test-project/browser/index.html', /app-shell works!/);
84+
}

0 commit comments

Comments
 (0)