1
0
mirror of https://github.com/angular/angular-cli.git synced 2025-05-15 18:13:38 +08:00

Compare commits

...

4 Commits

Author SHA1 Message Date
Angular Robot
9f4a25a0c4 build: update devinfra digest to 2707802 2025-03-18 21:04:16 +00:00
Charles Lyding
b66d36b4b0 build: move @angular/build specific dependencies out of root
With the migration to `rules_js`, package specific dependencies now only
need to be referenced with the source `package.json` for each specific
package. Most of the `@angular/build` specific dependencies have now been
moved. This is not exhaustive and further changes will continue to move
additional dependencies.
2025-03-18 13:01:26 -04:00
Alan Agius
26fd4ea73a feat(@schematics/angular): add migrations for server rendering updates
- Migrate imports of `provideServerRendering` from `@angular/platform-server` to `@angular/ssr`.
- Update `provideServerRendering` to use `withRoutes` and remove `provideServerRouting` from `@angular/ssr`.
2025-03-18 17:58:59 +01:00
Alan Agius
33b9de3eb1 feat(@angular/ssr): expose provideServerRendering and remove provideServerRouting
This commit introduces `provideServerRendering` as the primary function for configuring server-side rendering, replacing `provideServerRouting`. `provideServerRendering` now includes the functionality of `provideServerRouting` through the use of the `withRoutes` feature.

This change consolidates server-side rendering configuration into a single, more flexible function, aligning with the evolution of Angular SSR.

**Before:**
```ts
import { provideServerRouting } from '@angular/ssr';
import { serverRoutes } from './app.routes';

provideServerRouting(serverRoutes);
```

**After:**
```ts
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { serverRoutes } from './app.routes';

provideServerRendering(withRoutes(serverRoutes));
```
2025-03-18 17:58:59 +01:00
25 changed files with 623 additions and 153 deletions
WORKSPACE
goldens/public-api/angular/ssr
package.json
packages
angular
angular_devkit
build_angular
schematics
schematics/angular
app-shell
migrations
migration-collection.json
replace-provide-server-rendering-import
replace-provide-server-routing
server/files
application-builder
server-builder/standalone-src/app
pnpm-lock.yaml

@ -234,7 +234,7 @@ load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
git_repository(
name = "devinfra",
commit = "85eab901e27abe60bb725fbfd8def94559cbe636",
commit = "27078026111b01a7202449e9788ce38f2b2e103f",
remote = "https://github.com/angular/dev-infra.git",
)

@ -27,7 +27,7 @@ export enum PrerenderFallback {
}
// @public
export function provideServerRouting(routes: ServerRoute[], ...features: ServerRoutesFeature<ServerRoutesFeatureKind>[]): EnvironmentProviders;
export function provideServerRendering(...features: ServerRenderingFeature<ServerRenderingFeatureKind>[]): EnvironmentProviders;
// @public
export enum RenderMode {
@ -72,7 +72,10 @@ export interface ServerRouteServer extends ServerRouteCommon {
}
// @public
export function withAppShell(component: Type<unknown> | (() => Promise<Type<unknown> | DefaultExport<Type<unknown>>>)): ServerRoutesFeature<ServerRoutesFeatureKind.AppShell>;
export function withAppShell(component: Type<unknown> | (() => Promise<Type<unknown> | DefaultExport<Type<unknown>>>)): ServerRenderingFeature<ServerRenderingFeatureKind.AppShell>;
// @public
export function withRoutes(routes: ServerRoute[]): ServerRenderingFeature<ServerRenderingFeatureKind.ServerRoutes>;
// (No @packageDocumentation comment for this package)

@ -46,7 +46,6 @@
},
"homepage": "https://github.com/angular/angular-cli",
"devDependencies": {
"@ampproject/remapping": "2.3.0",
"@angular/animations": "20.0.0-next.2",
"@angular/cdk": "20.0.0-next.1",
"@angular/common": "20.0.0-next.2",
@ -134,19 +133,11 @@
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"karma-source-map-support": "1.4.0",
"less": "4.2.2",
"listr2": "8.2.5",
"lmdb": "3.2.6",
"lodash": "^4.17.21",
"magic-string": "0.30.17",
"mrmime": "2.0.1",
"ng-packagr": "20.0.0-next.1",
"npm": "^11.0.0",
"open": "10.1.0",
"ora": "5.4.1",
"parse5-html-rewriting-stream": "7.0.0",
"piscina": "4.9.0",
"postcss": "8.5.3",
"prettier": "^3.0.0",
"protractor": "~7.0.0",
"puppeteer": "18.2.1",
@ -155,13 +146,11 @@
"rollup-license-plugin": "~3.0.1",
"rollup-plugin-sourcemaps": "^0.6.0",
"rxjs": "7.8.2",
"sass": "1.85.1",
"semver": "7.7.1",
"shelljs": "^0.9.0",
"source-map-support": "0.5.21",
"symbol-observable": "4.0.0",
"tar": "^7.0.0",
"tinyglobby": "0.2.12",
"tree-kill": "1.2.2",
"ts-node": "^10.9.1",
"tslib": "2.8.1",
@ -170,7 +159,6 @@
"unenv": "^1.10.0",
"verdaccio": "6.0.5",
"verdaccio-auth-memory": "^10.0.0",
"watchpack": "2.4.2",
"yargs-parser": "21.1.1",
"zone.js": "^0.15.0"
},

@ -74,15 +74,28 @@ ts_project(
data = RUNTIME_ASSETS,
module_name = "@angular/build",
deps = [
":node_modules/@ampproject/remapping",
":node_modules/@angular-devkit/architect",
":node_modules/@angular-devkit/core",
":node_modules/@angular/ssr",
":node_modules/@inquirer/confirm",
":node_modules/@vitejs/plugin-basic-ssl",
":node_modules/jsonc-parser",
":node_modules/less",
":node_modules/listr2",
":node_modules/lmdb",
":node_modules/magic-string",
":node_modules/mrmime",
":node_modules/ng-packagr",
":node_modules/parse5-html-rewriting-stream",
":node_modules/picomatch",
":node_modules/piscina",
":node_modules/postcss",
":node_modules/sass",
":node_modules/source-map-support",
":node_modules/tinyglobby",
":node_modules/vite",
"//:node_modules/@ampproject/remapping",
":node_modules/watchpack",
"//:node_modules/@angular/common",
"//:node_modules/@angular/compiler",
"//:node_modules/@angular/compiler-cli",
@ -108,23 +121,10 @@ ts_project(
"//:node_modules/https-proxy-agent",
"//:node_modules/istanbul-lib-instrument",
"//:node_modules/karma",
"//:node_modules/less",
"//:node_modules/listr2",
"//:node_modules/lmdb",
"//:node_modules/magic-string",
"//:node_modules/mrmime",
"//:node_modules/ng-packagr",
"//:node_modules/parse5-html-rewriting-stream",
"//:node_modules/piscina",
"//:node_modules/postcss",
"//:node_modules/rollup",
"//:node_modules/sass",
"//:node_modules/semver",
"//:node_modules/source-map-support",
"//:node_modules/tinyglobby",
"//:node_modules/tslib",
"//:node_modules/typescript",
"//:node_modules/watchpack",
],
)
@ -204,7 +204,7 @@ ts_project(
"//:node_modules/@angular/platform-browser",
"//:node_modules/@angular/platform-browser-dynamic",
"//:node_modules/@angular/router",
"//:node_modules/ng-packagr",
":node_modules/ng-packagr",
"//:node_modules/rxjs",
"//:node_modules/tslib",
"//:node_modules/typescript",

@ -51,7 +51,10 @@
},
"devDependencies": {
"@angular/ssr": "workspace:*",
"@angular-devkit/core": "workspace:*"
"@angular-devkit/core": "workspace:*",
"less": "4.2.2",
"ng-packagr": "20.0.0-next.1",
"postcss": "8.5.3"
},
"peerDependencies": {
"@angular/compiler": "0.0.0-ANGULAR-FW-PEER-DEP",

@ -36,8 +36,8 @@ ts_project(
deps = [
":node_modules/@angular-devkit/schematics",
":node_modules/@schematics/angular",
":node_modules/parse5-html-rewriting-stream",
"//:node_modules/@types/node",
"//:node_modules/parse5-html-rewriting-stream",
],
)

@ -14,8 +14,9 @@ export { createRequestHandler, type RequestHandlerFunction } from './src/handler
export {
PrerenderFallback,
type ServerRoute,
provideServerRouting,
provideServerRendering,
withAppShell,
withRoutes,
RenderMode,
type ServerRouteClient,
type ServerRoutePrerender,

@ -11,8 +11,11 @@ import {
InjectionToken,
Provider,
Type,
inject,
makeEnvironmentProviders,
provideEnvironmentInitializer,
} from '@angular/core';
import { provideServerRendering as provideServerRenderingPlatformServer } from '@angular/platform-server';
import { type DefaultExport, ROUTES, type Route } from '@angular/router';
/**
@ -22,25 +25,26 @@ import { type DefaultExport, ROUTES, type Route } from '@angular/router';
const APP_SHELL_ROUTE = 'ng-app-shell';
/**
* Identifies a particular kind of `ServerRoutesFeatureKind`.
* @see {@link ServerRoutesFeature}
* Identifies a particular kind of `ServerRenderingFeatureKind`.
* @see {@link ServerRenderingFeature}
*/
enum ServerRoutesFeatureKind {
enum ServerRenderingFeatureKind {
AppShell,
ServerRoutes,
}
/**
* Helper type to represent a server routes feature.
* @see {@link ServerRoutesFeatureKind}
* @see {@link ServerRenderingFeatureKind}
*/
interface ServerRoutesFeature<FeatureKind extends ServerRoutesFeatureKind> {
interface ServerRenderingFeature<FeatureKind extends ServerRenderingFeatureKind> {
ɵkind: FeatureKind;
ɵproviders: Provider[];
ɵproviders: (Provider | EnvironmentProviders)[];
}
/**
* Different rendering modes for server routes.
* @see {@link provideServerRouting}
* @see {@link withRoutes}
* @see {@link ServerRoute}
*/
export enum RenderMode {
@ -171,7 +175,7 @@ export interface ServerRouteServer extends ServerRouteCommon {
/**
* Server route configuration.
* @see {@link provideServerRouting}
* @see {@link withRoutes}
*/
export type ServerRoute =
| ServerRouteClient
@ -200,62 +204,103 @@ export interface ServerRoutesConfig {
export const SERVER_ROUTES_CONFIG = new InjectionToken<ServerRoutesConfig>('SERVER_ROUTES_CONFIG');
/**
* Sets up the necessary providers for configuring server routes.
* This function accepts an array of server routes and optional configuration
* options, returning an `EnvironmentProviders` object that encapsulates
* the server routes and configuration settings.
* Configures server-side routing for the application.
*
* @param routes - An array of server routes to be provided.
* @param features - (Optional) server routes features.
* @returns An `EnvironmentProviders` instance with the server routes configuration.
* This function registers an array of `ServerRoute` definitions, enabling server-side rendering
* for specific URL paths. These routes are used to pre-render content on the server, improving
* initial load performance and SEO.
*
* @param routes - An array of `ServerRoute` objects, each defining a server-rendered route.
* @returns A `ServerRenderingFeature` object configuring server-side routes.
*
* @example
* ```ts
* import { provideServerRendering, withRoutes, ServerRoute, RenderMode } from '@angular/ssr';
*
* const serverRoutes: ServerRoute[] = [
* {
* route: '', // This renders the "/" route on the client (CSR)
* renderMode: RenderMode.Client,
* },
* {
* route: 'about', // This page is static, so we prerender it (SSG)
* renderMode: RenderMode.Prerender,
* },
* {
* route: 'profile', // This page requires user-specific data, so we use SSR
* renderMode: RenderMode.Server,
* },
* {
* route: '**', // All other routes will be rendered on the server (SSR)
* renderMode: RenderMode.Server,
* },
* ];
*
* provideServerRendering(withRoutes(serverRoutes));
* ```
*
* @see {@link provideServerRendering}
* @see {@link ServerRoute}
* @see {@link withAppShell}
*/
export function provideServerRouting(
export function withRoutes(
routes: ServerRoute[],
...features: ServerRoutesFeature<ServerRoutesFeatureKind>[]
): EnvironmentProviders {
): ServerRenderingFeature<ServerRenderingFeatureKind.ServerRoutes> {
const config: ServerRoutesConfig = { routes };
const hasAppShell = features.some((f) => f.ɵkind === ServerRoutesFeatureKind.AppShell);
if (hasAppShell) {
config.appShellRoute = APP_SHELL_ROUTE;
}
const providers: Provider[] = [
{
provide: SERVER_ROUTES_CONFIG,
useValue: config,
},
];
for (const feature of features) {
providers.push(...feature.ɵproviders);
}
return makeEnvironmentProviders(providers);
return {
ɵkind: ServerRenderingFeatureKind.ServerRoutes,
ɵproviders: [
{
provide: SERVER_ROUTES_CONFIG,
useValue: config,
},
],
};
}
/**
* Configures the app shell route with the provided component.
* Configures the shell of the application.
*
* The app shell serves as the main entry point for the application and is commonly used
* to enable server-side rendering (SSR) of the application shell. It handles requests
* that do not match any specific server route, providing a fallback mechanism and improving
* perceived performance during navigation.
* The app shell is a minimal, static HTML page that is served immediately, while the
* full Angular application loads in the background. This improves perceived performance
* by providing instant feedback to the user.
*
* This configuration is particularly useful in applications leveraging Progressive Web App (PWA)
* patterns, such as service workers, to deliver a seamless user experience.
* This function configures the app shell route, which serves the provided component for
* requests that do not match any defined server routes.
*
* @param component The Angular component to render for the app shell route.
* @returns A server routes feature configuration for the app shell.
* @param component - The Angular component to render for the app shell. Can be a direct
* component type or a dynamic import function.
* @returns A `ServerRenderingFeature` object configuring the app shell.
*
* @see {@link provideServerRouting}
* @example
* ```ts
* import { provideServerRendering, withAppShell, withRoutes } from '@angular/ssr';
* import { AppShellComponent } from './app-shell.component';
*
* provideServerRendering(
* withRoutes(serverRoutes),
* withAppShell(AppShellComponent)
* );
* ```
*
* @example
* ```ts
* import { provideServerRendering, withAppShell, withRoutes } from '@angular/ssr';
*
* provideServerRendering(
* withRoutes(serverRoutes),
* withAppShell(() =>
* import('./app-shell.component').then((m) => m.AppShellComponent)
* )
* );
* ```
*
* @see {@link provideServerRendering}
* @see {@link https://angular.dev/ecosystem/service-workers/app-shell | App shell pattern on Angular.dev}
*/
export function withAppShell(
component: Type<unknown> | (() => Promise<Type<unknown> | DefaultExport<Type<unknown>>>),
): ServerRoutesFeature<ServerRoutesFeatureKind.AppShell> {
): ServerRenderingFeature<ServerRenderingFeatureKind.AppShell> {
const routeConfig: Route = {
path: APP_SHELL_ROUTE,
};
@ -267,13 +312,73 @@ export function withAppShell(
}
return {
ɵkind: ServerRoutesFeatureKind.AppShell,
ɵkind: ServerRenderingFeatureKind.AppShell,
ɵproviders: [
{
provide: ROUTES,
useValue: routeConfig,
multi: true,
},
provideEnvironmentInitializer(() => {
const config = inject(SERVER_ROUTES_CONFIG);
config.appShellRoute = APP_SHELL_ROUTE;
}),
],
};
}
/**
* Configures server-side rendering for an Angular application.
*
* This function sets up the necessary providers for server-side rendering, including
* support for server routes and app shell. It combines features configured using
* `withRoutes` and `withAppShell` to provide a comprehensive server-side rendering setup.
*
* @param features - Optional features to configure additional server rendering behaviors.
* @returns An `EnvironmentProviders` instance with the server-side rendering configuration.
*
* @example
* Basic example of how you can enable server-side rendering in your application
* when using the `bootstrapApplication` function:
*
* ```ts
* import { bootstrapApplication } from '@angular/platform-browser';
* import { provideServerRendering, withRoutes, withAppShell } from '@angular/ssr';
* import { AppComponent } from './app/app.component';
* import { SERVER_ROUTES } from './app/app.server.routes';
* import { AppShellComponent } from './app/app-shell.component';
*
* bootstrapApplication(AppComponent, {
* providers: [
* provideServerRendering(
* withRoutes(SERVER_ROUTES),
* withAppShell(AppShellComponent)
* )
* ]
* });
* ```
* @see {@link withRoutes} configures server-side routing
* @see {@link withAppShell} configures the application shell
*/
export function provideServerRendering(
...features: ServerRenderingFeature<ServerRenderingFeatureKind>[]
): EnvironmentProviders {
let hasAppShell = false;
let hasServerRoutes = false;
const providers: (Provider | EnvironmentProviders)[] = [provideServerRenderingPlatformServer()];
for (const { ɵkind, ɵproviders } of features) {
hasAppShell ||= ɵkind === ServerRenderingFeatureKind.AppShell;
hasServerRoutes ||= ɵkind === ServerRenderingFeatureKind.ServerRoutes;
providers.push(...ɵproviders);
}
if (!hasServerRoutes && hasAppShell) {
throw new Error(
`Configuration error: found 'withAppShell()' without 'withRoutes()' in the same call to 'provideServerRendering()'.` +
`The 'withAppShell()' function requires 'withRoutes()' to be used.`,
);
}
return makeEnvironmentProviders(providers);
}

@ -13,7 +13,6 @@ ts_project(
"//:node_modules/@angular/compiler",
"//:node_modules/@angular/core",
"//:node_modules/@angular/platform-browser",
"//:node_modules/@angular/platform-server",
"//:node_modules/@angular/router",
"//:node_modules/@types/node",
"//packages/angular/ssr",

@ -24,12 +24,6 @@ const CRITTERS_ACTUAL_LICENSE_FILE_PATH = join(
'third_party/beasties/THIRD_PARTY_LICENSES.txt',
);
/**
* Path to the golden reference license file for the Beasties library.
* This file is used as a reference for comparison and is located in the same directory as this script.
*/
const CRITTERS_GOLDEN_LICENSE_FILE_PATH = join(__dirname, 'THIRD_PARTY_LICENSES.txt.golden');
describe('NPM Package Tests', () => {
it('should not include the contents of third_party/beasties/index.js in the FESM bundle', async () => {
const fesmFilePath = join(ANGULAR_SSR_PACKAGE_PATH, 'fesm2022/ssr.mjs');

@ -14,11 +14,10 @@ import {
provideExperimentalZonelessChangeDetection,
} from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { provideServerRendering } from '@angular/platform-server';
import { RouterOutlet, Routes, provideRouter } from '@angular/router';
import { destroyAngularServerApp } from '../src/app';
import { ServerAsset, setAngularAppManifest } from '../src/manifest';
import { ServerRoute, provideServerRouting } from '../src/routes/route-config';
import { ServerRoute, provideServerRendering, withRoutes } from '../src/routes/route-config';
@Component({
standalone: true,
@ -94,10 +93,9 @@ export function setAngularAppTestingManifest(
bootstrap: async () => () => {
return bootstrapApplication(rootComponent, {
providers: [
provideServerRendering(),
provideExperimentalZonelessChangeDetection(),
provideRouter(routes),
provideServerRouting(serverRoutes),
provideServerRendering(withRoutes(serverRoutes)),
...extraProviders,
],
});

@ -129,6 +129,7 @@ ts_project(
data = RUNTIME_ASSETS,
module_name = "@angular-devkit/build-angular",
deps = [
":node_modules/@ampproject/remapping",
":node_modules/@angular-devkit/architect",
":node_modules/@angular-devkit/build-webpack",
":node_modules/@angular-devkit/core",
@ -151,17 +152,21 @@ ts_project(
":node_modules/license-webpack-plugin",
":node_modules/loader-utils",
":node_modules/mini-css-extract-plugin",
":node_modules/ng-packagr",
":node_modules/piscina",
":node_modules/postcss",
":node_modules/postcss-loader",
":node_modules/resolve-url-loader",
":node_modules/sass",
":node_modules/sass-loader",
":node_modules/source-map-loader",
":node_modules/source-map-support",
":node_modules/terser",
":node_modules/webpack",
":node_modules/webpack-dev-middleware",
":node_modules/webpack-dev-server",
":node_modules/webpack-merge",
":node_modules/webpack-subresource-integrity",
"//:node_modules/@ampproject/remapping",
"//:node_modules/@angular/common",
"//:node_modules/@angular/compiler-cli",
"//:node_modules/@angular/core",
@ -193,15 +198,10 @@ ts_project(
"//:node_modules/istanbul-lib-instrument",
"//:node_modules/karma",
"//:node_modules/karma-source-map-support",
"//:node_modules/ng-packagr",
"//:node_modules/open",
"//:node_modules/ora",
"//:node_modules/piscina",
"//:node_modules/postcss",
"//:node_modules/rxjs",
"//:node_modules/sass",
"//:node_modules/semver",
"//:node_modules/source-map-support",
"//:node_modules/tree-kill",
"//:node_modules/tslib",
"//:node_modules/typescript",

@ -66,8 +66,9 @@
"esbuild": "0.25.1"
},
"devDependencies": {
"undici": "7.5.0",
"@angular/ssr": "workspace:*"
"@angular/ssr": "workspace:*",
"ng-packagr": "20.0.0-next.1",
"undici": "7.5.0"
},
"peerDependencies": {
"@angular/compiler-cli": "0.0.0-ANGULAR-FW-PEER-DEP",

@ -29,8 +29,8 @@ ts_project(
deps = [
":node_modules/@angular-devkit/core",
":node_modules/jsonc-parser",
":node_modules/magic-string",
"//:node_modules/@types/node",
"//:node_modules/magic-string",
"//:node_modules/rxjs",
],
)

@ -300,12 +300,12 @@ function addServerRoutingConfig(options: AppShellOptions, isStandalone: boolean)
/** max */ undefined,
/** recursive */ true,
).find(
(n) => ts.isIdentifier(n.expression) && n.expression.getText() === 'provideServerRouting',
(n) => ts.isIdentifier(n.expression) && n.expression.getText() === 'provideServerRendering',
);
if (!functionCall) {
throw new SchematicsException(
`Cannot find the "provideServerRouting" function call in "${configFilePath}".`,
`Cannot find the "provideServerRendering" function call in "${configFilePath}".`,
);
}

@ -123,11 +123,11 @@ describe('App Shell Schematic', () => {
expect(content).toMatch(/app-shell/);
});
it(`should update the 'provideServerRouting' call to include 'withAppShell'`, async () => {
it(`should update the 'provideServerRendering' call to include 'withAppShell'`, async () => {
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const content = tree.readContent('/projects/bar/src/app/app.config.server.ts');
expect(tags.oneLine`${content}`).toContain(
tags.oneLine`provideServerRouting(serverRoutes, withAppShell(AppShell))`,
tags.oneLine`provideServerRendering(withRoutes(serverRoutes), withAppShell(AppShell))`,
);
});

@ -1,5 +1,15 @@
{
"schematics": {
"replace-provide-server-rendering-import": {
"version": "20.0.0",
"factory": "./replace-provide-server-rendering-import/migration",
"description": "Migrate imports of 'provideServerRendering' from '@angular/platform-server' to '@angular/ssr'."
},
"replace-provide-server-routing": {
"version": "20.0.0",
"factory": "./replace-provide-server-routing/migration",
"description": "Migrate 'provideServerRendering' to use 'withRoutes' and remove 'provideServerRouting' from '@angular/ssr'."
},
"use-application-builder": {
"version": "20.0.0",
"factory": "./use-application-builder/migration",

@ -0,0 +1,110 @@
/**
* @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
*/
import { DirEntry, Rule } from '@angular-devkit/schematics';
import * as ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
import { NodeDependencyType, addPackageJsonDependency } from '../../utility/dependencies';
import { latestVersions } from '../../utility/latest-versions';
function* visit(directory: DirEntry): IterableIterator<[fileName: string, contents: string]> {
for (const path of directory.subfiles) {
if (path.endsWith('.ts') && !path.endsWith('.d.ts')) {
const entry = directory.file(path);
if (entry) {
const content = entry.content;
if (
content.includes('provideServerRendering') &&
content.includes('@angular/platform-server')
) {
// Only need to rename the import so we can just string replacements.
yield [entry.path, content.toString()];
}
}
}
}
for (const path of directory.subdirs) {
if (path === 'node_modules' || path.startsWith('.')) {
continue;
}
yield* visit(directory.dir(path));
}
}
export default function (): Rule {
return async (tree) => {
addPackageJsonDependency(tree, {
name: '@angular/ssr',
version: latestVersions.AngularSSR,
type: NodeDependencyType.Default,
overwrite: false,
});
for (const [filePath, content] of visit(tree.root)) {
let updatedContent = content;
const ssrImports = new Set<string>();
const platformServerImports = new Set<string>();
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
sourceFile.forEachChild((node) => {
if (ts.isImportDeclaration(node)) {
const moduleSpecifier = node.moduleSpecifier.getText(sourceFile);
if (moduleSpecifier.includes('@angular/platform-server')) {
const importClause = node.importClause;
if (
importClause &&
importClause.namedBindings &&
ts.isNamedImports(importClause.namedBindings)
) {
const namedImports = importClause.namedBindings.elements.map((e) =>
e.getText(sourceFile),
);
namedImports.forEach((importName) => {
if (importName === 'provideServerRendering') {
ssrImports.add(importName);
} else {
platformServerImports.add(importName);
}
});
}
updatedContent = updatedContent.replace(node.getFullText(sourceFile), '');
} else if (moduleSpecifier.includes('@angular/ssr')) {
const importClause = node.importClause;
if (
importClause &&
importClause.namedBindings &&
ts.isNamedImports(importClause.namedBindings)
) {
importClause.namedBindings.elements.forEach((e) => {
ssrImports.add(e.getText(sourceFile));
});
}
updatedContent = updatedContent.replace(node.getFullText(sourceFile), '');
}
}
});
if (platformServerImports.size > 0) {
updatedContent =
`import { ${Array.from(platformServerImports).sort().join(', ')} } from '@angular/platform-server';\n` +
updatedContent;
}
if (ssrImports.size > 0) {
updatedContent =
`import { ${Array.from(ssrImports).sort().join(', ')} } from '@angular/ssr';\n` +
updatedContent;
}
if (content !== updatedContent) {
tree.overwrite(filePath, updatedContent);
}
}
};
}

@ -0,0 +1,75 @@
/**
* @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
*/
import { EmptyTree } from '@angular-devkit/schematics';
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
describe(`Migration to use the 'provideServerRendering' from '@angular/ssr'`, () => {
const schematicRunner = new SchematicTestRunner(
'migrations',
require.resolve('../migration-collection.json'),
);
let tree: UnitTestTree;
const schematicName = 'replace-provide-server-rendering-import';
beforeEach(() => {
tree = new UnitTestTree(new EmptyTree());
tree.create(
'/package.json',
JSON.stringify({
dependencies: {
'@angular/ssr': '0.0.0',
},
}),
);
});
it('should replace provideServerRendering with @angular/ssr and keep other imports', async () => {
tree.create(
'test.ts',
`import { provideServerRendering, otherFunction } from '@angular/platform-server';`,
);
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
const content = newTree.readContent('test.ts');
expect(content).toContain("import { provideServerRendering } from '@angular/ssr';");
expect(content).toContain("import { otherFunction } from '@angular/platform-server';");
});
it('should not replace provideServerRendering that is imported from @angular/ssr', async () => {
tree.create(
'test.ts',
`
import { otherFunction } from '@angular/platform-server';
import { provideServerRendering, provideServerRouting } from '@angular/ssr';
`,
);
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
const content = newTree.readContent('test.ts');
expect(content).toContain(
"import { provideServerRendering, provideServerRouting } from '@angular/ssr';",
);
expect(content).toContain("import { otherFunction } from '@angular/platform-server';");
});
it('should merge with existing @angular/ssr imports', async () => {
tree.create(
'test.ts',
`
import { provideServerRouting } from '@angular/ssr';
import { provideServerRendering } from '@angular/platform-server';
`,
);
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
const content = newTree.readContent('test.ts');
expect(content).toContain(
"import { provideServerRendering, provideServerRouting } from '@angular/ssr';",
);
expect(content.match(/@angular\/ssr/g) || []).toHaveSize(1);
});
});

@ -0,0 +1,114 @@
/**
* @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
*/
import { DirEntry, Rule } from '@angular-devkit/schematics';
import * as ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
import { getPackageJsonDependency } from '../../utility/dependencies';
function* visit(directory: DirEntry): IterableIterator<[fileName: string, contents: string]> {
for (const path of directory.subfiles) {
if (path.endsWith('.ts') && !path.endsWith('.d.ts')) {
const entry = directory.file(path);
if (entry) {
const content = entry.content;
if (content.includes('provideServerRouting') && content.includes('@angular/ssr')) {
// Only need to rename the import so we can just string replacements.
yield [entry.path, content.toString()];
}
}
}
}
for (const path of directory.subdirs) {
if (path === 'node_modules' || path.startsWith('.')) {
continue;
}
yield* visit(directory.dir(path));
}
}
export default function (): Rule {
return async (tree) => {
if (!getPackageJsonDependency(tree, '@angular/ssr')) {
return;
}
for (const [filePath, content] of visit(tree.root)) {
const recorder = tree.beginUpdate(filePath);
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
function visit(node: ts.Node) {
if (
ts.isPropertyAssignment(node) &&
ts.isIdentifier(node.name) &&
node.name.text === 'providers' &&
ts.isArrayLiteralExpression(node.initializer)
) {
const providersArray = node.initializer;
const newProviders = providersArray.elements
.filter((el) => {
return !(
ts.isCallExpression(el) &&
ts.isIdentifier(el.expression) &&
el.expression.text === 'provideServerRendering'
);
})
.map((el) => {
if (
ts.isCallExpression(el) &&
ts.isIdentifier(el.expression) &&
el.expression.text === 'provideServerRouting'
) {
const [withRouteVal, ...others] = el.arguments.map((arg) => arg.getText());
return `provideServerRendering(withRoutes(${withRouteVal})${others.length ? ', ' + others.join(', ') : ''})`;
}
return el.getText();
});
// Update the 'providers' array in the source file
recorder.remove(providersArray.getStart(), providersArray.getWidth());
recorder.insertRight(providersArray.getStart(), `[${newProviders.join(', ')}]`);
}
ts.forEachChild(node, visit);
}
// Visit all nodes to update 'providers'
visit(sourceFile);
// Update imports by removing 'provideServerRouting'
const importDecl = sourceFile.statements.find(
(stmt) =>
ts.isImportDeclaration(stmt) &&
ts.isStringLiteral(stmt.moduleSpecifier) &&
stmt.moduleSpecifier.text === '@angular/ssr',
) as ts.ImportDeclaration | undefined;
if (importDecl?.importClause?.namedBindings) {
const namedBindings = importDecl?.importClause.namedBindings;
if (ts.isNamedImports(namedBindings)) {
const elements = namedBindings.elements;
const updatedElements = elements
.map((el) => el.getText())
.filter((x) => x !== 'provideServerRouting');
updatedElements.push('withRoutes');
recorder.remove(namedBindings.getStart(), namedBindings.getWidth());
recorder.insertLeft(namedBindings.getStart(), `{ ${updatedElements.sort().join(', ')} }`);
}
}
tree.commitUpdate(recorder);
}
};
}

@ -0,0 +1,89 @@
/**
* @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
*/
import { EmptyTree } from '@angular-devkit/schematics';
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
describe(`Migration to replace 'provideServerRouting' with 'provideServerRendering' from '@angular/ssr'`, () => {
const schematicRunner = new SchematicTestRunner(
'migrations',
require.resolve('../migration-collection.json'),
);
const schematicName = 'replace-provide-server-routing';
let tree: UnitTestTree;
beforeEach(async () => {
tree = new UnitTestTree(new EmptyTree());
tree.create(
'/package.json',
JSON.stringify({
dependencies: {
'@angular/ssr': '0.0.0',
},
}),
);
tree.create(
'src/app/app.config.ts',
`
import { ApplicationConfig } from '@angular/core';
import { provideServerRendering, provideServerRouting } from '@angular/ssr';
import { serverRoutes } from './app.routes';
const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering(),
provideServerRouting(serverRoutes)
]
};
`,
);
});
it('should add "withRoutes" to the import statement', async () => {
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
const content = newTree.readContent('src/app/app.config.ts');
expect(content).toContain(`import { provideServerRendering, withRoutes } from '@angular/ssr';`);
});
it('should remove "provideServerRouting" and update "provideServerRendering"', async () => {
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
const content = newTree.readContent('src/app/app.config.ts');
expect(content).toContain(`providers: [provideServerRendering(withRoutes(serverRoutes))]`);
expect(content).not.toContain(`provideServerRouting(serverRoutes)`);
});
it('should correctly handle provideServerRouting with extra arguments', async () => {
tree.overwrite(
'src/app/app.config.ts',
`
import { ApplicationConfig } from '@angular/core';
import { provideServerRendering, provideServerRouting } from '@angular/ssr';
import { serverRoutes } from './app.routes';
const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering(),
provideServerRouting(serverRoutes, withAppShell(AppShellComponent))
]
};
`,
);
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
const content = newTree.readContent('src/app/app.config.ts');
expect(content).toContain(
`providers: [provideServerRendering(withRoutes(serverRoutes), withAppShell(AppShellComponent))]`,
);
expect(content).not.toContain(`provideServerRouting(serverRoutes)`);
});
});

@ -1,13 +1,12 @@
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { provideServerRouting } from '@angular/ssr';
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { App } from './app';
import { AppModule } from './app.module';
import { serverRoutes } from './app.routes.server';
@NgModule({
imports: [AppModule, ServerModule],
providers: [provideServerRouting(serverRoutes)],
imports: [AppModule],
providers: [provideServerRendering(withRoutes(serverRoutes))],
bootstrap: [App],
})
export class AppServerModule {}

@ -1,13 +1,11 @@
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { provideServerRouting } from '@angular/ssr';
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';
const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering(),
provideServerRouting(serverRoutes)
provideServerRendering(withRoutes(serverRoutes))
]
};

@ -1,5 +1,5 @@
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { provideServerRendering } from '@angular/ssr';
import { appConfig } from './app.config';
const serverConfig: ApplicationConfig = {

63
pnpm-lock.yaml generated

@ -14,9 +14,6 @@ importers:
.:
devDependencies:
'@ampproject/remapping':
specifier: 2.3.0
version: 2.3.0
'@angular/animations':
specifier: 20.0.0-next.2
version: 20.0.0-next.2(@angular/core@20.0.0-next.2)
@ -278,27 +275,12 @@ importers:
karma-source-map-support:
specifier: 1.4.0
version: 1.4.0
less:
specifier: 4.2.2
version: 4.2.2
listr2:
specifier: 8.2.5
version: 8.2.5
lmdb:
specifier: 3.2.6
version: 3.2.6
lodash:
specifier: ^4.17.21
version: 4.17.21
magic-string:
specifier: 0.30.17
version: 0.30.17
mrmime:
specifier: 2.0.1
version: 2.0.1
ng-packagr:
specifier: 20.0.0-next.1
version: 20.0.0-next.1(@angular/compiler-cli@20.0.0-next.2(@angular/compiler@20.0.0-next.2)(typescript@5.8.2))(tslib@2.8.1)(typescript@5.8.2)
npm:
specifier: ^11.0.0
version: 11.2.0
@ -308,15 +290,6 @@ importers:
ora:
specifier: 5.4.1
version: 5.4.1
parse5-html-rewriting-stream:
specifier: 7.0.0
version: 7.0.0
piscina:
specifier: 4.9.0
version: 4.9.0
postcss:
specifier: 8.5.3
version: 8.5.3
prettier:
specifier: ^3.0.0
version: 3.5.3
@ -341,9 +314,6 @@ importers:
rxjs:
specifier: 7.8.2
version: 7.8.2
sass:
specifier: 1.85.1
version: 1.85.1
semver:
specifier: 7.7.1
version: 7.7.1
@ -359,9 +329,6 @@ importers:
tar:
specifier: ^7.0.0
version: 7.4.3
tinyglobby:
specifier: 0.2.12
version: 0.2.12
tree-kill:
specifier: 1.2.2
version: 1.2.2
@ -386,9 +353,6 @@ importers:
verdaccio-auth-memory:
specifier: ^10.0.0
version: 10.2.2
watchpack:
specifier: 2.4.2
version: 2.4.2
yargs-parser:
specifier: 21.1.1
version: 21.1.1
@ -510,6 +474,15 @@ importers:
'@angular/ssr':
specifier: workspace:*
version: link:../ssr
less:
specifier: 4.2.2
version: 4.2.2
ng-packagr:
specifier: 20.0.0-next.1
version: 20.0.0-next.1(@angular/compiler-cli@20.0.0-next.2(@angular/compiler@20.0.0-next.2)(typescript@5.8.2))(tslib@2.8.1)(typescript@5.8.2)
postcss:
specifier: 8.5.3
version: 8.5.3
packages/angular/cli:
dependencies:
@ -817,6 +790,9 @@ importers:
'@angular/ssr':
specifier: workspace:*
version: link:../../angular/ssr
ng-packagr:
specifier: 20.0.0-next.1
version: 20.0.0-next.1(@angular/compiler-cli@20.0.0-next.2(@angular/compiler@20.0.0-next.2)(typescript@5.8.2))(tslib@2.8.1)(typescript@5.8.2)
undici:
specifier: 7.5.0
version: 7.5.0
@ -11636,7 +11612,8 @@ snapshots:
detect-libc@1.0.3:
optional: true
detect-libc@2.0.3: {}
detect-libc@2.0.3:
optional: true
detect-node@2.1.0: {}
@ -13457,6 +13434,7 @@ snapshots:
'@lmdb/lmdb-linux-arm64': 3.2.6
'@lmdb/lmdb-linux-x64': 3.2.6
'@lmdb/lmdb-win32-x64': 3.2.6
optional: true
loader-runner@4.3.0: {}
@ -13737,6 +13715,7 @@ snapshots:
msgpackr@1.11.2:
optionalDependencies:
msgpackr-extract: 3.0.3
optional: true
multicast-dns@7.2.5:
dependencies:
@ -13799,7 +13778,8 @@ snapshots:
nice-try@1.0.5: {}
node-addon-api@6.1.0: {}
node-addon-api@6.1.0:
optional: true
node-addon-api@7.1.1:
optional: true
@ -13831,6 +13811,7 @@ snapshots:
node-gyp-build-optional-packages@5.2.2:
dependencies:
detect-libc: 2.0.3
optional: true
node-gyp@11.1.0:
dependencies:
@ -14017,7 +13998,8 @@ snapshots:
strip-ansi: 6.0.1
wcwidth: 1.0.1
ordered-binary@1.5.3: {}
ordered-binary@1.5.3:
optional: true
os-tmpdir@1.0.2: {}
@ -15773,7 +15755,8 @@ snapshots:
dependencies:
defaults: 1.0.4
weak-lru-cache@1.2.2: {}
weak-lru-cache@1.2.2:
optional: true
web-streams-polyfill@3.3.3: {}