Skip to content

Commit

Permalink
feat(@angular/ssr): add server routing configuration API
Browse files Browse the repository at this point in the history
This commit introduces a new server routing configuration API, as discussed in RFC angular/angular#56785. The new API provides several enhancements:

```ts
const serverRoutes: ServerRoute[] = [
  {
    path: '/error',
    renderMode: RenderMode.Server,
    status: 404,
    headers: {
      'Cache-Control': 'no-cache'
    }
  }
];
```

```ts
const serverRoutes: ServerRoute[] = [
  {
    path: '/product/:id',
    renderMode: RenderMode.Prerender,
    async getPrerenderPaths() {
      const dataService = inject(ProductService);
      const ids = await dataService.getIds(); // Assuming this returns ['1', '2', '3']
      return ids.map(id => ({ id })); // Generates paths like: [{ id: '1' }, { id: '2' }, { id: '3' }]
    }
  }
];
```

```ts
const serverRoutes: ServerRoute[] = [
  {
    path: '/product/:id',
    renderMode: RenderMode.Prerender,
    fallback: PrerenderFallback.Server, // Can be Server, Client, or None
    async getPrerenderPaths() {
    }
  }
];
```

```ts
const serverRoutes: ServerRoute[] = [
  {
    path: '/product/:id',
    renderMode: RenderMode.Server,
  },
  {
    path: '/error',
    renderMode: RenderMode.Client,
  },
  {
    path: '/**',
    renderMode: RenderMode.Prerender,
  },
];
```

These additions aim to provide greater flexibility and control over server-side rendering configurations and prerendering behaviors.
  • Loading branch information
alan-agius4 committed Sep 12, 2024
1 parent 793f6a0 commit d66aaa3
Show file tree
Hide file tree
Showing 27 changed files with 868 additions and 225 deletions.
25 changes: 24 additions & 1 deletion goldens/public-api/angular/ssr/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,36 @@
```ts

import { EnvironmentProviders } from '@angular/core';

// @public
export class AngularAppEngine {
getHeaders(request: Request): ReadonlyMap<string, string>;
getPrerenderHeaders(request: Request): ReadonlyMap<string, string>;
render(request: Request, requestContext?: unknown): Promise<Response | null>;
static ɵhooks: Hooks;
}

// @public
export enum PrerenderFallback {
Client = 1,
None = 2,
Server = 0
}

// @public
export function provideServerRoutesConfig(routes: ServerRoute[]): EnvironmentProviders;

// @public
export enum RenderMode {
AppShell = 0,
Client = 2,
Prerender = 3,
Server = 1
}

// @public
export type ServerRoute = ServerRouteAppShell | ServerRouteClient | ServerRoutePrerender | ServerRoutePrerenderWithParams | ServerRouteServer;

// (No @packageDocumentation comment for this package)

```
43 changes: 43 additions & 0 deletions goldens/public-api/angular/ssr/index_transitive.api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
## API Report File for "@angular/devkit-repo"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts

// @public
export interface ServerRouteAppShell extends Omit<ServerRouteCommon, 'headers' | 'status'> {
renderMode: RenderMode.AppShell;
}

// @public
export interface ServerRouteClient extends ServerRouteCommon {
renderMode: RenderMode.Client;
}

// @public
export interface ServerRouteCommon {
headers?: Record<string, string>;
path: string;
status?: number;
}

// @public
export interface ServerRoutePrerender extends Omit<ServerRouteCommon, 'status'> {
fallback?: never;
renderMode: RenderMode.Prerender;
}

// @public
export interface ServerRoutePrerenderWithParams extends Omit<ServerRoutePrerender, 'fallback'> {
fallback?: PrerenderFallback;
getPrerenderParams: () => Promise<Record<string, string>[]>;
}

// @public
export interface ServerRouteServer extends ServerRouteCommon {
renderMode: RenderMode.Server;
}

// (No @packageDocumentation comment for this package)

```
2 changes: 1 addition & 1 deletion goldens/public-api/angular/ssr/node/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Type } from '@angular/core';

// @public
export class AngularNodeAppEngine {
getHeaders(request: IncomingMessage): ReadonlyMap<string, string>;
getPrerenderHeaders(request: IncomingMessage): ReadonlyMap<string, string>;
render(request: IncomingMessage, requestContext?: unknown): Promise<Response | null>;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
harness.expectFile('dist/browser/main.js').toExist();
const indexFileContent = harness.expectFile('dist/browser/index.html').content;
indexFileContent.toContain('app-shell works!');
indexFileContent.toContain('ng-server-context="app-shell"');
// TODO(alanagius): enable once integration of routes in complete.
// indexFileContent.toContain('ng-server-context="app-shell"');
});

it('critical CSS is inlined', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,6 @@ export function createServerMainCodeBundleOptions(

// Add @angular/ssr exports
`export {
ɵServerRenderContext,
ɵdestroyAngularServerApp,
ɵextractRoutesAndCreateRouteTree,
ɵgetOrCreateAngularServerApp,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@
*/

import type { ApplicationRef, Type } from '@angular/core';
import type {
ɵServerRenderContext,
ɵextractRoutesAndCreateRouteTree,
ɵgetOrCreateAngularServerApp,
} from '@angular/ssr';
import type { ɵextractRoutesAndCreateRouteTree, ɵgetOrCreateAngularServerApp } from '@angular/ssr';
import { assertIsError } from '../error';
import { loadEsmModule } from '../load-esm';

Expand All @@ -20,7 +16,6 @@ import { loadEsmModule } from '../load-esm';
*/
interface MainServerBundleExports {
default: (() => Promise<ApplicationRef>) | Type<unknown>;
ɵServerRenderContext: typeof ɵServerRenderContext;
ɵextractRoutesAndCreateRouteTree: typeof ɵextractRoutesAndCreateRouteTree;
ɵgetOrCreateAngularServerApp: typeof ɵgetOrCreateAngularServerApp;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,13 @@ async function renderPages(
route.slice(baseHrefWithLeadingSlash.length - 1),
);

const isAppShellRoute = appShellRoute === routeWithoutBaseHref;
const render: Promise<string | null> = renderWorker.run({ url: route, isAppShellRoute });
const render: Promise<string | null> = renderWorker.run({ url: route });
const renderResult: Promise<void> = render
.then((content) => {
if (content !== null) {
const outPath = posix.join(removeLeadingSlash(routeWithoutBaseHref), 'index.html');
const isAppShellRoute = appShellRoute === routeWithoutBaseHref;

output[outPath] = { content, appShellRoute: isAppShellRoute };
}
})
Expand Down
18 changes: 6 additions & 12 deletions packages/angular/build/src/utils/server-rendering/render-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,18 @@ export interface RenderWorkerData extends ESMInMemoryFileLoaderWorkerData {

export interface RenderOptions {
url: string;
isAppShellRoute: boolean;
}

/**
* Renders each route in routes and writes them to <outputPath>/<route>/index.html.
*/
async function renderPage({ url, isAppShellRoute }: RenderOptions): Promise<string | null> {
const {
ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp,
ɵServerRenderContext: ServerRenderContext,
} = await loadEsmModuleFromMemory('./main.server.mjs');
async function renderPage({ url }: RenderOptions): Promise<string | null> {
const { ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp } =
await loadEsmModuleFromMemory('./main.server.mjs');
const angularServerApp = getOrCreateAngularServerApp();
const response = await angularServerApp.render(
new Request(new URL(url, 'http://local-angular-prerender'), {
signal: AbortSignal.timeout(30_000),
}),
undefined,
isAppShellRoute ? ServerRenderContext.AppShell : ServerRenderContext.SSG,
const response = await angularServerApp.renderStatic(
new URL(url, 'http://local-angular-prerender'),
AbortSignal.timeout(30_000),
);

return response ? response.text() : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ async function extractRoutes(): Promise<RoutersExtractorWorkerResult> {

const routeTree = await extractRoutesAndCreateRouteTree(
new URL('http://local-angular-prerender/'),
/** manifest */ undefined,
/** invokeGetPrerenderParams */ true,
);

return routeTree.toObject();
Expand Down
12 changes: 11 additions & 1 deletion packages/angular/ssr/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
load("@npm//@angular/build-tooling/bazel/api-golden:index.bzl", "api_golden_test_npm_package")
load("@npm//@angular/build-tooling/bazel/api-golden:index.bzl", "api_golden_test", "api_golden_test_npm_package")
load("@rules_pkg//:pkg.bzl", "pkg_tar")
load("//tools:defaults.bzl", "ng_package", "ts_library")

Expand Down Expand Up @@ -67,3 +67,13 @@ api_golden_test_npm_package(
golden_dir = "angular_cli/goldens/public-api/angular/ssr",
npm_package = "angular_cli/packages/angular/ssr/npm_package",
)

api_golden_test(
name = "ssr_transitive_api",
data = [
":ssr",
"//goldens:public-api",
],
entry_point = "angular_cli/packages/angular/ssr/public_api_transitive.d.ts",
golden = "angular_cli/goldens/public-api/angular/ssr/index_transitive.api.md",
)
6 changes: 3 additions & 3 deletions packages/angular/ssr/node/src/app-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class AngularNodeAppEngine {
* app.use(express.static('dist/browser', {
* setHeaders: (res, path) => {
* // Retrieve headers for the current request
* const headers = angularAppEngine.getHeaders(res.req);
* const headers = angularAppEngine.getPrerenderHeaders(res.req);
*
* // Apply the retrieved headers to the response
* for (const { key, value } of headers) {
Expand All @@ -66,7 +66,7 @@ export class AngularNodeAppEngine {
}));
* ```
*/
getHeaders(request: IncomingMessage): ReadonlyMap<string, string> {
return this.angularAppEngine.getHeaders(createWebRequestFromNodeRequest(request));
getPrerenderHeaders(request: IncomingMessage): ReadonlyMap<string, string> {
return this.angularAppEngine.getPrerenderHeaders(createWebRequestFromNodeRequest(request));
}
}
1 change: 0 additions & 1 deletion packages/angular/ssr/private_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export {
extractRoutesAndCreateRouteTree as ɵextractRoutesAndCreateRouteTree,
} from './src/routes/ng-routes';
export {
ServerRenderContext as ɵServerRenderContext,
getOrCreateAngularServerApp as ɵgetOrCreateAngularServerApp,
destroyAngularServerApp as ɵdestroyAngularServerApp,
} from './src/app';
Expand Down
7 changes: 7 additions & 0 deletions packages/angular/ssr/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,10 @@
export * from './private_export';

export { AngularAppEngine } from './src/app-engine';

export {
type PrerenderFallback,
type RenderMode,
type ServerRoute,
provideServerRoutesConfig,
} from './src/routes/route-config';
20 changes: 20 additions & 0 deletions packages/angular/ssr/public_api_transitive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

// This file exports symbols that are not part of the public API but are
// dependencies of public API symbols. Including them here ensures they
// are tracked in the API golden file, preventing accidental breaking changes.

export type {
ServerRouteAppShell,
ServerRouteClient,
ServerRoutePrerender,
ServerRoutePrerenderWithParams,
ServerRouteServer,
ServerRouteCommon,
} from './src/routes/route-config';
2 changes: 1 addition & 1 deletion packages/angular/ssr/src/app-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export class AngularAppEngine {
* @returns A `Map` containing the HTTP headers as key-value pairs.
* @note This function should be used exclusively for retrieving headers of SSG pages.
*/
getHeaders(request: Request): ReadonlyMap<string, string> {
getPrerenderHeaders(request: Request): ReadonlyMap<string, string> {
if (this.manifest.staticPathsHeaders.size === 0) {
return new Map();
}
Expand Down
Loading

0 comments on commit d66aaa3

Please sign in to comment.