Skip to content

Commit

Permalink
feat(@angular-devkit/build-angular): support standalone app-shell gen…
Browse files Browse the repository at this point in the history
…eration

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 }]),
  ],
});
```
  • Loading branch information
alan-agius4 authored and angular-robot[bot] committed Mar 14, 2023
1 parent 04274af commit 2908020
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/

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

Expand All @@ -19,6 +19,23 @@ const { zonePackage } = workerData as {
zonePackage: string;
};

interface ServerBundleExports {
/** An internal token that allows providing extra information about the server context. */
ɵSERVER_CONTEXT?: typeof ɵSERVER_CONTEXT;

/** Render an NgModule application. */
renderModule?: typeof renderModule;

/** NgModule to render. */
AppServerModule?: Type<unknown>;

/** Method to render a standalone application. */
renderApplication?: typeof renderApplication;

/** Standalone application bootstrapping function. */
default?: () => Promise<ApplicationRef>;
}

/**
* A request to render a Server bundle generate by the universal server builder.
*/
Expand All @@ -43,29 +60,45 @@ interface RenderRequest {
* @returns A promise that resolves to the render HTML document for the application.
*/
async function render({ serverBundlePath, document, url }: RenderRequest): Promise<string> {
const { AppServerModule, renderModule, ɵSERVER_CONTEXT } = (await import(serverBundlePath)) as {
renderModule: typeof platformServer.renderModule | undefined;
ɵSERVER_CONTEXT: typeof platformServer.ɵSERVER_CONTEXT | undefined;
AppServerModule: Type<unknown> | undefined;
};
const {
ɵSERVER_CONTEXT,
AppServerModule,
renderModule,
renderApplication,
default: bootstrapAppFn,
} = (await import(serverBundlePath)) as ServerBundleExports;

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

const platformProviders: StaticProvider[] = [
{
provide: ɵSERVER_CONTEXT,
useValue: 'app-shell',
},
];

// Render platform server module
const html = await renderModule(AppServerModule, {
if (bootstrapAppFn) {
assert(renderApplication, `renderApplication was not exported from: ${serverBundlePath}.`);

return renderApplication(bootstrapAppFn, {
document,
url,
platformProviders,
});
}

assert(
AppServerModule,
`Neither an AppServerModule nor a bootstrapping function was exported from: ${serverBundlePath}.`,
);
assert(renderModule, `renderModule was not exported from: ${serverBundlePath}.`);

return renderModule(AppServerModule, {
document,
url,
extraProviders: [
{
provide: ɵSERVER_CONTEXT,
useValue: 'app-shell',
},
],
extraProviders: platformProviders,
});

return html;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function (
const source = `${content}
// EXPORTS added by @angular-devkit/build-angular
export { renderModule, ɵSERVER_CONTEXT } from '@angular/platform-server';
export { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/platform-server';
`;

this.callback(null, source, map);
Expand Down
84 changes: 84 additions & 0 deletions tests/legacy-cli/e2e/tests/build/app-shell/app-shell-standalone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { getGlobalVariable } from '../../../utils/env';
import { appendToFile, expectFileToMatch, writeMultipleFiles } from '../../../utils/fs';
import { installPackage } from '../../../utils/packages';
import { ng } from '../../../utils/process';
import { updateJsonFile } from '../../../utils/project';

const snapshots = require('../../../ng-snapshot/package.json');

export default async function () {
await appendToFile('src/app/app.component.html', '<router-outlet></router-outlet>');
await ng('generate', 'app-shell', '--project', 'test-project');

const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots'];
if (isSnapshotBuild) {
const packagesToInstall: string[] = [];
await updateJsonFile('package.json', (packageJson) => {
const dependencies = packageJson['dependencies'];
// Iterate over all of the packages to update them to the snapshot version.
for (const [name, version] of Object.entries(
snapshots.dependencies as { [p: string]: string },
)) {
if (name in dependencies && dependencies[name] !== version) {
packagesToInstall.push(version);
}
}
});

for (const pkg of packagesToInstall) {
await installPackage(pkg);
}
}

// TODO(alanagius): update the below once we have a standalone schematic.
await writeMultipleFiles({
'src/app/app.component.ts': `
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
template: '<router-outlet></router-outlet>',
imports: [RouterOutlet],
})
export class AppComponent {}
`,
'src/main.ts': `
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
provideRouter([]),
],
});
`,
'src/main.server.ts': `
import { importProvidersFrom } from '@angular/core';
import { BrowserModule, bootstrapApplication } from '@angular/platform-browser';
import { ServerModule } from '@angular/platform-server';
import { provideRouter } from '@angular/router';
import { AppShellComponent } from './app/app-shell/app-shell.component';
import { AppComponent } from './app/app.component';
export default () => bootstrapApplication(AppComponent, {
providers: [
importProvidersFrom(BrowserModule.withServerTransition({ appId: 'app' })),
importProvidersFrom(ServerModule),
provideRouter([{ path: 'shell', component: AppShellComponent }]),
],
});
`,
});

await ng('run', 'test-project:app-shell:development');
await expectFileToMatch('dist/test-project/browser/index.html', /app-shell works!/);

await ng('run', 'test-project:app-shell');
await expectFileToMatch('dist/test-project/browser/index.html', /app-shell works!/);
}

0 comments on commit 2908020

Please sign in to comment.