diff --git a/templates/app-nolayers/angular/angular.json b/templates/app-nolayers/angular/angular.json index bfdb10c374..36eaa6baf6 100644 --- a/templates/app-nolayers/angular/angular.json +++ b/templates/app-nolayers/angular/angular.json @@ -21,7 +21,7 @@ "build": { "builder": "@angular-devkit/build-angular:browser", "options": { - "outputPath": "dist/MyProjectName", + "outputPath": "dist/MyProjectName/browser", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", @@ -183,6 +183,67 @@ "options": { "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] } + }, + "server": { + "builder": "@angular-devkit/build-angular:server", + "options": { + "outputPath": "dist/MyProjectName/server", + "main": "src/server.ts", + "tsConfig": "tsconfig.server.json", + "inlineStyleLanguage": "scss" + }, + "configurations": { + "production": { + "outputHashing": "media", + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "sourceMap": true, + "extractLicenses": false, + "vendorChunk": true + } + }, + "defaultConfiguration": "production" + }, + "serve-ssr": { + "builder": "@angular-devkit/build-angular:ssr-dev-server", + "configurations": { + "development": { + "browserTarget": "MyProjectName:build:development", + "serverTarget": "MyProjectName:server:development" + }, + "production": { + "browserTarget": "MyProjectName:build:production", + "serverTarget": "MyProjectName:server:production" + } + }, + "defaultConfiguration": "development" + }, + "prerender": { + "builder": "@angular-devkit/build-angular:prerender", + "options": { + "routes": [ + "/" + ] + }, + "configurations": { + "production": { + "browserTarget": "MyProjectName:build:production", + "serverTarget": "MyProjectName:server:production" + }, + "development": { + "browserTarget": "MyProjectName:build:development", + "serverTarget": "MyProjectName:server:development" + } + }, + "defaultConfiguration": "production" } } } diff --git a/templates/app-nolayers/angular/package.json b/templates/app-nolayers/angular/package.json index dad6781521..2c19a2920a 100644 --- a/templates/app-nolayers/angular/package.json +++ b/templates/app-nolayers/angular/package.json @@ -8,7 +8,11 @@ "build:prod": "ng build --configuration production", "watch": "ng build --watch --configuration development", "test": "ng test", - "lint": "ng lint" + "lint": "ng lint", + "dev:ssr": "ng run MyProjectName:serve-ssr", + "serve:ssr": "node dist/MyProjectName/server/main.js", + "build:ssr": "ng build && ng run MyProjectName:server", + "prerender": "ng run MyProjectName:prerender" }, "private": true, "dependencies": { @@ -29,8 +33,11 @@ "@angular/localize": "~20.0.0", "@angular/platform-browser": "~20.0.0", "@angular/platform-browser-dynamic": "~20.0.0", + "@angular/platform-server": "~20.0.0", "@angular/router": "~20.0.0", + "@angular/ssr": "^20.0.5", "bootstrap-icons": "~1.8.0", + "express": "^5.1.0", "rxjs": "~7.8.0", "tslib": "^2.0.0", "zone.js": "~0.15.0" @@ -46,10 +53,12 @@ "@angular/cli": "~20.0.0", "@angular/compiler-cli": "~20.0.0", "@angular/language-service": "~20.0.0", + "@types/express": "^5.0.1", "@types/jasmine": "~3.6.0", - "@types/node": "^12.11.1", + "@types/node": "^20.17.19", "@typescript-eslint/eslint-plugin": "7.16.0", "@typescript-eslint/parser": "7.16.0", + "browser-sync": "^3.0.0", "eslint": "^8.0.0", "jasmine-core": "~4.0.0", "karma": "~6.3.0", @@ -59,4 +68,4 @@ "karma-jasmine-html-reporter": "^1.7.0", "typescript": "~5.8.0" } -} +} \ No newline at end of file diff --git a/templates/app-nolayers/angular/src/app/app.config.server.ts b/templates/app-nolayers/angular/src/app/app.config.server.ts new file mode 100644 index 0000000000..05c29319d5 --- /dev/null +++ b/templates/app-nolayers/angular/src/app/app.config.server.ts @@ -0,0 +1,11 @@ +import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; +import { provideServerRendering } from '@angular/ssr'; +import { appConfig } from './app.config'; + +const serverConfig: ApplicationConfig = { + providers: [ + provideServerRendering() + ] +}; + +export const config = mergeApplicationConfig(appConfig, serverConfig); diff --git a/templates/app-nolayers/angular/src/app/app.config.ts b/templates/app-nolayers/angular/src/app/app.config.ts index 450a31aab8..041567383e 100644 --- a/templates/app-nolayers/angular/src/app/app.config.ts +++ b/templates/app-nolayers/angular/src/app/app.config.ts @@ -16,6 +16,7 @@ import { provideFeatureManagementConfig } from '@abp/ng.feature-management'; import { ThemeLeptonXModule } from '@abp/ng.theme.lepton-x'; import { SideMenuLayoutModule } from '@abp/ng.theme.lepton-x/layouts'; import { provideLogo, withEnvironmentOptions } from '@volo/ngx-lepton-x.core'; +import { provideClientHydration, withEventReplay } from '@angular/platform-browser'; export const appConfig: ApplicationConfig = { providers: [ @@ -36,6 +37,6 @@ export const appConfig: ApplicationConfig = { provideFeatureManagementConfig(), provideAnimations(), provideLogo(withEnvironmentOptions(environment)), - importProvidersFrom([ThemeLeptonXModule.forRoot(), SideMenuLayoutModule.forRoot()]), + importProvidersFrom([ThemeLeptonXModule.forRoot(), SideMenuLayoutModule.forRoot()]), provideClientHydration(withEventReplay()), ], }; diff --git a/templates/app-nolayers/angular/src/main.server.ts b/templates/app-nolayers/angular/src/main.server.ts new file mode 100644 index 0000000000..4b9d4d1545 --- /dev/null +++ b/templates/app-nolayers/angular/src/main.server.ts @@ -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/templates/app-nolayers/angular/src/server.ts b/templates/app-nolayers/angular/src/server.ts new file mode 100644 index 0000000000..26298e12df --- /dev/null +++ b/templates/app-nolayers/angular/src/server.ts @@ -0,0 +1,74 @@ +import 'zone.js/node'; + +import { APP_BASE_HREF } from '@angular/common'; +import { CommonEngine } from '@angular/ssr/node'; +import express from 'express'; +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import bootstrap from './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 distFolder = join(process.cwd(), 'dist/MyProjectName/browser'); + 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/{*splat}', (req, res) => { }); + // Serve static files from /browser + server.use(express.static(distFolder, { + maxAge: '1y', + index: false, + })); + + // All regular routes use the Angular engine + server.use((req, res, next) => { + const { protocol, originalUrl, baseUrl, headers } = req; + + commonEngine + .render({ + bootstrap, + 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'] || 4000; + + // Start up the Node server + const server = app(); + server.listen(port, (error) => { + if (error) { + throw error; + } + + 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 bootstrap; diff --git a/templates/app-nolayers/angular/tsconfig.server.json b/templates/app-nolayers/angular/tsconfig.server.json new file mode 100644 index 0000000000..1166d1a0aa --- /dev/null +++ b/templates/app-nolayers/angular/tsconfig.server.json @@ -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": "./out-tsc/server", + "types": [ + "node", + "@angular/localize" + ] + }, + "files": [ + "src/main.server.ts", + "src/server.ts" + ] +} diff --git a/templates/app/angular/package.json b/templates/app/angular/package.json index cfd77f85c2..3adf87748e 100644 --- a/templates/app/angular/package.json +++ b/templates/app/angular/package.json @@ -33,11 +33,11 @@ "@angular/localize": "~20.0.0", "@angular/platform-browser": "~20.0.0", "@angular/platform-browser-dynamic": "~20.0.0", - "@angular/router": "~20.0.0", "@angular/platform-server": "~20.0.0", - "@angular/ssr": "~20.0.0", + "@angular/router": "~20.0.0", + "@angular/ssr": "20.0.5", "bootstrap-icons": "~1.8.0", - "express": "~4.18.0", + "express": "^5.1.0", "rxjs": "~7.8.0", "tslib": "^2.0.0", "zone.js": "~0.15.0" @@ -53,7 +53,7 @@ "@angular/cli": "~20.0.0", "@angular/compiler-cli": "~20.0.0", "@angular/language-service": "~20.0.0", - "@types/express": "~4.17.0", + "@types/express": "^5.0.1", "@types/jasmine": "~3.6.0", "@types/node": "~18.18.0", "@typescript-eslint/eslint-plugin": "7.16.0", @@ -68,4 +68,4 @@ "karma-jasmine-html-reporter": "^1.7.0", "typescript": "~5.8.0" } -} +} \ No newline at end of file diff --git a/templates/app/angular/src/server.ts b/templates/app/angular/src/server.ts index 3051e954ae..26298e12df 100644 --- a/templates/app/angular/src/server.ts +++ b/templates/app/angular/src/server.ts @@ -2,7 +2,7 @@ import 'zone.js/node'; import { APP_BASE_HREF } from '@angular/common'; import { CommonEngine } from '@angular/ssr/node'; -import * as express from 'express'; +import express from 'express'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; import bootstrap from './main.server'; @@ -21,14 +21,15 @@ export function app(): express.Express { server.set('views', distFolder); // Example Express Rest API endpoints - // server.get('/api/**', (req, res) => { }); + // server.get('/api/{*splat}', (req, res) => { }); // Serve static files from /browser - server.get('*.*', express.static(distFolder, { - maxAge: '1y' + server.use(express.static(distFolder, { + maxAge: '1y', + index: false, })); // All regular routes use the Angular engine - server.get('*', (req, res, next) => { + server.use((req, res, next) => { const { protocol, originalUrl, baseUrl, headers } = req; commonEngine @@ -51,7 +52,11 @@ function run(): void { // Start up the Node server const server = app(); - server.listen(port, () => { + server.listen(port, (error) => { + if (error) { + throw error; + } + console.log(`Node Express server listening on http://localhost:${port}`); }); } diff --git a/templates/app/angular/src/server_legacy.ts b/templates/app/angular/src/server_legacy.ts new file mode 100644 index 0000000000..3051e954ae --- /dev/null +++ b/templates/app/angular/src/server_legacy.ts @@ -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 bootstrap from './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 distFolder = join(process.cwd(), 'dist/MyProjectName/browser'); + 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, + 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'] || 4000; + + // 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 bootstrap; diff --git a/templates/app/angular/tsconfig.server.json b/templates/app/angular/tsconfig.server.json index 1166d1a0aa..48bcde8ca3 100644 --- a/templates/app/angular/tsconfig.server.json +++ b/templates/app/angular/tsconfig.server.json @@ -11,6 +11,7 @@ }, "files": [ "src/main.server.ts", + "src/server.ts", "src/server.ts" ] } diff --git a/templates/module/angular/package.json b/templates/module/angular/package.json index 90cd54f25c..9c8d6b5663 100644 --- a/templates/module/angular/package.json +++ b/templates/module/angular/package.json @@ -31,6 +31,7 @@ "@angular/platform-browser": "~20.0.0", "@angular/platform-browser-dynamic": "~20.0.0", "@angular/router": "~20.0.0", + "@angular/ssr": "20.0.5", "rxjs": "~7.8.0", "tslib": "^2.0.0", "zone.js": "~0.15.0"