Skip to content

Commit 4e2a5fe

Browse files
committed
fix(@schematics/angular): enable opt-in for new @angular/ssr feature
This commit updates several schematics to make the new `@angular/ssr` feature opt-in. Users can opt in by using the `--server-routing` option or by responding with `yes` to the prompt. (cherry picked from commit 43cd458)
1 parent af95f2f commit 4e2a5fe

31 files changed

+168
-35
lines changed

packages/angular/ssr/schematics/ng-add/schema.json

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
"description": "Skip installing dependency packages.",
1616
"type": "boolean",
1717
"default": false
18+
},
19+
"serverRouting": {
20+
"description": "Creates a server application using the Server Routing and App Engine APIs (Developer Preview).",
21+
"type": "boolean"
1822
}
1923
},
2024
"required": ["project"],

packages/schematics/angular/app-shell/index.ts

+31-4
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import {
2929
import { applyToUpdateRecorder } from '../utility/change';
3030
import { getAppModulePath, isStandaloneApp } from '../utility/ng-ast-utils';
3131
import { findBootstrapApplicationCall, getMainFilePath } from '../utility/standalone/util';
32-
import { getWorkspace } from '../utility/workspace';
32+
import { getWorkspace, updateWorkspace } from '../utility/workspace';
33+
import { Builders } from '../utility/workspace-models';
3334
import { Schema as AppShellOptions } from './schema';
3435

3536
const APP_SHELL_ROUTE = 'shell';
@@ -169,6 +170,29 @@ function getMetadataProperty(metadata: ts.Node, propertyName: string): ts.Proper
169170
return property;
170171
}
171172

173+
function addAppShellConfigToWorkspace(options: AppShellOptions): Rule {
174+
return updateWorkspace((workspace) => {
175+
const project = workspace.projects.get(options.project);
176+
if (!project) {
177+
return;
178+
}
179+
const buildTarget = project.targets.get('build');
180+
if (
181+
buildTarget?.builder === Builders.Application ||
182+
buildTarget?.builder === Builders.BuildApplication
183+
) {
184+
// Application builder configuration.
185+
const prodConfig = buildTarget.configurations?.production;
186+
if (!prodConfig) {
187+
throw new SchematicsException(
188+
`A "production" configuration is not defined for the "build" builder.`,
189+
);
190+
}
191+
prodConfig.appShell = true;
192+
}
193+
});
194+
}
195+
172196
function addServerRoutes(options: AppShellOptions): Rule {
173197
return async (host: Tree) => {
174198
// The workspace gets updated so this needs to be reloaded
@@ -349,9 +373,12 @@ export default function (options: AppShellOptions): Rule {
349373
return chain([
350374
validateProject(browserEntryPoint),
351375
schematic('server', options),
352-
isStandalone ? noop() : addRouterModule(browserEntryPoint),
353-
isStandalone ? addStandaloneServerRoute(options) : addServerRoutes(options),
354-
addServerRoutingConfig(options),
376+
...(isStandalone
377+
? [addStandaloneServerRoute(options)]
378+
: [addRouterModule(browserEntryPoint), addServerRoutes(options)]),
379+
options.serverRouting
380+
? addServerRoutingConfig(options)
381+
: addAppShellConfigToWorkspace(options),
355382
schematic('component', {
356383
name: 'app-shell',
357384
module: 'app.module.server.ts',

packages/schematics/angular/app-shell/index_spec.ts

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ describe('App Shell Schematic', () => {
1919
);
2020
const defaultOptions: AppShellOptions = {
2121
project: 'bar',
22+
serverRouting: true,
2223
};
2324

2425
const workspaceOptions: WorkspaceOptions = {

packages/schematics/angular/app-shell/schema.json

+5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
"$default": {
1313
"$source": "projectName"
1414
}
15+
},
16+
"serverRouting": {
17+
"description": "Creates a server application using the Server Routing API (Developer Preview).",
18+
"type": "boolean",
19+
"default": false
1520
}
1621
},
1722
"required": ["project"]

packages/schematics/angular/application/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export default function (options: ApplicationOptions): Rule {
101101
options.ssr
102102
? schematic('ssr', {
103103
project: options.name,
104+
serverRouting: options.serverRouting,
104105
skipInstall: true,
105106
})
106107
: noop(),

packages/schematics/angular/application/schema.json

+4
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@
118118
"default": false,
119119
"x-user-analytics": "ep.ng_ssr"
120120
},
121+
"serverRouting": {
122+
"description": "Creates a server application using the Server Routing and App Engine APIs (Developer Preview).",
123+
"type": "boolean"
124+
},
121125
"experimentalZoneless": {
122126
"description": "Create an application that does not utilize zone.js.",
123127
"type": "boolean",

packages/schematics/angular/ng-new/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export default function (options: NgNewOptions): Rule {
5757
minimal: options.minimal,
5858
standalone: options.standalone,
5959
ssr: options.ssr,
60+
serverRouting: options.serverRouting,
6061
experimentalZoneless: options.experimentalZoneless,
6162
};
6263

packages/schematics/angular/ng-new/schema.json

+4
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@
139139
"type": "boolean",
140140
"x-user-analytics": "ep.ng_ssr"
141141
},
142+
"serverRouting": {
143+
"description": "Creates a server application using the Server Routing and App Engine APIs (Developer Preview).",
144+
"type": "boolean"
145+
},
142146
"experimentalZoneless": {
143147
"description": "Create an application that does not utilize zone.js.",
144148
"type": "boolean",
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { NgModule } from '@angular/core';
2-
import { ServerModule } from '@angular/platform-server';
3-
import { provideServerRoutesConfig } from '@angular/ssr';
2+
import { ServerModule } from '@angular/platform-server';<% if(serverRouting) { %>
3+
import { provideServerRoutesConfig } from '@angular/ssr';<% } %>
44
import { AppComponent } from './app.component';
5-
import { AppModule } from './app.module';
6-
import { serverRoutes } from './app.routes.server';
5+
import { AppModule } from './app.module';<% if(serverRouting) { %>
6+
import { serverRoutes } from './app.routes.server';<% } %>
77

88
@NgModule({
9-
imports: [AppModule, ServerModule],
10-
providers: [provideServerRoutesConfig(serverRoutes)],
9+
imports: [AppModule, ServerModule],<% if(serverRouting) { %>
10+
providers: [provideServerRoutesConfig(serverRoutes)],<% } %>
1111
bootstrap: [AppComponent],
1212
})
1313
export class AppServerModule {}

packages/schematics/angular/server/files/application-builder/standalone-src/app/app.config.server.ts.template

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
2-
import { provideServerRendering } from '@angular/platform-server';
3-
import { provideServerRoutesConfig } from '@angular/ssr';
4-
import { appConfig } from './app.config';
5-
import { serverRoutes } from './app.routes.server';
2+
import { provideServerRendering } from '@angular/platform-server';<% if(serverRouting) { %>
3+
import { provideServerRoutesConfig } from '@angular/ssr';<% } %>
4+
import { appConfig } from './app.config';<% if(serverRouting) { %>
5+
import { serverRoutes } from './app.routes.server';<% } %>
66

77
const serverConfig: ApplicationConfig = {
88
providers: [
9-
provideServerRendering(),
10-
provideServerRoutesConfig(serverRoutes)
9+
provideServerRendering(),<% if(serverRouting) { %>
10+
provideServerRoutesConfig(serverRoutes)<% } %>
1111
]
1212
};
1313

packages/schematics/angular/server/index.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import {
1414
apply,
1515
applyTemplates,
1616
chain,
17+
filter,
1718
mergeWith,
1819
move,
19-
renameTemplateFiles,
20+
noop,
2021
strings,
2122
url,
2223
} from '@angular-devkit/schematics';
@@ -112,7 +113,9 @@ function updateConfigFileApplicationBuilder(options: ServerOptions): Rule {
112113
serverMainEntryName,
113114
);
114115

115-
buildTarget.options['outputMode'] = 'static';
116+
if (options.serverRouting) {
117+
buildTarget.options['outputMode'] = 'static';
118+
}
116119
});
117120
}
118121

@@ -191,6 +194,9 @@ export default function (options: ServerOptions): Rule {
191194
filesUrl += isStandalone ? 'standalone-src' : 'ngmodule-src';
192195

193196
const templateSource = apply(url(filesUrl), [
197+
options.serverRouting
198+
? noop()
199+
: filter((path) => !path.endsWith('app.routes.server.ts.template')),
194200
applyTemplates({
195201
...strings,
196202
...options,

packages/schematics/angular/server/schema.json

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
"description": "Do not install packages for dependencies.",
1818
"type": "boolean",
1919
"default": false
20+
},
21+
"serverRouting": {
22+
"description": "Creates a server application using the Server Routing and App Engine APIs (Developer Preview).",
23+
"type": "boolean"
2024
}
2125
},
2226
"required": ["project"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { APP_BASE_HREF } from '@angular/common';
2+
import { CommonEngine, isMainModule } from '@angular/ssr/node';
3+
import express from 'express';
4+
import { dirname, join, resolve } from 'node:path';
5+
import { fileURLToPath } from 'node:url';
6+
import <% if (isStandalone) { %>bootstrap<% } else { %>AppServerModule<% } %> from './main.server';
7+
8+
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
9+
const browserDistFolder = resolve(serverDistFolder, '../<%= browserDistDirectory %>');
10+
const indexHtml = join(serverDistFolder, 'index.server.html');
11+
12+
const app = express();
13+
const commonEngine = new CommonEngine();
14+
15+
/**
16+
* Example Express Rest API endpoints can be defined here.
17+
* Uncomment and define endpoints as necessary.
18+
*
19+
* Example:
20+
* ```ts
21+
* app.get('/api/**', (req, res) => {
22+
* // Handle API request
23+
* });
24+
* ```
25+
*/
26+
27+
/**
28+
* Serve static files from /<%= browserDistDirectory %>
29+
*/
30+
app.get(
31+
'**',
32+
express.static(browserDistFolder, {
33+
maxAge: '1y',
34+
index: 'index.html'
35+
}),
36+
);
37+
38+
/**
39+
* Handle all other requests by rendering the Angular application.
40+
*/
41+
app.get('**', (req, res, next) => {
42+
const { protocol, originalUrl, baseUrl, headers } = req;
43+
44+
commonEngine
45+
.render({
46+
<% if (isStandalone) { %>bootstrap<% } else { %>bootstrap: AppServerModule<% } %>,
47+
documentFilePath: indexHtml,
48+
url: `${protocol}://${headers.host}${originalUrl}`,
49+
publicPath: browserDistFolder,
50+
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
51+
})
52+
.then((html) => res.send(html))
53+
.catch((err) => next(err));
54+
});
55+
56+
/**
57+
* Start the server if this module is the main entry point.
58+
* The server listens on the port defined by the `PORT` environment variable, or defaults to 4000.
59+
*/
60+
if (isMainModule(import.meta.url)) {
61+
const port = process.env['PORT'] || 4000;
62+
app.listen(port, () => {
63+
console.log(`Node Express server listening on http://localhost:${port}`);
64+
});
65+
}

packages/schematics/angular/ssr/index.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,8 @@ function updateApplicationBuilderWorkspaceConfigRule(
201201
buildTarget.options = {
202202
...buildTarget.options,
203203
outputPath,
204-
outputMode: 'server',
204+
outputMode: options.serverRouting ? 'server' : undefined,
205+
prerender: options.serverRouting ? undefined : true,
205206
ssr: {
206207
entry: join(normalize(projectSourceRoot), 'server.ts'),
207208
},
@@ -340,9 +341,12 @@ function addServerFile(
340341
? (await getApplicationBuilderOutputPaths(host, projectName)).browser
341342
: await getLegacyOutputPaths(host, projectName, 'build');
342343

344+
const applicationBuilderFiles =
345+
'application-builder' + (options.serverRouting ? '' : '-common-engine');
346+
343347
return mergeWith(
344348
apply(
345-
url(`./files/${isUsingApplicationBuilder ? 'application-builder' : 'server-builder'}`),
349+
url(`./files/${isUsingApplicationBuilder ? applicationBuilderFiles : 'server-builder'}`),
346350
[
347351
applyTemplates({
348352
...strings,

packages/schematics/angular/ssr/schema.json

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
"description": "Skip installing dependency packages.",
1616
"type": "boolean",
1717
"default": false
18+
},
19+
"serverRouting": {
20+
"description": "Creates a server application using the Server Routing and App Engine APIs (Developer Preview).",
21+
"x-prompt": "Would you like to use the Server Routing and App Engine APIs (Developer Preview) for this server application?",
22+
"type": "boolean",
23+
"default": false
1824
}
1925
},
2026
"required": ["project"],

tests/legacy-cli/e2e/tests/build/prerender/discover-routes-ngmodule.ts

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export default async function () {
3939
projectName,
4040
'--skip-confirmation',
4141
'--skip-install',
42+
'--server-routing',
4243
);
4344

4445
await useSha();

tests/legacy-cli/e2e/tests/build/prerender/http-requests-assets.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default async function () {
1313

1414
// Forcibly remove in case another test doesn't clean itself up.
1515
await rimraf('node_modules/@angular/ssr');
16-
await ng('add', '@angular/ssr', '--skip-confirmation');
16+
await ng('add', '@angular/ssr', '--server-routing', '--skip-confirmation');
1717
await useSha();
1818
await installWorkspacePackages();
1919

tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default async function () {
99
const useWebpackBuilder = !getGlobalVariable('argv')['esbuild'];
1010
// forcibly remove in case another test doesn't clean itself up
1111
await rimraf('node_modules/@angular/ssr');
12-
await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
12+
await ng('add', '@angular/ssr', '--server-routing', '--skip-confirmation', '--skip-install');
1313

1414
await useSha();
1515
await installWorkspacePackages();

tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-standalone.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { updateJsonFile, updateServerFileForWebpack, useSha } from '../../../uti
88
export default async function () {
99
// forcibly remove in case another test doesn't clean itself up
1010
await rimraf('node_modules/@angular/ssr');
11-
await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
11+
await ng('add', '@angular/ssr', '--server-routing', '--skip-confirmation', '--skip-install');
1212

1313
const useWebpackBuilder = !getGlobalVariable('argv')['esbuild'];
1414
if (!useWebpackBuilder) {

tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-base-href.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default async function () {
1919

2020
// Forcibly remove in case another test doesn't clean itself up.
2121
await uninstallPackage('@angular/ssr');
22-
await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
22+
await ng('add', '@angular/ssr', '--server-routing', '--skip-confirmation', '--skip-install');
2323
await useSha();
2424
await installWorkspacePackages();
2525

tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default async function () {
1919

2020
// Forcibly remove in case another test doesn't clean itself up.
2121
await uninstallPackage('@angular/ssr');
22-
await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
22+
await ng('add', '@angular/ssr', '--server-routing', '--skip-confirmation', '--skip-install');
2323
await useSha();
2424
await installWorkspacePackages();
2525

tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-platform-neutral.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default async function () {
2020

2121
// Forcibly remove in case another test doesn't clean itself up.
2222
await uninstallPackage('@angular/ssr');
23-
await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
23+
await ng('add', '@angular/ssr', '--server-routing', '--skip-confirmation', '--skip-install');
2424
await useSha();
2525
await installWorkspacePackages();
2626
await installPackage('h3@1');

tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default async function () {
1616

1717
// Forcibly remove in case another test doesn't clean itself up.
1818
await uninstallPackage('@angular/ssr');
19-
await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
19+
await ng('add', '@angular/ssr', '--server-routing', '--skip-confirmation', '--skip-install');
2020
await useSha();
2121
await installWorkspacePackages();
2222

tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-static-http-calls.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default async function () {
1313

1414
// Forcibly remove in case another test doesn't clean itself up.
1515
await uninstallPackage('@angular/ssr');
16-
await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
16+
await ng('add', '@angular/ssr', '--server-routing', '--skip-confirmation', '--skip-install');
1717
await useSha();
1818
await installWorkspacePackages();
1919

0 commit comments

Comments
 (0)