diff --git a/docs/generated/packages/angular/generators/application.json b/docs/generated/packages/angular/generators/application.json index 1a8e9c415f903..61fc9d05b35ce 100644 --- a/docs/generated/packages/angular/generators/application.json +++ b/docs/generated/packages/angular/generators/application.json @@ -178,6 +178,11 @@ "type": "boolean", "x-prompt": "Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)?", "default": false + }, + "serverRouting": { + "description": "Creates a server application using the Server Routing and App Engine APIs (Developer Preview). _Note: this is only supported in Angular versions >= 19.0.0_.", + "type": "boolean", + "default": false } }, "additionalProperties": false, diff --git a/docs/generated/packages/angular/generators/host.json b/docs/generated/packages/angular/generators/host.json index b5a8f60a83661..53fc32fef2401 100644 --- a/docs/generated/packages/angular/generators/host.json +++ b/docs/generated/packages/angular/generators/host.json @@ -169,6 +169,11 @@ "default": false, "x-priority": "important" }, + "serverRouting": { + "description": "Creates a server application using the Server Routing and App Engine APIs (Developer Preview). _Note: this is only supported in Angular versions >= 19.0.0_.", + "type": "boolean", + "default": false + }, "typescriptConfiguration": { "type": "boolean", "description": "Whether the module federation configuration and webpack configuration files should use TS.", diff --git a/docs/generated/packages/angular/generators/remote.json b/docs/generated/packages/angular/generators/remote.json index 268d6b61bbf4a..28e680845e075 100644 --- a/docs/generated/packages/angular/generators/remote.json +++ b/docs/generated/packages/angular/generators/remote.json @@ -162,6 +162,11 @@ "type": "boolean", "default": false }, + "serverRouting": { + "description": "Creates a server application using the Server Routing and App Engine APIs (Developer Preview). _Note: this is only supported in Angular versions >= 19.0.0_.", + "type": "boolean", + "default": false + }, "typescriptConfiguration": { "type": "boolean", "description": "Whether the module federation configuration and webpack configuration files should use TS.", diff --git a/docs/generated/packages/angular/generators/setup-ssr.json b/docs/generated/packages/angular/generators/setup-ssr.json index 8933e22e246fd..c5fab7fe34c04 100644 --- a/docs/generated/packages/angular/generators/setup-ssr.json +++ b/docs/generated/packages/angular/generators/setup-ssr.json @@ -52,6 +52,11 @@ "description": "Set up Hydration for the SSR application.", "default": true }, + "serverRouting": { + "description": "Creates a server application using the Server Routing and App Engine APIs (Developer Preview). _Note: this is only supported in Angular versions >= 19.0.0_.", + "type": "boolean", + "default": false + }, "skipFormat": { "type": "boolean", "description": "Skip formatting the workspace after the generator completes.", diff --git a/docs/generated/packages/workspace/generators/new.json b/docs/generated/packages/workspace/generators/new.json index 966acc7342925..a5e27d152e1ad 100644 --- a/docs/generated/packages/workspace/generators/new.json +++ b/docs/generated/packages/workspace/generators/new.json @@ -80,6 +80,11 @@ "type": "boolean", "default": false }, + "serverRouting": { + "description": "Use the Angular Server Routing and App Engine APIs (Developer Preview).", + "type": "boolean", + "default": false + }, "prefix": { "description": "The prefix to use for Angular component and directive selectors.", "type": "string" diff --git a/docs/generated/packages/workspace/generators/preset.json b/docs/generated/packages/workspace/generators/preset.json index b6dafd5b72c1f..9185d0be1acfb 100644 --- a/docs/generated/packages/workspace/generators/preset.json +++ b/docs/generated/packages/workspace/generators/preset.json @@ -97,6 +97,11 @@ "type": "boolean", "default": false }, + "serverRouting": { + "description": "Use the Angular Server Routing and App Engine APIs (Developer Preview).", + "type": "boolean", + "default": false + }, "prefix": { "description": "The prefix to use for Angular component and directive selectors.", "type": "string" diff --git a/packages/angular/src/generators/application/application.spec.ts b/packages/angular/src/generators/application/application.spec.ts index 83c7e4ed5755b..59a2afa27b5c5 100644 --- a/packages/angular/src/generators/application/application.spec.ts +++ b/packages/angular/src/generators/application/application.spec.ts @@ -1240,7 +1240,7 @@ describe('app', () => { await generateApp(appTree, 'app1', { ssr: true }); expect(appTree.exists('app1/src/main.server.ts')).toBe(true); - expect(appTree.exists('app1/server.ts')).toBe(true); + expect(appTree.exists('app1/src/server.ts')).toBe(true); }); }); diff --git a/packages/angular/src/generators/application/application.ts b/packages/angular/src/generators/application/application.ts index 8a4390e628c96..230b5c5fce3bc 100644 --- a/packages/angular/src/generators/application/application.ts +++ b/packages/angular/src/generators/application/application.ts @@ -98,6 +98,7 @@ export async function applicationGenerator( project: options.name, standalone: options.standalone, skipPackageJson: options.skipPackageJson, + serverRouting: options.serverRouting, }); } diff --git a/packages/angular/src/generators/application/schema.d.ts b/packages/angular/src/generators/application/schema.d.ts index 855f9e718f014..e51984ef7aaf7 100644 --- a/packages/angular/src/generators/application/schema.d.ts +++ b/packages/angular/src/generators/application/schema.d.ts @@ -29,5 +29,6 @@ export interface Schema { minimal?: boolean; bundler?: 'webpack' | 'esbuild'; ssr?: boolean; + serverRouting?: boolean; nxCloudToken?: string; } diff --git a/packages/angular/src/generators/application/schema.json b/packages/angular/src/generators/application/schema.json index dcda76406fff3..efd3056c0f433 100644 --- a/packages/angular/src/generators/application/schema.json +++ b/packages/angular/src/generators/application/schema.json @@ -181,6 +181,11 @@ "type": "boolean", "x-prompt": "Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)?", "default": false + }, + "serverRouting": { + "description": "Creates a server application using the Server Routing and App Engine APIs (Developer Preview). _Note: this is only supported in Angular versions >= 19.0.0_.", + "type": "boolean", + "default": false } }, "additionalProperties": false, diff --git a/packages/angular/src/generators/host/__snapshots__/host.spec.ts.snap b/packages/angular/src/generators/host/__snapshots__/host.spec.ts.snap index fdd39c48636aa..669bfde0b6c5a 100644 --- a/packages/angular/src/generators/host/__snapshots__/host.spec.ts.snap +++ b/packages/angular/src/generators/host/__snapshots__/host.spec.ts.snap @@ -232,7 +232,7 @@ exports[`Host App Generator --ssr should generate the correct files 9`] = ` "customWebpackConfig": { "path": "test/webpack.server.config.js", }, - "main": "test/server.ts", + "main": "test/src/server.ts", "outputPath": "dist/test/server", "tsConfig": "test/tsconfig.server.json", }, @@ -456,7 +456,7 @@ exports[`Host App Generator --ssr should generate the correct files for standalo "customWebpackConfig": { "path": "test/webpack.server.config.js", }, - "main": "test/server.ts", + "main": "test/src/server.ts", "outputPath": "dist/test/server", "tsConfig": "test/tsconfig.server.json", }, @@ -681,7 +681,7 @@ exports[`Host App Generator --ssr should generate the correct files for standalo "customWebpackConfig": { "path": "test/webpack.server.config.ts", }, - "main": "test/server.ts", + "main": "test/src/server.ts", "outputPath": "dist/test/server", "tsConfig": "test/tsconfig.server.json", }, @@ -891,7 +891,7 @@ exports[`Host App Generator --ssr should generate the correct files when --types "customWebpackConfig": { "path": "test/webpack.server.config.ts", }, - "main": "test/server.ts", + "main": "test/src/server.ts", "outputPath": "dist/test/server", "tsConfig": "test/tsconfig.server.json", }, diff --git a/packages/angular/src/generators/host/schema.d.ts b/packages/angular/src/generators/host/schema.d.ts index 7269786bed725..5bbda96316f89 100644 --- a/packages/angular/src/generators/host/schema.d.ts +++ b/packages/angular/src/generators/host/schema.d.ts @@ -27,5 +27,6 @@ export interface Schema { skipFormat?: boolean; standalone?: boolean; ssr?: boolean; + serverRouting?: boolean; typescriptConfiguration?: boolean; } diff --git a/packages/angular/src/generators/host/schema.json b/packages/angular/src/generators/host/schema.json index 4e63378f3faa1..e6fba6c11bae9 100644 --- a/packages/angular/src/generators/host/schema.json +++ b/packages/angular/src/generators/host/schema.json @@ -172,6 +172,11 @@ "default": false, "x-priority": "important" }, + "serverRouting": { + "description": "Creates a server application using the Server Routing and App Engine APIs (Developer Preview). _Note: this is only supported in Angular versions >= 19.0.0_.", + "type": "boolean", + "default": false + }, "typescriptConfiguration": { "type": "boolean", "description": "Whether the module federation configuration and webpack configuration files should use TS.", diff --git a/packages/angular/src/generators/remote/__snapshots__/remote.spec.ts.snap b/packages/angular/src/generators/remote/__snapshots__/remote.spec.ts.snap index 12b9dcaebdb7c..c7a5b1ae24aba 100644 --- a/packages/angular/src/generators/remote/__snapshots__/remote.spec.ts.snap +++ b/packages/angular/src/generators/remote/__snapshots__/remote.spec.ts.snap @@ -212,7 +212,7 @@ exports[`MF Remote App Generator --ssr should generate the correct files 11`] = "customWebpackConfig": { "path": "test/webpack.server.config.js", }, - "main": "test/server.ts", + "main": "test/src/server.ts", "outputPath": "dist/test/server", "tsConfig": "test/tsconfig.server.json", }, @@ -438,7 +438,7 @@ exports[`MF Remote App Generator --ssr should generate the correct files when -- "customWebpackConfig": { "path": "test/webpack.server.config.ts", }, - "main": "test/server.ts", + "main": "test/src/server.ts", "outputPath": "dist/test/server", "tsConfig": "test/tsconfig.server.json", }, diff --git a/packages/angular/src/generators/remote/schema.d.ts b/packages/angular/src/generators/remote/schema.d.ts index 36e9fac09b841..4b73b14d0e944 100644 --- a/packages/angular/src/generators/remote/schema.d.ts +++ b/packages/angular/src/generators/remote/schema.d.ts @@ -26,5 +26,6 @@ export interface Schema { skipFormat?: boolean; standalone?: boolean; ssr?: boolean; + serverRouting?: boolean; typescriptConfiguration?: boolean; } diff --git a/packages/angular/src/generators/remote/schema.json b/packages/angular/src/generators/remote/schema.json index 8176ca31a6ad5..8be3489eae3cf 100644 --- a/packages/angular/src/generators/remote/schema.json +++ b/packages/angular/src/generators/remote/schema.json @@ -165,6 +165,11 @@ "type": "boolean", "default": false }, + "serverRouting": { + "description": "Creates a server application using the Server Routing and App Engine APIs (Developer Preview). _Note: this is only supported in Angular versions >= 19.0.0_.", + "type": "boolean", + "default": false + }, "typescriptConfiguration": { "type": "boolean", "description": "Whether the module federation configuration and webpack configuration files should use TS.", diff --git a/packages/angular/src/generators/setup-ssr/__snapshots__/setup-ssr.spec.ts.snap b/packages/angular/src/generators/setup-ssr/__snapshots__/setup-ssr.spec.ts.snap index 7b16b5af8536a..4e96a33d6fccd 100644 --- a/packages/angular/src/generators/setup-ssr/__snapshots__/setup-ssr.spec.ts.snap +++ b/packages/angular/src/generators/setup-ssr/__snapshots__/setup-ssr.spec.ts.snap @@ -43,7 +43,7 @@ exports[`setupSSR with application builder should create the files correctly for "scripts": [], "server": "app1/src/main.server.ts", "ssr": { - "entry": "app1/server.ts", + "entry": "app1/src/server.ts", }, "styles": [ "app1/src/styles.css", @@ -58,65 +58,70 @@ exports[`setupSSR with application builder should create the files correctly for exports[`setupSSR with application builder should create the files correctly for ssr 2`] = ` "import { APP_BASE_HREF } from '@angular/common'; -import { CommonEngine } from '@angular/ssr/node'; +import { CommonEngine, isMainModule } from '@angular/ssr/node'; import express from 'express'; -import { fileURLToPath } from 'node:url'; import { dirname, join, resolve } from 'node:path'; -import AppServerModule from './src/main.server'; - -// The Express app is exported so that it can be used by serverless Functions. -export function app(): express.Express { - const server = express(); - const serverDistFolder = dirname(fileURLToPath(import.meta.url)); - const browserDistFolder = resolve(serverDistFolder, '../browser'); - const indexHtml = join(serverDistFolder, 'index.server.html'); - - const commonEngine = new CommonEngine(); - - server.set('view engine', 'html'); - server.set('views', browserDistFolder); - - // Example Express Rest API endpoints - // server.get('/api/**', (req, res) => { }); - // Serve static files from /browser - server.get( - '**', - express.static(browserDistFolder, { - maxAge: '1y', - index: 'index.html', +import { fileURLToPath } from 'node:url'; +import AppServerModule from './main.server'; + +const serverDistFolder = dirname(fileURLToPath(import.meta.url)); +const browserDistFolder = resolve(serverDistFolder, '../browser'); +const indexHtml = join(serverDistFolder, 'index.server.html'); + +const app = express(); +const commonEngine = new CommonEngine(); + +/** + * Example Express Rest API endpoints can be defined here. + * Uncomment and define endpoints as necessary. + * + * Example: + * \`\`\`ts + * app.get('/api/**', (req, res) => { + * // Handle API request + * }); + * \`\`\` + */ + +/** + * Serve static files from /browser + */ +app.get( + '**', + express.static(browserDistFolder, { + maxAge: '1y', + index: 'index.html', + }) +); + +/** + * Handle all other requests by rendering the Angular application. + */ +app.get('**', (req, res, next) => { + const { protocol, originalUrl, baseUrl, headers } = req; + + commonEngine + .render({ + bootstrap: AppServerModule, + documentFilePath: indexHtml, + url: \`\${protocol}://\${headers.host}\${originalUrl}\`, + publicPath: browserDistFolder, + providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], }) - ); - - // All regular routes use the Angular engine - server.get('**', (req, res, next) => { - const { protocol, originalUrl, baseUrl, headers } = req; - - commonEngine - .render({ - bootstrap: AppServerModule, - documentFilePath: indexHtml, - url: \`\${protocol}://\${headers.host}\${originalUrl}\`, - publicPath: browserDistFolder, - providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], - }) - .then((html) => res.send(html)) - .catch((err) => next(err)); - }); - - return server; -} - -function run(): void { + .then((html) => res.send(html)) + .catch((err) => next(err)); +}); + +/** + * Start the server if this module is the main entry point. + * The server listens on the port defined by the \`PORT\` environment variable, or defaults to 4000. + */ +if (isMainModule(import.meta.url)) { const port = process.env['PORT'] || 4000; - - // Start up the Node server - const server = app(); - server.listen(port, () => { + app.listen(port, () => { console.log(\`Node Express server listening on http://localhost:\${port}\`); }); } - -run(); " `; @@ -163,7 +168,7 @@ exports[`setupSSR with application builder should create the files correctly for "scripts": [], "server": "app1/src/main.server.ts", "ssr": { - "entry": "app1/server.ts", + "entry": "app1/src/server.ts", }, "styles": [ "app1/src/styles.css", @@ -178,65 +183,70 @@ exports[`setupSSR with application builder should create the files correctly for exports[`setupSSR with application builder should create the files correctly for ssr when app is standalone 2`] = ` "import { APP_BASE_HREF } from '@angular/common'; -import { CommonEngine } from '@angular/ssr/node'; +import { CommonEngine, isMainModule } from '@angular/ssr/node'; import express from 'express'; -import { fileURLToPath } from 'node:url'; import { dirname, join, resolve } from 'node:path'; -import bootstrap from './src/main.server'; - -// The Express app is exported so that it can be used by serverless Functions. -export function app(): express.Express { - const server = express(); - const serverDistFolder = dirname(fileURLToPath(import.meta.url)); - const browserDistFolder = resolve(serverDistFolder, '../browser'); - const indexHtml = join(serverDistFolder, 'index.server.html'); - - const commonEngine = new CommonEngine(); - - server.set('view engine', 'html'); - server.set('views', browserDistFolder); - - // Example Express Rest API endpoints - // server.get('/api/**', (req, res) => { }); - // Serve static files from /browser - server.get( - '**', - express.static(browserDistFolder, { - maxAge: '1y', - index: 'index.html', +import { fileURLToPath } from 'node:url'; +import bootstrap from './main.server'; + +const serverDistFolder = dirname(fileURLToPath(import.meta.url)); +const browserDistFolder = resolve(serverDistFolder, '../browser'); +const indexHtml = join(serverDistFolder, 'index.server.html'); + +const app = express(); +const commonEngine = new CommonEngine(); + +/** + * Example Express Rest API endpoints can be defined here. + * Uncomment and define endpoints as necessary. + * + * Example: + * \`\`\`ts + * app.get('/api/**', (req, res) => { + * // Handle API request + * }); + * \`\`\` + */ + +/** + * Serve static files from /browser + */ +app.get( + '**', + express.static(browserDistFolder, { + maxAge: '1y', + index: 'index.html', + }) +); + +/** + * Handle all other requests by rendering the Angular application. + */ +app.get('**', (req, res, next) => { + const { protocol, originalUrl, baseUrl, headers } = req; + + commonEngine + .render({ + bootstrap, + documentFilePath: indexHtml, + url: \`\${protocol}://\${headers.host}\${originalUrl}\`, + publicPath: browserDistFolder, + providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], }) - ); - - // All regular routes use the Angular engine - server.get('**', (req, res, next) => { - const { protocol, originalUrl, baseUrl, headers } = req; - - commonEngine - .render({ - bootstrap, - documentFilePath: indexHtml, - url: \`\${protocol}://\${headers.host}\${originalUrl}\`, - publicPath: browserDistFolder, - providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], - }) - .then((html) => res.send(html)) - .catch((err) => next(err)); - }); - - return server; -} - -function run(): void { + .then((html) => res.send(html)) + .catch((err) => next(err)); +}); + +/** + * Start the server if this module is the main entry point. + * The server listens on the port defined by the \`PORT\` environment variable, or defaults to 4000. + */ +if (isMainModule(import.meta.url)) { const port = process.env['PORT'] || 4000; - - // Start up the Node server - const server = app(); - server.listen(port, () => { + app.listen(port, () => { console.log(\`Node Express server listening on http://localhost:\${port}\`); }); } - -run(); " `; @@ -260,7 +270,7 @@ exports[`setupSSR with browser builder should create the files correctly for ssr ], "executor": "@angular-devkit/build-angular:server", "options": { - "main": "app1/server.ts", + "main": "app1/src/server.ts", "outputPath": "dist/app1/server", "tsConfig": "app1/tsconfig.server.json", }, @@ -275,7 +285,7 @@ import { CommonEngine } from '@angular/ssr/node'; import * as express from 'express'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; -import AppServerModule from './src/main.server'; +import AppServerModule from './main.server'; // The Express app is exported so that it can be used by serverless Functions. export function app(): express.Express { @@ -293,12 +303,9 @@ export function app(): express.Express { // Example Express Rest API endpoints // server.get('/api/**', (req, res) => { }); // Serve static files from /browser - server.get( - '*.*', - express.static(distFolder, { - maxAge: '1y', - }) - ); + server.get('*.*', express.static(distFolder, { + maxAge: '1y' + })); // All regular routes use the Angular engine server.get('*', (req, res, next) => { @@ -363,7 +370,7 @@ exports[`setupSSR with browser builder should create the files correctly for ssr ], "executor": "@angular-devkit/build-angular:server", "options": { - "main": "app1/server.ts", + "main": "app1/src/server.ts", "outputPath": "dist/app1/server", "tsConfig": "app1/tsconfig.server.json", }, @@ -378,7 +385,7 @@ import { CommonEngine } from '@angular/ssr/node'; import * as express from 'express'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; -import bootstrap from './src/main.server'; +import bootstrap from './main.server'; // The Express app is exported so that it can be used by serverless Functions. export function app(): express.Express { @@ -396,12 +403,9 @@ export function app(): express.Express { // Example Express Rest API endpoints // server.get('/api/**', (req, res) => { }); // Serve static files from /browser - server.get( - '*.*', - express.static(distFolder, { - maxAge: '1y', - }) - ); + server.get('*.*', express.static(distFolder, { + maxAge: '1y' + })); // All regular routes use the Angular engine server.get('*', (req, res, next) => { diff --git a/packages/angular/src/generators/setup-ssr/files/ngmodule/src/__main__ b/packages/angular/src/generators/setup-ssr/files/pre-v19/ngmodule-src/__main__ similarity index 100% rename from packages/angular/src/generators/setup-ssr/files/ngmodule/src/__main__ rename to packages/angular/src/generators/setup-ssr/files/pre-v19/ngmodule-src/__main__ diff --git a/packages/angular/src/generators/setup-ssr/files/ngmodule/src/app/__rootModuleFileName__ b/packages/angular/src/generators/setup-ssr/files/pre-v19/ngmodule-src/app/__rootModuleFileName__ similarity index 100% rename from packages/angular/src/generators/setup-ssr/files/ngmodule/src/app/__rootModuleFileName__ rename to packages/angular/src/generators/setup-ssr/files/pre-v19/ngmodule-src/app/__rootModuleFileName__ diff --git a/packages/angular/src/generators/setup-ssr/files/root/tsconfig.server.json__tpl__ b/packages/angular/src/generators/setup-ssr/files/pre-v19/root/tsconfig.server.json__tpl__ similarity index 100% rename from packages/angular/src/generators/setup-ssr/files/root/tsconfig.server.json__tpl__ rename to packages/angular/src/generators/setup-ssr/files/pre-v19/root/tsconfig.server.json__tpl__ diff --git a/packages/angular/src/generators/setup-ssr/files/server/application-builder/__serverFileName__ b/packages/angular/src/generators/setup-ssr/files/pre-v19/server/application-builder/__serverFileName__ similarity index 95% rename from packages/angular/src/generators/setup-ssr/files/server/application-builder/__serverFileName__ rename to packages/angular/src/generators/setup-ssr/files/pre-v19/server/application-builder/__serverFileName__ index c284f5b361a2f..5960141bcb76b 100644 --- a/packages/angular/src/generators/setup-ssr/files/server/application-builder/__serverFileName__ +++ b/packages/angular/src/generators/setup-ssr/files/pre-v19/server/application-builder/__serverFileName__ @@ -1,5 +1,5 @@ import { APP_BASE_HREF } from '@angular/common'; -import { CommonEngine } from '<%= commonEngineEntryPoint %>'; +import { CommonEngine } from '@angular/ssr'; import express from 'express'; import { fileURLToPath } from 'node:url'; import { dirname, join, resolve } from 'node:path'; @@ -9,7 +9,7 @@ import <% if (standalone) { %>bootstrap<% } else { %><%= rootModuleClassName %>< export function app(): express.Express { const server = express(); const serverDistFolder = dirname(fileURLToPath(import.meta.url)); - const browserDistFolder = resolve(serverDistFolder, '../<%= browserBundleOutputPath %>'); + const browserDistFolder = resolve(serverDistFolder, '../<%= browserDistDirectory %>'); const indexHtml = join(serverDistFolder, 'index.server.html'); const commonEngine = new CommonEngine(); diff --git a/packages/angular/src/generators/setup-ssr/files/server/server-builder/__serverFileName__ b/packages/angular/src/generators/setup-ssr/files/pre-v19/server/server-builder/__serverFileName__ similarity index 94% rename from packages/angular/src/generators/setup-ssr/files/server/server-builder/__serverFileName__ rename to packages/angular/src/generators/setup-ssr/files/pre-v19/server/server-builder/__serverFileName__ index dd656fea2d379..90a4b80576612 100644 --- a/packages/angular/src/generators/setup-ssr/files/server/server-builder/__serverFileName__ +++ b/packages/angular/src/generators/setup-ssr/files/pre-v19/server/server-builder/__serverFileName__ @@ -1,7 +1,7 @@ import 'zone.js/node'; import { APP_BASE_HREF } from '@angular/common'; -import { CommonEngine } from '<%= commonEngineEntryPoint %>'; +import { CommonEngine } from '@angular/ssr'; import * as express from 'express'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; @@ -10,7 +10,7 @@ import <% if (standalone) { %>bootstrap<% } else { %><%= rootModuleClassName %>< // The Express app is exported so that it can be used by serverless Functions. export function app(): express.Express { const server = express(); - const distFolder = join(process.cwd(), '<%= browserBundleOutputPath %>'); + const distFolder = join(process.cwd(), '<%= browserDistDirectory %>'); const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? join(distFolder, 'index.original.html') : join(distFolder, 'index.html'); diff --git a/packages/angular/src/generators/setup-ssr/files/standalone/src/__main__ b/packages/angular/src/generators/setup-ssr/files/pre-v19/standalone-src/__main__ similarity index 100% rename from packages/angular/src/generators/setup-ssr/files/standalone/src/__main__ rename to packages/angular/src/generators/setup-ssr/files/pre-v19/standalone-src/__main__ diff --git a/packages/angular/src/generators/setup-ssr/files/standalone/src/app/app.config.server.ts__tpl__ b/packages/angular/src/generators/setup-ssr/files/pre-v19/standalone-src/app/app.config.server.ts__tpl__ similarity index 100% rename from packages/angular/src/generators/setup-ssr/files/standalone/src/app/app.config.server.ts__tpl__ rename to packages/angular/src/generators/setup-ssr/files/pre-v19/standalone-src/app/app.config.server.ts__tpl__ diff --git a/packages/angular/src/generators/setup-ssr/files/v19+/application-builder-common-engine/server/__serverFileName__ b/packages/angular/src/generators/setup-ssr/files/v19+/application-builder-common-engine/server/__serverFileName__ new file mode 100644 index 0000000000000..62e440d80cd98 --- /dev/null +++ b/packages/angular/src/generators/setup-ssr/files/v19+/application-builder-common-engine/server/__serverFileName__ @@ -0,0 +1,65 @@ +import { APP_BASE_HREF } from '@angular/common'; +import { CommonEngine, isMainModule } from '@angular/ssr/node'; +import express from 'express'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import <% if (standalone) { %>bootstrap<% } else { %><%= rootModuleClassName %><% } %> from './<%= main.slice(0, -3) %>'; + +const serverDistFolder = dirname(fileURLToPath(import.meta.url)); +const browserDistFolder = resolve(serverDistFolder, '../<%= browserDistDirectory %>'); +const indexHtml = join(serverDistFolder, 'index.server.html'); + +const app = express(); +const commonEngine = new CommonEngine(); + +/** + * Example Express Rest API endpoints can be defined here. + * Uncomment and define endpoints as necessary. + * + * Example: + * ```ts + * app.get('/api/**', (req, res) => { + * // Handle API request + * }); + * ``` + */ + +/** + * Serve static files from /<%= browserDistDirectory %> + */ +app.get( + '**', + express.static(browserDistFolder, { + maxAge: '1y', + index: 'index.html' + }), +); + +/** + * Handle all other requests by rendering the Angular application. + */ +app.get('**', (req, res, next) => { + const { protocol, originalUrl, baseUrl, headers } = req; + + commonEngine + .render({ + bootstrap<% if (!standalone) { %>: <%= rootModuleClassName %><% } %>, + documentFilePath: indexHtml, + url: `${protocol}://${headers.host}${originalUrl}`, + publicPath: browserDistFolder, + providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], + }) + .then((html) => res.send(html)) + .catch((err) => next(err)); +}); + +/** + * Start the server if this module is the main entry point. + * The server listens on the port defined by the `PORT` environment variable, or defaults to 4000. + */ +if (isMainModule(import.meta.url)) { + const port = process.env['PORT'] || <%= serverPort %>; + app.listen(port, () => { + console.log(`Node Express server listening on http://localhost:${port}`); + }); +} diff --git a/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/ngmodule-src/__main__ b/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/ngmodule-src/__main__ new file mode 100644 index 0000000000000..063a6f9eac8a5 --- /dev/null +++ b/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/ngmodule-src/__main__ @@ -0,0 +1 @@ +export { <%= rootModuleClassName %> as default } from './app/<%= rootModuleFileName.slice(0, -3) %>'; diff --git a/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/ngmodule-src/app/__rootModuleFileName__ b/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/ngmodule-src/app/__rootModuleFileName__ new file mode 100644 index 0000000000000..de44f6010d669 --- /dev/null +++ b/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/ngmodule-src/app/__rootModuleFileName__ @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { ServerModule } from '@angular/platform-server';<% if(serverRouting) { %> +import { provideServerRoutesConfig } from '@angular/ssr';<% } %> +import { AppComponent } from './app.component'; +import { AppModule } from './app.module';<% if(serverRouting) { %> +import { serverRoutes } from './app.routes.server';<% } %> + +@NgModule({ + imports: [AppModule, ServerModule],<% if(serverRouting) { %> + providers: [provideServerRoutesConfig(serverRoutes)],<% } %> + bootstrap: [AppComponent], +}) +export class <%= rootModuleClassName %> {} diff --git a/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/ngmodule-src/app/app.routes.server.ts__tpl__ b/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/ngmodule-src/app/app.routes.server.ts__tpl__ new file mode 100644 index 0000000000000..ffd37b1f233c6 --- /dev/null +++ b/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/ngmodule-src/app/app.routes.server.ts__tpl__ @@ -0,0 +1,8 @@ +import { RenderMode, ServerRoute } from '@angular/ssr'; + +export const serverRoutes: ServerRoute[] = [ + { + path: '**', + renderMode: RenderMode.Prerender + } +]; diff --git a/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/server/__serverFileName__ b/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/server/__serverFileName__ new file mode 100644 index 0000000000000..fc07d1d4f98c2 --- /dev/null +++ b/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/server/__serverFileName__ @@ -0,0 +1,66 @@ +import { + AngularNodeAppEngine, + createNodeRequestHandler, + isMainModule, + writeResponseToNodeResponse, +} from '@angular/ssr/node'; +import express from 'express'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const serverDistFolder = dirname(fileURLToPath(import.meta.url)); +const browserDistFolder = resolve(serverDistFolder, '../<%= browserDistDirectory %>'); + +const app = express(); +const angularApp = new AngularNodeAppEngine(); + +/** + * Example Express Rest API endpoints can be defined here. + * Uncomment and define endpoints as necessary. + * + * Example: + * ```ts + * app.get('/api/**', (req, res) => { + * // Handle API request + * }); + * ``` + */ + +/** + * Serve static files from /<%= browserDistDirectory %> + */ +app.use( + express.static(browserDistFolder, { + maxAge: '1y', + index: false, + redirect: false, + }) +); + +/** + * Handle all other requests by rendering the Angular application. + */ +app.use('/**', (req, res, next) => { + angularApp + .handle(req) + .then((response) => + response ? writeResponseToNodeResponse(response, res) : next() + ) + .catch(next); +}); + +/** + * Start the server if this module is the main entry point. + * The server listens on the port defined by the `PORT` environment variable, or defaults to <%= serverPort %>. + */ +if (isMainModule(import.meta.url)) { + const port = process.env['PORT'] || <%= serverPort %>; + app.listen(port, () => { + console.log(`Node Express server listening on http://localhost:${port}`); + }); +} + +/** + * The request handler used by the Angular CLI (dev-server and during build). + */ +export const reqHandler = createNodeRequestHandler(app); diff --git a/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/standalone-src/__main__ b/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/standalone-src/__main__ new file mode 100644 index 0000000000000..4b9d4d1545c1a --- /dev/null +++ b/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/standalone-src/__main__ @@ -0,0 +1,7 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { config } from './app/app.config.server'; + +const bootstrap = () => bootstrapApplication(AppComponent, config); + +export default bootstrap; diff --git a/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/standalone-src/app/app.config.server.ts__tpl__ b/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/standalone-src/app/app.config.server.ts__tpl__ new file mode 100644 index 0000000000000..ccc12e6b84458 --- /dev/null +++ b/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/standalone-src/app/app.config.server.ts__tpl__ @@ -0,0 +1,14 @@ +import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; +import { provideServerRendering } from '@angular/platform-server';<% if(serverRouting) { %> +import { provideServerRoutesConfig } from '@angular/ssr';<% } %> +import { appConfig } from './app.config';<% if(serverRouting) { %> +import { serverRoutes } from './app.routes.server';<% } %> + +const serverConfig: ApplicationConfig = { + providers: [ + provideServerRendering()<% if(serverRouting) { %>, + provideServerRoutesConfig(serverRoutes)<% } %> + ] +}; + +export const config = mergeApplicationConfig(appConfig, serverConfig); diff --git a/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/standalone-src/app/app.routes.server.ts__tpl__ b/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/standalone-src/app/app.routes.server.ts__tpl__ new file mode 100644 index 0000000000000..ffd37b1f233c6 --- /dev/null +++ b/packages/angular/src/generators/setup-ssr/files/v19+/application-builder/standalone-src/app/app.routes.server.ts__tpl__ @@ -0,0 +1,8 @@ +import { RenderMode, ServerRoute } from '@angular/ssr'; + +export const serverRoutes: ServerRoute[] = [ + { + path: '**', + renderMode: RenderMode.Prerender + } +]; diff --git a/packages/angular/src/generators/setup-ssr/files/v19+/server-builder/ngmodule-src/__main__ b/packages/angular/src/generators/setup-ssr/files/v19+/server-builder/ngmodule-src/__main__ new file mode 100644 index 0000000000000..063a6f9eac8a5 --- /dev/null +++ b/packages/angular/src/generators/setup-ssr/files/v19+/server-builder/ngmodule-src/__main__ @@ -0,0 +1 @@ +export { <%= rootModuleClassName %> as default } from './app/<%= rootModuleFileName.slice(0, -3) %>'; diff --git a/packages/angular/src/generators/setup-ssr/files/v19+/server-builder/ngmodule-src/app/__rootModuleFileName__ b/packages/angular/src/generators/setup-ssr/files/v19+/server-builder/ngmodule-src/app/__rootModuleFileName__ new file mode 100644 index 0000000000000..af9a56e17c922 --- /dev/null +++ b/packages/angular/src/generators/setup-ssr/files/v19+/server-builder/ngmodule-src/app/__rootModuleFileName__ @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { ServerModule } from '@angular/platform-server'; + +import { AppModule } from './app.module'; +import { AppComponent } from './app.component'; + +@NgModule({ + imports: [ + AppModule, + ServerModule, + ], + bootstrap: [AppComponent], +}) +export class <%= rootModuleClassName %> {} diff --git a/packages/angular/src/generators/setup-ssr/files/v19+/server-builder/root/tsconfig.server.json.template b/packages/angular/src/generators/setup-ssr/files/v19+/server-builder/root/tsconfig.server.json.template new file mode 100644 index 0000000000000..9f8fb4f01ae0f --- /dev/null +++ b/packages/angular/src/generators/setup-ssr/files/v19+/server-builder/root/tsconfig.server.json.template @@ -0,0 +1,16 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "<%= rootOffset %>out-tsc/server", + "types": [ + "node"<% if (hasLocalizePackage) { %>, + "@angular/localize"<% } %> + ] + }, + "files": [ + "src/<%= main %>", + "src/server.ts" + ] +} diff --git a/packages/angular/src/generators/setup-ssr/files/v19+/server-builder/server/__serverFileName__ b/packages/angular/src/generators/setup-ssr/files/v19+/server-builder/server/__serverFileName__ new file mode 100644 index 0000000000000..abeb7c54c27c1 --- /dev/null +++ b/packages/angular/src/generators/setup-ssr/files/v19+/server-builder/server/__serverFileName__ @@ -0,0 +1,69 @@ +import 'zone.js/node'; + +import { APP_BASE_HREF } from '@angular/common'; +import { CommonEngine } from '@angular/ssr/node'; +import * as express from 'express'; +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import <% if (standalone) { %>bootstrap<% } else { %><%= rootModuleClassName %><% } %> from './<%= main.slice(0, -3) %>'; + +// The Express app is exported so that it can be used by serverless Functions. +export function app(): express.Express { + const server = express(); + const distFolder = join(process.cwd(), '<%= browserDistDirectory %>'); + const indexHtml = existsSync(join(distFolder, 'index.original.html')) + ? join(distFolder, 'index.original.html') + : join(distFolder, 'index.html'); + + const commonEngine = new CommonEngine(); + + server.set('view engine', 'html'); + server.set('views', distFolder); + + // Example Express Rest API endpoints + // server.get('/api/**', (req, res) => { }); + // Serve static files from /browser + server.get('*.*', express.static(distFolder, { + maxAge: '1y' + })); + + // All regular routes use the Angular engine + server.get('*', (req, res, next) => { + const { protocol, originalUrl, baseUrl, headers } = req; + + commonEngine + .render({ + bootstrap<% if (!standalone) { %>: <%= rootModuleClassName %><% } %>, + documentFilePath: indexHtml, + url: `${protocol}://${headers.host}${originalUrl}`, + publicPath: distFolder, + providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], + }) + .then((html) => res.send(html)) + .catch((err) => next(err)); + }); + + return server; +} + +function run(): void { + const port = process.env['PORT'] || <%= serverPort %>; + + // Start up the Node server + const server = app(); + server.listen(port, () => { + console.log(`Node Express server listening on http://localhost:${port}`); + }); +} + +// Webpack will replace 'require' with '__webpack_require__' +// '__non_webpack_require__' is a proxy to Node 'require' +// The below code is to ensure that the server is run only when not requiring the bundle. +declare const __non_webpack_require__: NodeRequire; +const mainModule = __non_webpack_require__.main; +const moduleFilename = mainModule && mainModule.filename || ''; +if (moduleFilename === __filename || moduleFilename.includes('iisnode')) { + run(); +} + +export default <% if (standalone) { %>bootstrap<% } else { %><%= rootModuleClassName %><% } %>; diff --git a/packages/angular/src/generators/setup-ssr/files/v19+/server-builder/standalone-src/__main__ b/packages/angular/src/generators/setup-ssr/files/v19+/server-builder/standalone-src/__main__ new file mode 100644 index 0000000000000..4b9d4d1545c1a --- /dev/null +++ b/packages/angular/src/generators/setup-ssr/files/v19+/server-builder/standalone-src/__main__ @@ -0,0 +1,7 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { config } from './app/app.config.server'; + +const bootstrap = () => bootstrapApplication(AppComponent, config); + +export default bootstrap; diff --git a/packages/angular/src/generators/setup-ssr/files/v19+/server-builder/standalone-src/app/app.config.server.ts.template b/packages/angular/src/generators/setup-ssr/files/v19+/server-builder/standalone-src/app/app.config.server.ts.template new file mode 100644 index 0000000000000..b4d57c94235f8 --- /dev/null +++ b/packages/angular/src/generators/setup-ssr/files/v19+/server-builder/standalone-src/app/app.config.server.ts.template @@ -0,0 +1,11 @@ +import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; +import { provideServerRendering } from '@angular/platform-server'; +import { appConfig } from './app.config'; + +const serverConfig: ApplicationConfig = { + providers: [ + provideServerRendering() + ] +}; + +export const config = mergeApplicationConfig(appConfig, serverConfig); diff --git a/packages/angular/src/generators/setup-ssr/lib/add-hydration.spec.ts b/packages/angular/src/generators/setup-ssr/lib/add-hydration.spec.ts index 24478ced9cf5f..8d6fae01c27e5 100644 --- a/packages/angular/src/generators/setup-ssr/lib/add-hydration.spec.ts +++ b/packages/angular/src/generators/setup-ssr/lib/add-hydration.spec.ts @@ -28,7 +28,11 @@ export const appConfig: ApplicationConfig = { ` ); - addHydration(tree, { project: 'app1', standalone: true }); + addHydration(tree, { + project: 'app1', + standalone: true, + isUsingApplicationBuilder: true, + }); expect(tree.read('app1/src/app/app.config.ts', 'utf-8')) .toMatchInlineSnapshot(` @@ -58,7 +62,11 @@ export const appConfig: ApplicationConfig = { ` ); - addHydration(tree, { project: 'app1', standalone: true }); + addHydration(tree, { + project: 'app1', + standalone: true, + isUsingApplicationBuilder: true, + }); expect(tree.read('app1/src/app/app.config.ts', 'utf-8')) .toMatchInlineSnapshot(` @@ -93,7 +101,11 @@ export class AppModule {} ` ); - addHydration(tree, { project: 'app1', standalone: false }); + addHydration(tree, { + project: 'app1', + standalone: false, + isUsingApplicationBuilder: true, + }); expect(tree.read('app1/src/app/app.module.ts', 'utf-8')) .toMatchInlineSnapshot(` @@ -135,7 +147,11 @@ export class AppModule {} ` ); - addHydration(tree, { project: 'app1', standalone: false }); + addHydration(tree, { + project: 'app1', + standalone: false, + isUsingApplicationBuilder: true, + }); expect(tree.read('app1/src/app/app.module.ts', 'utf-8')) .toMatchInlineSnapshot(` diff --git a/packages/angular/src/generators/setup-ssr/lib/add-hydration.ts b/packages/angular/src/generators/setup-ssr/lib/add-hydration.ts index 537c32e0bdd00..f9b7db762e60c 100644 --- a/packages/angular/src/generators/setup-ssr/lib/add-hydration.ts +++ b/packages/angular/src/generators/setup-ssr/lib/add-hydration.ts @@ -11,12 +11,12 @@ import { addProviderToModule, } from '../../../utils/nx-devkit/ast-utils'; import { getInstalledAngularVersionInfo } from '../../utils/version-utils'; -import { type Schema } from '../schema'; +import type { NormalizedGeneratorOptions } from '../schema'; let tsModule: typeof import('typescript'); let tsquery: typeof import('@phenomnomnominal/tsquery').tsquery; -export function addHydration(tree: Tree, options: Schema) { +export function addHydration(tree: Tree, options: NormalizedGeneratorOptions) { const projectConfig = readProjectConfiguration(tree, options.project); if (!tsModule) { diff --git a/packages/angular/src/generators/setup-ssr/lib/add-server-file.ts b/packages/angular/src/generators/setup-ssr/lib/add-server-file.ts index be6edaf2bdfbe..09b10ae3935ca 100644 --- a/packages/angular/src/generators/setup-ssr/lib/add-server-file.ts +++ b/packages/angular/src/generators/setup-ssr/lib/add-server-file.ts @@ -4,40 +4,52 @@ import { joinPathFragments, readProjectConfiguration, } from '@nx/devkit'; +import { join } from 'path'; import { getInstalledAngularVersionInfo } from '../../utils/version-utils'; -import type { Schema } from '../schema'; +import type { NormalizedGeneratorOptions } from '../schema'; import { DEFAULT_BROWSER_DIR } from './constants'; -export function addServerFile( - tree: Tree, - schema: Schema, - isUsingApplicationBuilder: boolean -) { - const { root: projectRoot, targets } = readProjectConfiguration( - tree, - schema.project - ); - const { outputPath } = targets.build.options; - const browserBundleOutputPath = isUsingApplicationBuilder +export function addServerFile(tree: Tree, options: NormalizedGeneratorOptions) { + const project = readProjectConfiguration(tree, options.project); + const { outputPath } = project.targets.build.options; + const browserDistDirectory = options.isUsingApplicationBuilder ? getApplicationBuilderBrowserOutputPath(outputPath) : outputPath; - const pathToFiles = joinPathFragments(__dirname, '..', 'files'); const { major: angularMajorVersion } = getInstalledAngularVersionInfo(tree); + const baseFilesPath = join(__dirname, '..', 'files'); + let pathToFiles: string; + if (angularMajorVersion >= 19) { + pathToFiles = join( + baseFilesPath, + 'v19+', + options.isUsingApplicationBuilder + ? 'application-builder' + + (options.serverRouting ? '' : '-common-engine') + : 'server-builder', + 'server' + ); + } else { + pathToFiles = join( + baseFilesPath, + 'pre-v19', + 'server', + options.isUsingApplicationBuilder + ? 'application-builder' + : 'server-builder' + ); + } + + const sourceRoot = + project.sourceRoot ?? joinPathFragments(project.root, 'src'); generateFiles( tree, - joinPathFragments( - pathToFiles, - 'server', - isUsingApplicationBuilder ? 'application-builder' : 'server-builder' - ), - projectRoot, + pathToFiles, + angularMajorVersion >= 19 ? sourceRoot : project.root, { - ...schema, - browserBundleOutputPath, - commonEngineEntryPoint: - angularMajorVersion >= 19 ? '@angular/ssr/node' : '@angular/ssr', + ...options, + browserDistDirectory, tpl: '', } ); diff --git a/packages/angular/src/generators/setup-ssr/lib/generate-files.ts b/packages/angular/src/generators/setup-ssr/lib/generate-files.ts index 651d235b029f3..66863a781e109 100644 --- a/packages/angular/src/generators/setup-ssr/lib/generate-files.ts +++ b/packages/angular/src/generators/setup-ssr/lib/generate-files.ts @@ -4,41 +4,51 @@ import { joinPathFragments, readProjectConfiguration, } from '@nx/devkit'; -import type { Schema } from '../schema'; +import { join } from 'path'; +import { getInstalledAngularVersionInfo } from '../../utils/version-utils'; +import type { NormalizedGeneratorOptions } from '../schema'; export function generateSSRFiles( tree: Tree, - schema: Schema, - isUsingApplicationBuilder: boolean + options: NormalizedGeneratorOptions ) { - const { root: projectRoot, targets } = readProjectConfiguration( - tree, - schema.project - ); + const project = readProjectConfiguration(tree, options.project); if ( - targets.server || - (isUsingApplicationBuilder && targets.build.options?.server !== undefined) + project.targets.server || + (options.isUsingApplicationBuilder && + project.targets.build.options?.server !== undefined) ) { // server has already been added return; } - const pathToFiles = joinPathFragments(__dirname, '..', 'files'); - - if (schema.standalone) { - generateFiles( - tree, - joinPathFragments(pathToFiles, 'standalone'), - projectRoot, - { ...schema, tpl: '' } + const { major: angularMajorVersion } = getInstalledAngularVersionInfo(tree); + const baseFilesPath = join(__dirname, '..', 'files'); + let pathToFiles: string; + if (angularMajorVersion >= 19) { + pathToFiles = join( + baseFilesPath, + 'v19+', + options.isUsingApplicationBuilder + ? 'application-builder' + : 'server-builder', + options.standalone ? 'standalone-src' : 'ngmodule-src' ); } else { - generateFiles( - tree, - joinPathFragments(pathToFiles, 'ngmodule'), - projectRoot, - { ...schema, tpl: '' } + pathToFiles = join( + baseFilesPath, + 'pre-v19', + options.standalone ? 'standalone-src' : 'ngmodule-src' ); } + + const sourceRoot = + project.sourceRoot ?? joinPathFragments(project.root, 'src'); + + generateFiles(tree, pathToFiles, sourceRoot, { ...options, tpl: '' }); + + if (angularMajorVersion >= 19 && !options.serverRouting) { + tree.delete(joinPathFragments(sourceRoot, 'app/app.routes.server.ts')); + } } diff --git a/packages/angular/src/generators/setup-ssr/lib/generate-server-ts-config.ts b/packages/angular/src/generators/setup-ssr/lib/generate-server-ts-config.ts index 79caa9819804b..c24085983721c 100644 --- a/packages/angular/src/generators/setup-ssr/lib/generate-server-ts-config.ts +++ b/packages/angular/src/generators/setup-ssr/lib/generate-server-ts-config.ts @@ -1,24 +1,33 @@ import { generateFiles, + joinPathFragments, + offsetFromRoot, + readJson, readProjectConfiguration, updateJson, type Tree, - joinPathFragments, } from '@nx/devkit'; import { join } from 'path'; -import type { Schema } from '../schema'; +import { getInstalledAngularVersionInfo } from '../../utils/version-utils'; +import type { NormalizedGeneratorOptions } from '../schema'; export function setServerTsConfigOptionsForApplicationBuilder( tree: Tree, - options: Schema + options: NormalizedGeneratorOptions ) { const { targets } = readProjectConfiguration(tree, options.project); const tsConfigPath = targets.build.options.tsConfig; updateJson(tree, tsConfigPath, (json) => { + const { major: angularMajorVersion } = getInstalledAngularVersionInfo(tree); + const files = new Set(json.files ?? []); files.add(joinPathFragments('src', options.main)); - files.add(joinPathFragments(options.serverFileName)); + if (angularMajorVersion >= 19) { + files.add(joinPathFragments('src', options.serverFileName)); + } else { + files.add(joinPathFragments(options.serverFileName)); + } json.files = Array.from(files); json.compilerOptions ??= {}; @@ -32,12 +41,29 @@ export function setServerTsConfigOptionsForApplicationBuilder( export function generateTsConfigServerJsonForBrowserBuilder( tree: Tree, - options: Schema + options: NormalizedGeneratorOptions ) { - const { root } = readProjectConfiguration(tree, options.project); + const project = readProjectConfiguration(tree, options.project); + + const { major: angularMajorVersion } = getInstalledAngularVersionInfo(tree); + + const packageJson = readJson(tree, 'package.json'); + const hasLocalizePackage = + !!packageJson.dependencies?.['@angular/localize'] || + !!packageJson.devDependencies?.['@angular/localize']; + + const baseFilesPath = join(__dirname, '..', 'files'); + let pathToFiles: string; + if (angularMajorVersion >= 19) { + pathToFiles = join(baseFilesPath, 'v19+', 'server-builder', 'root'); + } else { + pathToFiles = join(baseFilesPath, 'pre-v19', 'root'); + } - generateFiles(tree, join(__dirname, '..', 'files', 'root'), root, { + generateFiles(tree, pathToFiles, project.root, { ...options, + rootOffset: offsetFromRoot(project.root), + hasLocalizePackage, tpl: '', }); } diff --git a/packages/angular/src/generators/setup-ssr/lib/normalize-options.ts b/packages/angular/src/generators/setup-ssr/lib/normalize-options.ts index 239cf5ab171ea..0bc42bca9c8df 100644 --- a/packages/angular/src/generators/setup-ssr/lib/normalize-options.ts +++ b/packages/angular/src/generators/setup-ssr/lib/normalize-options.ts @@ -1,8 +1,40 @@ -import type { Tree } from '@nx/devkit'; +import { readProjectConfiguration, type Tree } from '@nx/devkit'; +import { promptWhenInteractive } from '@nx/devkit/src/generators/prompt'; import { isNgStandaloneApp } from '../../../utils/nx-devkit/ast-utils'; -import type { Schema } from '../schema'; +import { getInstalledAngularVersionInfo } from '../../utils/version-utils'; +import type { NormalizedGeneratorOptions, Schema } from '../schema'; + +export async function normalizeOptions( + tree: Tree, + options: Schema +): Promise { + const { targets } = readProjectConfiguration(tree, options.project); + const isUsingApplicationBuilder = + targets.build.executor === '@angular-devkit/build-angular:application' || + targets.build.executor === '@angular/build:application' || + targets.build.executor === '@nx/angular:application'; + + if (options.serverRouting !== undefined && isUsingApplicationBuilder) { + const { major: angularMajorVersion } = getInstalledAngularVersionInfo(tree); + + if (angularMajorVersion >= 19) { + options.serverRouting = await promptWhenInteractive<{ + serverRouting: boolean; + }>( + { + type: 'confirm', + name: 'serverRouting', + message: + 'Would you like to use the Server Routing and App Engine APIs (Developer Preview) for this server application?', + initial: false, + }, + { serverRouting: false } + ).then(({ serverRouting }) => serverRouting); + } else { + options.serverRouting = false; + } + } -export function normalizeOptions(tree: Tree, options: Schema) { const isStandaloneApp = isNgStandaloneApp(tree, options.project); return { @@ -16,5 +48,7 @@ export function normalizeOptions(tree: Tree, options: Schema) { skipFormat: options.skipFormat ?? false, standalone: options.standalone ?? isStandaloneApp, hydration: options.hydration ?? true, + serverRouting: options.serverRouting, + isUsingApplicationBuilder, }; } diff --git a/packages/angular/src/generators/setup-ssr/lib/set-router-initial-navigation.spec.ts b/packages/angular/src/generators/setup-ssr/lib/set-router-initial-navigation.spec.ts index 9caeae2644c5c..06eba3c64bfa9 100644 --- a/packages/angular/src/generators/setup-ssr/lib/set-router-initial-navigation.spec.ts +++ b/packages/angular/src/generators/setup-ssr/lib/set-router-initial-navigation.spec.ts @@ -27,6 +27,7 @@ export const appConfig: ApplicationConfig = { setRouterInitialNavigation(tree, { project: 'app1', standalone: true, + isUsingApplicationBuilder: true, }); expect(tree.read('apps/app1/src/app.config.ts', 'utf-8')) @@ -64,6 +65,7 @@ export const appConfig: ApplicationConfig = { setRouterInitialNavigation(tree, { project: 'app1', standalone: true, + isUsingApplicationBuilder: true, }); expect(tree.read('apps/app1/src/app.config.ts', 'utf-8')) @@ -110,6 +112,7 @@ export class AppModule {} setRouterInitialNavigation(tree, { project: 'app1', standalone: false, + isUsingApplicationBuilder: true, }); expect(tree.read('apps/app1/src/app.module.ts', 'utf-8')) diff --git a/packages/angular/src/generators/setup-ssr/lib/set-router-initial-navigation.ts b/packages/angular/src/generators/setup-ssr/lib/set-router-initial-navigation.ts index 1ae55dddfc78d..df1cf24a51a09 100644 --- a/packages/angular/src/generators/setup-ssr/lib/set-router-initial-navigation.ts +++ b/packages/angular/src/generators/setup-ssr/lib/set-router-initial-navigation.ts @@ -21,9 +21,12 @@ import { isIdentifier, isPropertyAssignment, } from 'typescript'; -import type { Schema } from '../schema'; +import type { NormalizedGeneratorOptions } from '../schema'; -export function setRouterInitialNavigation(tree: Tree, options: Schema): void { +export function setRouterInitialNavigation( + tree: Tree, + options: NormalizedGeneratorOptions +): void { const printer = createPrinter(); const project = readProjectConfiguration(tree, options.project); diff --git a/packages/angular/src/generators/setup-ssr/lib/update-project-config.ts b/packages/angular/src/generators/setup-ssr/lib/update-project-config.ts index 10636a0ca9557..793c654fd0997 100644 --- a/packages/angular/src/generators/setup-ssr/lib/update-project-config.ts +++ b/packages/angular/src/generators/setup-ssr/lib/update-project-config.ts @@ -12,7 +12,7 @@ import { updateProjectConfiguration, } from '@nx/devkit'; import { getInstalledAngularVersionInfo } from '../../utils/version-utils'; -import type { Schema } from '../schema'; +import type { NormalizedGeneratorOptions } from '../schema'; import { DEFAULT_BROWSER_DIR, DEFAULT_MEDIA_DIR, @@ -21,7 +21,7 @@ import { export function updateProjectConfigForApplicationBuilder( tree: Tree, - options: Schema + options: NormalizedGeneratorOptions ): void { const project = readProjectConfiguration(tree, options.project); const buildTarget = project.targets.build; @@ -51,25 +51,38 @@ export function updateProjectConfigForApplicationBuilder( } } + const { major: angularMajorVersion } = getInstalledAngularVersionInfo(tree); + const sourceRoot = + project.sourceRoot ?? joinPathFragments(project.root, 'src'); + buildTarget.options ??= {}; buildTarget.options.outputPath = outputPath; - buildTarget.options.server = joinPathFragments( - project.sourceRoot ?? joinPathFragments(project.root, 'src'), - options.main - ); - buildTarget.options.prerender = true; - buildTarget.options.ssr = { - entry: joinPathFragments(project.root, options.serverFileName), - }; + buildTarget.options.server = joinPathFragments(sourceRoot, options.main); + + if (angularMajorVersion >= 19) { + buildTarget.options.ssr = { + entry: joinPathFragments(sourceRoot, options.serverFileName), + }; + if (options.serverRouting) { + buildTarget.options.outputMode = 'server'; + } else { + buildTarget.options.prerender = true; + } + } else { + buildTarget.options.prerender = true; + buildTarget.options.ssr = { + entry: joinPathFragments(project.root, options.serverFileName), + }; + } updateProjectConfiguration(tree, options.project, project); } export function updateProjectConfigForBrowserBuilder( tree: Tree, - schema: Schema + options: NormalizedGeneratorOptions ) { - const projectConfig = readProjectConfiguration(tree, schema.project); + const projectConfig = readProjectConfiguration(tree, options.project); const buildTarget = projectConfig.targets.build; const baseOutputPath = buildTarget.options.outputPath; buildTarget.options.outputPath = joinPathFragments(baseOutputPath, 'browser'); @@ -82,6 +95,10 @@ export function updateProjectConfigForBrowserBuilder( } } + const { major: angularMajorVersion } = getInstalledAngularVersionInfo(tree); + const sourceRoot = + projectConfig.sourceRoot ?? joinPathFragments(projectConfig.root, 'src'); + projectConfig.targets.server = { dependsOn: ['build'], executor: buildTarget.executor.startsWith('@angular-devkit/build-angular:') @@ -89,7 +106,10 @@ export function updateProjectConfigForBrowserBuilder( : '@nx/angular:webpack-server', options: { outputPath: joinPathFragments(baseOutputPath, 'server'), - main: joinPathFragments(projectConfig.root, schema.serverFileName), + main: joinPathFragments( + angularMajorVersion >= 19 ? sourceRoot : projectConfig.root, + options.serverFileName + ), tsConfig: joinPathFragments(projectConfig.root, 'tsconfig.server.json'), ...(buildTarget.options ? getServerOptions(buildTarget.options) : {}), }, @@ -97,18 +117,16 @@ export function updateProjectConfigForBrowserBuilder( defaultConfiguration: 'production', }; - const { major: angularMajorVersion } = getInstalledAngularVersionInfo(tree); - projectConfig.targets['serve-ssr'] = { executor: '@angular-devkit/build-angular:ssr-dev-server', configurations: { development: { - browserTarget: `${schema.project}:build:development`, - serverTarget: `${schema.project}:server:development`, + browserTarget: `${options.project}:build:development`, + serverTarget: `${options.project}:server:development`, }, production: { - browserTarget: `${schema.project}:build:production`, - serverTarget: `${schema.project}:server:production`, + browserTarget: `${options.project}:build:production`, + serverTarget: `${options.project}:server:production`, }, }, defaultConfiguration: 'development', @@ -121,18 +139,18 @@ export function updateProjectConfigForBrowserBuilder( }, configurations: { development: { - browserTarget: `${schema.project}:build:development`, - serverTarget: `${schema.project}:server:development`, + browserTarget: `${options.project}:build:development`, + serverTarget: `${options.project}:server:development`, }, production: { - browserTarget: `${schema.project}:build:production`, - serverTarget: `${schema.project}:server:production`, + browserTarget: `${options.project}:build:production`, + serverTarget: `${options.project}:server:production`, }, }, defaultConfiguration: 'production', }; - updateProjectConfiguration(tree, schema.project, projectConfig); + updateProjectConfiguration(tree, options.project, projectConfig); const nxJson = readNxJson(tree); if ( diff --git a/packages/angular/src/generators/setup-ssr/lib/validate-options.ts b/packages/angular/src/generators/setup-ssr/lib/validate-options.ts index 23b1b0d3ed167..a9eae4de0d594 100644 --- a/packages/angular/src/generators/setup-ssr/lib/validate-options.ts +++ b/packages/angular/src/generators/setup-ssr/lib/validate-options.ts @@ -1,11 +1,20 @@ import type { Tree } from '@nx/devkit'; import { readProjectConfiguration } from '@nx/devkit'; import { validateProject as validateExistingProject } from '../../utils/validations'; +import { getInstalledAngularVersionInfo } from '../../utils/version-utils'; import type { Schema } from '../schema'; export function validateOptions(tree: Tree, options: Schema): void { validateProject(tree, options.project); validateBuildTarget(tree, options.project); + + const { major: angularMajorVersion, version: angularVersion } = + getInstalledAngularVersionInfo(tree); + if (angularMajorVersion < 19 && options.serverRouting) { + throw new Error( + `The "serverRouting" option is only supported in Angular versions >= 19.0.0. You are using Angular ${angularVersion}.` + ); + } } function validateProject(tree: Tree, project: string): void { diff --git a/packages/angular/src/generators/setup-ssr/schema.d.ts b/packages/angular/src/generators/setup-ssr/schema.d.ts index 5b16df69d5080..8c08243b57267 100644 --- a/packages/angular/src/generators/setup-ssr/schema.d.ts +++ b/packages/angular/src/generators/setup-ssr/schema.d.ts @@ -8,6 +8,11 @@ export interface Schema { rootModuleClassName?: string; standalone?: boolean; hydration?: boolean; + serverRouting?: boolean; skipFormat?: boolean; skipPackageJson?: boolean; } + +export interface NormalizedGeneratorOptions extends Schema { + isUsingApplicationBuilder: boolean; +} diff --git a/packages/angular/src/generators/setup-ssr/schema.json b/packages/angular/src/generators/setup-ssr/schema.json index 964721bef0fa7..a849f3d2821bd 100644 --- a/packages/angular/src/generators/setup-ssr/schema.json +++ b/packages/angular/src/generators/setup-ssr/schema.json @@ -52,6 +52,11 @@ "description": "Set up Hydration for the SSR application.", "default": true }, + "serverRouting": { + "description": "Creates a server application using the Server Routing and App Engine APIs (Developer Preview). _Note: this is only supported in Angular versions >= 19.0.0_.", + "type": "boolean", + "default": false + }, "skipFormat": { "type": "boolean", "description": "Skip formatting the workspace after the generator completes.", diff --git a/packages/angular/src/generators/setup-ssr/setup-ssr.spec.ts b/packages/angular/src/generators/setup-ssr/setup-ssr.spec.ts index d529f3fb263b1..b5653ce9880e9 100644 --- a/packages/angular/src/generators/setup-ssr/setup-ssr.spec.ts +++ b/packages/angular/src/generators/setup-ssr/setup-ssr.spec.ts @@ -37,7 +37,7 @@ describe('setupSSR', () => { expect( readProjectConfiguration(tree, 'app1').targets.build ).toMatchSnapshot(); - expect(tree.read('app1/server.ts', 'utf-8')).toMatchSnapshot(); + expect(tree.read('app1/src/server.ts', 'utf-8')).toMatchSnapshot(); expect(tree.read('app1/src/main.server.ts', 'utf-8')) .toMatchInlineSnapshot(` "export { AppServerModule as default } from './app/app.server.module'; @@ -58,15 +58,14 @@ describe('setupSSR', () => { expect(readJson(tree, 'app1/tsconfig.app.json').files).toStrictEqual([ 'src/main.ts', 'src/main.server.ts', - 'server.ts', + 'src/server.ts', ]); expect(tree.read('app1/src/app/app.server.module.ts', 'utf-8')) .toMatchInlineSnapshot(` "import { NgModule } from '@angular/core'; import { ServerModule } from '@angular/platform-server'; - - import { AppModule } from './app.module'; import { AppComponent } from './app.component'; + import { AppModule } from './app.module'; @NgModule({ imports: [AppModule, ServerModule], @@ -116,7 +115,7 @@ describe('setupSSR', () => { expect( readProjectConfiguration(tree, 'app1').targets.build ).toMatchSnapshot(); - expect(tree.read('app1/server.ts', 'utf-8')).toMatchSnapshot(); + expect(tree.read('app1/src/server.ts', 'utf-8')).toMatchSnapshot(); expect(tree.read('app1/src/main.server.ts', 'utf-8')) .toMatchInlineSnapshot(` "import { bootstrapApplication } from '@angular/platform-browser'; @@ -132,7 +131,7 @@ describe('setupSSR', () => { expect(readJson(tree, 'app1/tsconfig.app.json').files).toStrictEqual([ 'src/main.ts', 'src/main.server.ts', - 'server.ts', + 'src/server.ts', ]); expect(tree.read('app1/src/app/app.config.server.ts', 'utf-8')) .toMatchInlineSnapshot(` @@ -167,7 +166,7 @@ describe('setupSSR', () => { await setupSsr(tree, { project: 'app1' }); - const serverFileContent = tree.read('app1/server.ts', 'utf-8'); + const serverFileContent = tree.read('app1/src/server.ts', 'utf-8'); expect(serverFileContent).toContain( `resolve(serverDistFolder, '../public')` ); @@ -215,7 +214,7 @@ describe('setupSSR', () => { expect( readProjectConfiguration(tree, 'app1').targets.server ).toMatchSnapshot(); - expect(tree.read('app1/server.ts', 'utf-8')).toMatchSnapshot(); + expect(tree.read('app1/src/server.ts', 'utf-8')).toMatchSnapshot(); expect(tree.read('app1/src/main.server.ts', 'utf-8')) .toMatchInlineSnapshot(` "export { AppServerModule as default } from './app/app.server.module'; @@ -234,21 +233,22 @@ describe('setupSSR', () => { `); expect(tree.read('app1/tsconfig.server.json', 'utf-8')) .toMatchInlineSnapshot(` - "/* To learn more about this file see: https://angular.io/config/tsconfig. */ + "/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ + /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ { "extends": "./tsconfig.app.json", "compilerOptions": { - "outDir": "../../out-tsc/server", - "target": "es2019", + "outDir": "../out-tsc/server", "types": [ "node" ] }, "files": [ "src/main.server.ts", - "server.ts", + "src/server.ts" ] - }" + } + " `); expect(tree.read('app1/src/app/app.server.module.ts', 'utf-8')) .toMatchInlineSnapshot(` @@ -265,7 +265,8 @@ describe('setupSSR', () => { ], bootstrap: [AppComponent], }) - export class AppServerModule {}" + export class AppServerModule {} + " `); expect(tree.read('app1/src/app/app.module.ts', 'utf-8')) .toMatchInlineSnapshot(` @@ -308,7 +309,7 @@ describe('setupSSR', () => { expect( readProjectConfiguration(tree, 'app1').targets.server ).toMatchSnapshot(); - expect(tree.read('app1/server.ts', 'utf-8')).toMatchSnapshot(); + expect(tree.read('app1/src/server.ts', 'utf-8')).toMatchSnapshot(); expect(tree.read('app1/src/main.server.ts', 'utf-8')) .toMatchInlineSnapshot(` "import { bootstrapApplication } from '@angular/platform-browser'; @@ -322,21 +323,22 @@ describe('setupSSR', () => { `); expect(tree.read('app1/tsconfig.server.json', 'utf-8')) .toMatchInlineSnapshot(` - "/* To learn more about this file see: https://angular.io/config/tsconfig. */ + "/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ + /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ { "extends": "./tsconfig.app.json", "compilerOptions": { - "outDir": "../../out-tsc/server", - "target": "es2019", + "outDir": "../out-tsc/server", "types": [ "node" ] }, "files": [ "src/main.server.ts", - "server.ts", + "src/server.ts" ] - }" + } + " `); expect(tree.read('app1/src/app/app.config.server.ts', 'utf-8')) .toMatchInlineSnapshot(` diff --git a/packages/angular/src/generators/setup-ssr/setup-ssr.ts b/packages/angular/src/generators/setup-ssr/setup-ssr.ts index 2e12ab626100f..6791831a67e57 100644 --- a/packages/angular/src/generators/setup-ssr/setup-ssr.ts +++ b/packages/angular/src/generators/setup-ssr/setup-ssr.ts @@ -21,17 +21,12 @@ import type { Schema } from './schema'; export async function setupSsr(tree: Tree, schema: Schema) { validateOptions(tree, schema); - const options = normalizeOptions(tree, schema); - - const { targets } = readProjectConfiguration(tree, options.project); - const isUsingApplicationBuilder = - targets.build.executor === '@angular-devkit/build-angular:application' || - targets.build.executor === '@nx/angular:application'; + const options = await normalizeOptions(tree, schema); if (!schema.skipPackageJson) { - addDependencies(tree, isUsingApplicationBuilder); + addDependencies(tree, options.isUsingApplicationBuilder); } - generateSSRFiles(tree, options, isUsingApplicationBuilder); + generateSSRFiles(tree, options); if (options.hydration) { addHydration(tree, options); @@ -41,7 +36,7 @@ export async function setupSsr(tree: Tree, schema: Schema) { setRouterInitialNavigation(tree, options); } - if (isUsingApplicationBuilder) { + if (options.isUsingApplicationBuilder) { updateProjectConfigForApplicationBuilder(tree, options); setServerTsConfigOptionsForApplicationBuilder(tree, options); } else { @@ -49,7 +44,7 @@ export async function setupSsr(tree: Tree, schema: Schema) { generateTsConfigServerJsonForBrowserBuilder(tree, options); } - addServerFile(tree, options, isUsingApplicationBuilder); + addServerFile(tree, options); if (!options.skipFormat) { await formatFiles(tree); diff --git a/packages/create-nx-workspace/bin/create-nx-workspace.ts b/packages/create-nx-workspace/bin/create-nx-workspace.ts index 8a25b98a627af..c1b5060039113 100644 --- a/packages/create-nx-workspace/bin/create-nx-workspace.ts +++ b/packages/create-nx-workspace/bin/create-nx-workspace.ts @@ -64,6 +64,7 @@ interface AngularArguments extends BaseArguments { e2eTestRunner: 'none' | 'cypress' | 'playwright'; bundler: 'webpack' | 'esbuild'; ssr: boolean; + serverRouting: boolean; prefix: string; } @@ -755,6 +756,7 @@ async function determineAngularOptions( let e2eTestRunner: undefined | 'none' | 'cypress' | 'playwright' = undefined; let bundler: undefined | 'webpack' | 'esbuild' = undefined; let ssr: undefined | boolean = undefined; + let serverRouting: undefined | boolean = undefined; const standaloneApi = parsedArgs.standaloneApi; const routing = parsedArgs.routing; @@ -869,6 +871,25 @@ async function determineAngularOptions( ssr = reply.ssr === 'Yes'; } + if (parsedArgs.serverRouting !== undefined) { + serverRouting = parsedArgs.serverRouting; + } else if (ssr && bundler === 'esbuild') { + const reply = await enquirer.prompt<{ serverRouting: 'Yes' | 'No' }>([ + { + name: 'serverRouting', + message: + 'Would you like to use the Server Routing and App Engine APIs (Developer Preview) for this server application?', + type: 'autocomplete', + choices: [{ name: 'Yes' }, { name: 'No' }], + initial: 1, + skip: !parsedArgs.interactive || isCI(), + }, + ]); + serverRouting = reply.serverRouting === 'Yes'; + } else { + serverRouting = false; + } + e2eTestRunner = await determineE2eTestRunner(parsedArgs); return { @@ -880,6 +901,7 @@ async function determineAngularOptions( e2eTestRunner, bundler, ssr, + serverRouting, prefix, }; } diff --git a/packages/workspace/src/generators/new/generate-preset.ts b/packages/workspace/src/generators/new/generate-preset.ts index dba0c37028ac1..1105b6a1f6278 100644 --- a/packages/workspace/src/generators/new/generate-preset.ts +++ b/packages/workspace/src/generators/new/generate-preset.ts @@ -82,6 +82,7 @@ export function generatePreset(host: Tree, opts: NormalizedSchema) { ? `--e2eTestRunner=${opts.e2eTestRunner}` : null, opts.ssr ? `--ssr` : null, + opts.serverRouting ? `--server-routing` : null, opts.prefix !== undefined ? `--prefix=${opts.prefix}` : null, opts.nxCloudToken ? `--nxCloudToken=${opts.nxCloudToken}` : null, opts.formatter ? `--formatter=${opts.formatter}` : null, diff --git a/packages/workspace/src/generators/new/new.ts b/packages/workspace/src/generators/new/new.ts index b0ce463c6df18..1be7a3b79745a 100644 --- a/packages/workspace/src/generators/new/new.ts +++ b/packages/workspace/src/generators/new/new.ts @@ -34,6 +34,7 @@ interface Schema { packageManager?: PackageManager; e2eTestRunner?: 'cypress' | 'playwright' | 'detox' | 'jest' | 'none'; ssr?: boolean; + serverRouting?: boolean; prefix?: string; useGitHub?: boolean; nxCloud?: 'yes' | 'skip' | 'circleci' | 'github'; diff --git a/packages/workspace/src/generators/new/schema.json b/packages/workspace/src/generators/new/schema.json index 4f895ab3a8fda..14b8f66a900c3 100644 --- a/packages/workspace/src/generators/new/schema.json +++ b/packages/workspace/src/generators/new/schema.json @@ -83,6 +83,11 @@ "type": "boolean", "default": false }, + "serverRouting": { + "description": "Use the Angular Server Routing and App Engine APIs (Developer Preview).", + "type": "boolean", + "default": false + }, "prefix": { "description": "The prefix to use for Angular component and directive selectors.", "type": "string" diff --git a/packages/workspace/src/generators/preset/preset.ts b/packages/workspace/src/generators/preset/preset.ts index ad115d754021b..fcf4666160c92 100644 --- a/packages/workspace/src/generators/preset/preset.ts +++ b/packages/workspace/src/generators/preset/preset.ts @@ -36,6 +36,7 @@ async function createPreset(tree: Tree, options: Schema) { e2eTestRunner: options.e2eTestRunner ?? 'playwright', bundler: options.bundler, ssr: options.ssr, + serverRouting: options.serverRouting, prefix: options.prefix, nxCloudToken: options.nxCloudToken, }); @@ -55,6 +56,7 @@ async function createPreset(tree: Tree, options: Schema) { e2eTestRunner: options.e2eTestRunner ?? 'playwright', bundler: options.bundler, ssr: options.ssr, + serverRouting: options.serverRouting, prefix: options.prefix, nxCloudToken: options.nxCloudToken, }); diff --git a/packages/workspace/src/generators/preset/schema.d.ts b/packages/workspace/src/generators/preset/schema.d.ts index 8c9d19f54c1ed..f546a64f76cd8 100644 --- a/packages/workspace/src/generators/preset/schema.d.ts +++ b/packages/workspace/src/generators/preset/schema.d.ts @@ -19,6 +19,7 @@ export interface Schema { e2eTestRunner?: 'cypress' | 'playwright' | 'jest' | 'detox' | 'none'; js?: boolean; ssr?: boolean; + serverRouting?: boolean; prefix?: string; nxCloudToken?: string; } diff --git a/packages/workspace/src/generators/preset/schema.json b/packages/workspace/src/generators/preset/schema.json index 891e099699022..4ba9bd37ed516 100644 --- a/packages/workspace/src/generators/preset/schema.json +++ b/packages/workspace/src/generators/preset/schema.json @@ -100,6 +100,11 @@ "type": "boolean", "default": false }, + "serverRouting": { + "description": "Use the Angular Server Routing and App Engine APIs (Developer Preview).", + "type": "boolean", + "default": false + }, "prefix": { "description": "The prefix to use for Angular component and directive selectors.", "type": "string"