├── .gitignore ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── server └── main.ts ├── src ├── @types │ └── global.d.ts ├── app │ ├── about │ │ ├── about.component.css │ │ ├── about.component.html │ │ ├── about.component.spec.ts │ │ └── about.component.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.config.browser.ts │ ├── app.config.server.ts │ ├── app.config.ts │ ├── app.routes.ts │ ├── home │ │ ├── ServerService │ │ │ ├── TransferState.ts │ │ │ ├── example.service.browser.ts │ │ │ ├── example.service.server.ts │ │ │ └── index.ts │ │ ├── home.component.css │ │ ├── home.component.html │ │ ├── home.component.spec.ts │ │ └── home.component.ts │ └── todos │ │ ├── todos.component.css │ │ ├── todos.component.html │ │ ├── todos.component.spec.ts │ │ ├── todos.component.ts │ │ └── todos.service.ts ├── assets │ └── .gitkeep ├── bootstrap.browser.ts ├── bootstrap.server.ts ├── favicon.ico ├── index.html ├── main.ts └── styles.css ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.server.json ├── tsconfig.spec.json └── webpack.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | .angular 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Server Services 🍑 2 | 3 | This project presents an example of Angular Server Services implementation, utilizing proxying for client services to make RPC (Remote Procedure Calls) to the server service. The goal of this example is to demonstrate how easily these services can be auto-generated, potentially reducing half of your existing codebase. 4 | 5 | Whats used in the repo: 6 | * Angular 16 7 | * Standalone Components 8 | * Universal 9 | * Hydration 10 | * NgExpressEngine 11 | * Custom Webpack 12 | 13 | > Please note, we are currently using injection-js to bypass the Angular injector and micotask to force zone.js to wait on the server-side. This workarounds are only needed to workaround how Angular bootstraps and manages the apps on the server. I am also creating my own TransferState service just for this demo. 14 | 15 | ## Why 16 | 17 | The goal is to replicate GraphQL or RSC by moving all domain logic that lives in these services to the server. We want to do this so we can utilize caching on the server and remove all client code for these services (usually half your codebase) 18 | 19 | Angular can easily support this pattern in Angular Universal with little effort. 20 | 21 | ## Start 22 | 23 | ```bash 24 | $ npm install 25 | $ npm run dev:ssr 26 | ``` 27 | go to [localhost ](http://localhost:4200/) 28 | 29 | Screenshot 2023-05-30 at 7 32 35 PM 30 | 31 | 32 | Initial load uses transfer state. When you navigate to another page the back to Home we will make an RPC to get the updated state 33 | 34 | * [/app/home/ServerService/example.service.server.ts](https://github.com/PatrickJS/angular-server-services/blob/main/src/app/home/ServerService/example.service.server.ts): ServerService example 35 | * [/server/main.ts](https://github.com/PatrickJS/angular-server-services/blob/e5deec3011d17c1f7301b848eb3f88d268ea8454/server/main.ts#L36...L45): server RPC endpoint 36 | * [/app/app.config.browser](https://github.com/PatrickJS/angular-server-services/blob/main/src/app/app.config.browser.ts#L10...L38): client RPC requests 37 | 38 | If we had Angular support then the api would look like this (a lot less code) 39 | * [branch for ideal api](https://github.com/PatrickJS/angular-server-services/tree/ideal-api) 40 | * [ExampleService](https://github.com/PatrickJS/angular-server-services/blob/ideal-api/src/%40server/Example.service.ts) 41 | * [HomeComponent](https://github.com/PatrickJS/angular-server-services/blob/ideal-api/src/app/home/home.component.ts#L4) 42 | 43 | Production ready version 44 | * WIP https://github.com/PatrickJS/angular-server-services/pull/2 45 | * Preview 46 | 47 | https://github.com/PatrickJS/angular-server-services/assets/1016365/8b00d775-42c4-4d29-b79a-815906d35d04 48 | 49 | 50 | # TODO: production ready version 51 | - [x] use webpack to auto-generate ServerServices 52 | - [x] create @server folder in src that will be all server services and components 53 | - [x] use angular TransferState 54 | - [x] batch client requests 55 | - [x] batch server requests 56 | - [ ] server commponents 57 | - [ ] hook into router to batch requests for server components 58 | - [ ] mixed server in client components and vice versa 59 | - [ ] server and client caching 60 | - [ ] UI over http 61 | 62 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-server-services": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "standalone": true 11 | }, 12 | "@schematics/angular:directive": { 13 | "standalone": true 14 | }, 15 | "@schematics/angular:pipe": { 16 | "standalone": true 17 | } 18 | }, 19 | "root": "", 20 | "sourceRoot": "src", 21 | "prefix": "app", 22 | "architect": { 23 | "build": { 24 | "builder": "@angular-builders/custom-webpack:browser", 25 | "options": { 26 | "optimization": false, 27 | "customWebpackConfig": { 28 | "path": "./webpack.config.ts" 29 | }, 30 | "outputPath": "dist/angular-server-services/browser", 31 | "index": "src/index.html", 32 | "main": "src/main.ts", 33 | "polyfills": [ 34 | "zone.js" 35 | ], 36 | "tsConfig": "tsconfig.app.json", 37 | "assets": [ 38 | "src/favicon.ico", 39 | "src/assets" 40 | ], 41 | "styles": [ 42 | "src/styles.css" 43 | ], 44 | "scripts": [] 45 | }, 46 | "configurations": { 47 | "production": { 48 | "budgets": [ 49 | { 50 | "type": "initial", 51 | "maximumWarning": "500kb", 52 | "maximumError": "5mb" 53 | }, 54 | { 55 | "type": "anyComponentStyle", 56 | "maximumWarning": "2kb", 57 | "maximumError": "4kb" 58 | } 59 | ], 60 | "outputHashing": "all" 61 | }, 62 | "development": { 63 | "buildOptimizer": false, 64 | "optimization": false, 65 | "vendorChunk": true, 66 | "extractLicenses": false, 67 | "sourceMap": true, 68 | "namedChunks": true 69 | } 70 | }, 71 | "defaultConfiguration": "production" 72 | }, 73 | "serve": { 74 | "builder": "@angular-builders/custom-webpack:dev-server", 75 | "options": { 76 | "customWebpackConfig": { 77 | "path": "./webpack.config.ts" 78 | } 79 | }, 80 | "configurations": { 81 | "production": { 82 | "browserTarget": "angular-server-services:build:production" 83 | }, 84 | "development": { 85 | "browserTarget": "angular-server-services:build:development" 86 | } 87 | }, 88 | "defaultConfiguration": "development" 89 | }, 90 | "extract-i18n": { 91 | "builder": "@angular-devkit/build-angular:extract-i18n", 92 | "options": { 93 | "browserTarget": "angular-server-services:build" 94 | } 95 | }, 96 | "test": { 97 | "builder": "@angular-builders/custom-webpack:karma", 98 | "options": { 99 | "polyfills": [ 100 | "zone.js", 101 | "zone.js/testing" 102 | ], 103 | "tsConfig": "tsconfig.spec.json", 104 | "assets": [ 105 | "src/favicon.ico", 106 | "src/assets" 107 | ], 108 | "styles": [ 109 | "src/styles.css" 110 | ], 111 | "scripts": [] 112 | } 113 | }, 114 | "server": { 115 | "builder": "@angular-builders/custom-webpack:server", 116 | "options": { 117 | "customWebpackConfig": { 118 | "path": "./webpack.config.ts" 119 | }, 120 | "optimization": false, 121 | "outputPath": "dist/angular-server-services/server", 122 | "main": "server/main.ts", 123 | "tsConfig": "tsconfig.server.json" 124 | }, 125 | "configurations": { 126 | "production": { 127 | "outputHashing": "media" 128 | }, 129 | "development": { 130 | "optimization": false, 131 | "sourceMap": true, 132 | "extractLicenses": false, 133 | "vendorChunk": true 134 | } 135 | }, 136 | "defaultConfiguration": "production" 137 | }, 138 | "serve-ssr": { 139 | "builder": "@nguniversal/builders:ssr-dev-server", 140 | "configurations": { 141 | "development": { 142 | "browserTarget": "angular-server-services:build:development", 143 | "serverTarget": "angular-server-services:server:development" 144 | }, 145 | "production": { 146 | "browserTarget": "angular-server-services:build:production", 147 | "serverTarget": "angular-server-services:server:production" 148 | } 149 | }, 150 | "defaultConfiguration": "development" 151 | }, 152 | "prerender": { 153 | "builder": "@nguniversal/builders:prerender", 154 | "options": { 155 | "routes": [ 156 | "/", 157 | "/about", 158 | "/todos" 159 | ] 160 | }, 161 | "configurations": { 162 | "production": { 163 | "browserTarget": "angular-server-services:build:production", 164 | "serverTarget": "angular-server-services:server:production" 165 | }, 166 | "development": { 167 | "browserTarget": "angular-server-services:build:development", 168 | "serverTarget": "angular-server-services:server:development" 169 | } 170 | }, 171 | "defaultConfiguration": "production" 172 | } 173 | } 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-server-services", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test", 10 | "dev:ssr": "ng run angular-server-services:serve-ssr", 11 | "serve:ssr": "node dist/angular-server-services/server/main.js", 12 | "build:ssr": "ng build && ng run angular-server-services:server", 13 | "prerender": "ng run angular-server-services:prerender" 14 | }, 15 | "private": true, 16 | "dependencies": { 17 | "@angular-builders/custom-webpack": "16.0.0-beta.1", 18 | "@angular/animations": "~16.0.3", 19 | "@angular/common": "~16.0.3", 20 | "@angular/compiler": "~16.0.3", 21 | "@angular/core": "~16.0.3", 22 | "@angular/forms": "~16.0.3", 23 | "@angular/platform-browser": "~16.0.3", 24 | "@angular/platform-browser-dynamic": "~16.0.3", 25 | "@angular/platform-server": "~16.0.3", 26 | "@angular/router": "~16.0.3", 27 | "@nguniversal/express-engine": "^16.0.0-next.0", 28 | "body-parser": "1.20.2", 29 | "cross-fetch": "3.1.6", 30 | "express": "^4.15.2", 31 | "injection-js": "2.4.0", 32 | "is-browser": "2.1.0", 33 | "reflect-metadata": "0.1.13", 34 | "rxjs": "~7.8.0", 35 | "tslib": "^2.3.0", 36 | "zone.js": "~0.13.0" 37 | }, 38 | "devDependencies": { 39 | "@angular-devkit/build-angular": "^16.0.3", 40 | "@angular/cli": "~16.0.3", 41 | "@angular/compiler-cli": "~16.0.3", 42 | "@nguniversal/builders": "^16.0.0-next.0", 43 | "@types/express": "^4.17.0", 44 | "@types/jasmine": "~4.3.0", 45 | "@types/node": "^14.15.0", 46 | "jasmine-core": "~4.6.0", 47 | "karma": "~6.4.0", 48 | "karma-chrome-launcher": "~3.1.0", 49 | "karma-coverage": "~2.2.0", 50 | "karma-jasmine": "~5.1.0", 51 | "karma-jasmine-html-reporter": "~2.0.0", 52 | "typescript": "~5.0.2" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /server/main.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; // injection-js 2 | import 'cross-fetch/polyfill'; 3 | import 'zone.js/node'; 4 | import 'zone.js/dist/zone-patch-fetch'; 5 | 6 | import { APP_BASE_HREF } from '@angular/common'; 7 | import { ngExpressEngine } from '@nguniversal/express-engine'; 8 | import * as express from 'express'; 9 | import * as bodyParser from 'body-parser'; 10 | import { existsSync } from 'node:fs'; 11 | import { join } from 'node:path'; 12 | import bootstrap, {injector, transferState} from '../src/bootstrap.server'; 13 | 14 | // The Express app is exported so that it can be used by serverless Functions. 15 | export function app(): express.Express { 16 | const server = express(); 17 | const distFolder = join(process.cwd(), 'dist/angular-server-services/browser'); 18 | const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index'; 19 | 20 | // Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine) 21 | server.engine('html', ngExpressEngine({ 22 | bootstrap 23 | })); 24 | 25 | server.set('view engine', 'html'); 26 | server.set('views', distFolder); 27 | 28 | server.use(bodyParser.json()); 29 | 30 | 31 | // Example Express Rest API endpoints 32 | server.get('/api/**', (req, res) => { 33 | }); 34 | 35 | // TODO: auto generate this in ngExpressEngine to get injector 36 | server.post('/angular-server-services/:Service/:Method', (req, res) => { 37 | const service = injector.get(req.params.Service); 38 | console.log('angular-server-service request: service', req.params.Service) 39 | const method = service[req.params.Method]; 40 | console.log('angular-server-service request: method', req.params.Method) 41 | console.log('angular-server-service request: body', req.body) 42 | method.apply(service, req.body).then((result: any) => { 43 | res.json(result); 44 | }); 45 | }); 46 | 47 | // Serve static files from /browser 48 | server.get('*.*', express.static(distFolder, { 49 | maxAge: '0' 50 | })); 51 | 52 | // All regular routes use the Universal engine 53 | server.get('*', (req, res) => { 54 | // TODO: better transfer state 55 | const state = {}; 56 | transferState._state = state; 57 | res.render(indexHtml, { 58 | req, 59 | providers: [ 60 | { provide: APP_BASE_HREF, useValue: req.baseUrl }, 61 | ], 62 | }, (err, html) =>{ 63 | if (err) { 64 | console.error(err); 65 | res.send(err); 66 | } 67 | console.log('SSR done'); 68 | // TODO: better transfer state 69 | // TODO: auto generate this 70 | res.send(html.replace(//, ``)); 71 | }); 72 | }); 73 | 74 | return server; 75 | } 76 | 77 | function run(): void { 78 | const port = process.env['PORT'] || 4000; 79 | 80 | // Start up the Node server 81 | const server = app(); 82 | server.listen(port, () => { 83 | console.log(`Node Express server listening on http://localhost:${port}`); 84 | }); 85 | } 86 | 87 | // Webpack will replace 'require' with '__webpack_require__' 88 | // '__non_webpack_require__' is a proxy to Node 'require' 89 | // The below code is to ensure that the server is run only when not requiring the bundle. 90 | declare const __non_webpack_require__: NodeRequire; 91 | const mainModule = __non_webpack_require__.main; 92 | const moduleFilename = mainModule && mainModule.filename || ''; 93 | if (moduleFilename === __filename || moduleFilename.includes('iisnode')) { 94 | run(); 95 | } 96 | 97 | export * from '../src/bootstrap.server'; 98 | 99 | // fixes prerendering 100 | export default bootstrap; -------------------------------------------------------------------------------- /src/@types/global.d.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js/zone.d.ts'; 2 | 3 | // ignore this typescript thing 4 | export {}; 5 | 6 | // declare globals 7 | declare global { 8 | const APP_VERSION: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/about/about.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnfrench3/angular-server-repos/7bf87941f0f07cbc15772e7a97fc3566d351d002/src/app/about/about.component.css -------------------------------------------------------------------------------- /src/app/about/about.component.html: -------------------------------------------------------------------------------- 1 |

about works!

2 | 3 | -------------------------------------------------------------------------------- /src/app/about/about.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AboutComponent } from './about.component'; 4 | 5 | describe('AboutComponent', () => { 6 | let component: AboutComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [AboutComponent] 12 | }); 13 | fixture = TestBed.createComponent(AboutComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/app/about/about.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | @Component({ 5 | selector: 'app-about', 6 | standalone: true, 7 | imports: [CommonModule], 8 | templateUrl: './about.component.html', 9 | styleUrls: ['./about.component.css'] 10 | }) 11 | export default class AboutComponent { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnfrench3/angular-server-repos/7bf87941f0f07cbc15772e7a97fc3566d351d002/src/app/app.component.css -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | Home | 2 | About | 3 | Todos 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(() => TestBed.configureTestingModule({ 6 | imports: [AppComponent] 7 | })); 8 | 9 | it('should create the app', () => { 10 | const fixture = TestBed.createComponent(AppComponent); 11 | const app = fixture.componentInstance; 12 | expect(app).toBeTruthy(); 13 | }); 14 | 15 | it(`should have the 'angular-universal-standalone' title`, () => { 16 | const fixture = TestBed.createComponent(AppComponent); 17 | const app = fixture.componentInstance; 18 | expect(app.title).toEqual('angular-universal-standalone'); 19 | }); 20 | 21 | it('should render title', () => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | fixture.detectChanges(); 24 | const compiled = fixture.nativeElement as HTMLElement; 25 | expect(compiled.querySelector('.content span')?.textContent).toContain('angular-universal-standalone app is running!'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterLink, RouterOutlet } from '@angular/router'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | standalone: true, 8 | imports: [ 9 | CommonModule, 10 | RouterOutlet, 11 | RouterLink 12 | ], 13 | templateUrl: './app.component.html', 14 | styleUrls: ['./app.component.css'] 15 | }) 16 | export class AppComponent { 17 | title = 'angular-universal-standalone'; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/app.config.browser.ts: -------------------------------------------------------------------------------- 1 | import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; 2 | 3 | import { appConfig } from './app.config'; 4 | 5 | import { ExampleService } from './home/ServerService/example.service.browser'; 6 | 7 | const serverState = JSON.parse(document?.querySelector('#ng-universal-state')?.textContent as string); 8 | 9 | // TODO: auto generate this 10 | function createProxy(service: string) { 11 | // Proxy with fetch api 12 | return new Proxy({}, { 13 | get: (target, method: string) => { 14 | return async (...args: any[]) => { 15 | // deterministic stringify 16 | const arg = JSON.stringify(args); 17 | if (serverState[service] && serverState[service][method] && serverState[service][method][arg]) { 18 | const state = serverState[service][method][arg]; 19 | console.info(`Using server state for ${service}.${method}`, JSON.stringify(state, null, 2)); 20 | delete serverState[service][method]; 21 | return state; 22 | } else { 23 | console.info(`Requesting server state for ${service}.${method}`); 24 | } 25 | const ngServerService = `angular-server-services` 26 | // TODO: support GET and use query params 27 | const response = await fetch(`/${ngServerService}/${service}/${method}`, { 28 | method: 'POST', 29 | body: arg, 30 | headers: { 31 | 'Content-Type': 'application/json' 32 | } 33 | }); 34 | return response.json(); 35 | } 36 | } 37 | }); 38 | } 39 | 40 | 41 | export const browserConfig: ApplicationConfig = { 42 | providers: [ 43 | // TODO: auto generate this 44 | { provide: ExampleService, useFactory: () => createProxy('ExampleService')} 45 | ] 46 | }; 47 | 48 | export const config = mergeApplicationConfig(appConfig, browserConfig); 49 | -------------------------------------------------------------------------------- /src/app/app.config.server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; 10 | import { provideServerRendering } from '@angular/platform-server'; 11 | import { appConfig } from './app.config'; 12 | 13 | import { TransferState } from './home/ServerService/TransferState' ; 14 | import { ExampleService } from './home/ServerService/example.service.browser'; 15 | import { ExampleService as ExampleServiceServer } from './home/ServerService/example.service.server'; 16 | 17 | import { ReflectiveInjector } from 'injection-js'; 18 | 19 | export const transferState = new TransferState(); 20 | 21 | // TODO: better angular di control 22 | // TODO: auto generate this 23 | export const injector = ReflectiveInjector.resolveAndCreate([ 24 | { provide: TransferState, useValue: transferState}, 25 | { provide: ExampleServiceServer, useClass: ExampleServiceServer }, 26 | { provide: ExampleService, useExisting: ExampleServiceServer }, 27 | { provide: 'ExampleService', useExisting: ExampleServiceServer } 28 | ]); 29 | 30 | const serverConfig: ApplicationConfig = { 31 | providers: [ 32 | provideServerRendering(), 33 | // TODO: auto generate this 34 | { provide: ExampleService, useFactory: () => injector.get(ExampleService) } 35 | ] 36 | }; 37 | 38 | export const config = mergeApplicationConfig(appConfig, serverConfig); 39 | -------------------------------------------------------------------------------- /src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { provideHttpClient } from '@angular/common/http'; 2 | import { ApplicationConfig } from '@angular/core'; 3 | import { provideClientHydration } from '@angular/platform-browser'; 4 | import { provideRouter } from '@angular/router'; 5 | 6 | import { routes } from './app.routes'; 7 | 8 | export const appConfig: ApplicationConfig = { 9 | providers: [ 10 | provideRouter(routes), 11 | provideHttpClient(), 12 | provideClientHydration(), 13 | ] 14 | }; 15 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const routes: Routes = [ 4 | { 5 | path: '', loadComponent: () => import('./home/home.component') 6 | }, 7 | { 8 | path: 'about', loadComponent: () => import('./about/about.component') 9 | }, 10 | { 11 | path: 'todos', loadComponent: () => import('./todos/todos.component') 12 | } 13 | ]; 14 | -------------------------------------------------------------------------------- /src/app/home/ServerService/TransferState.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "injection-js"; 2 | 3 | // TODO: implement TransferState 4 | @Injectable() 5 | export class TransferState { 6 | _state: any = {}; 7 | get(key: string) { 8 | return this._state[key]; 9 | } 10 | set(key: string, value: any) { 11 | return this._state[key] = value; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/home/ServerService/example.service.browser.ts: -------------------------------------------------------------------------------- 1 | // TODO: auto generate this file 2 | import { Injectable } from "@angular/core"; 3 | // very important for angular 4 | @Injectable({ 5 | providedIn: "root" 6 | }) 7 | export class ExampleService { 8 | _transferState: any; 9 | async getTodo() {} 10 | } -------------------------------------------------------------------------------- /src/app/home/ServerService/example.service.server.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from "injection-js"; 2 | import { TransferState } from "./TransferState"; 3 | 4 | @Injectable() 5 | export class ExampleService { 6 | constructor( 7 | @Inject(TransferState) public _transferState: TransferState 8 | ) {}; 9 | async getTodo(options: { id: number }) { 10 | // TODO: zone.js fetch 11 | const macroTask = Zone.current 12 | .scheduleMacroTask( 13 | `WAITFOR-${Math.random()}-${Date.now()}`, 14 | () => { }, 15 | {}, 16 | () => { } 17 | ); 18 | 19 | const id = options.id 20 | console.log('server request', id); 21 | const data = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`) 22 | .then(response => response.json()) 23 | .then(json => { 24 | console.log(JSON.stringify(json, null, 2)); 25 | return json; 26 | }); 27 | 28 | // TODO: zone.js fetch 29 | // deterministic stringify 30 | const arg = JSON.stringify(Array.from(arguments)); 31 | this._transferState.set('ExampleService', { 32 | getTodo: { 33 | [arg]: data 34 | } 35 | }); 36 | macroTask.invoke(); 37 | return data; 38 | } 39 | } -------------------------------------------------------------------------------- /src/app/home/ServerService/index.ts: -------------------------------------------------------------------------------- 1 | // TODO: auto generate this file 2 | import * as isBrowser from 'is-browser'; 3 | import { ExampleService as ExampleServiceServer } from './example.service.server'; 4 | import { ExampleService as ExampleServiceBrowser } from './example.service.browser'; 5 | 6 | export const ExampleService: typeof ExampleServiceServer = ExampleServiceBrowser; -------------------------------------------------------------------------------- /src/app/home/home.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnfrench3/angular-server-repos/7bf87941f0f07cbc15772e7a97fc3566d351d002/src/app/home/home.component.css -------------------------------------------------------------------------------- /src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |

home works!

2 | APP_VERSION: {{ APP_VERSION }} 3 | 4 |
{{ example | async | json }}
-------------------------------------------------------------------------------- /src/app/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | 5 | describe('HomeComponent', () => { 6 | let component: HomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [HomeComponent] 12 | }); 13 | fixture = TestBed.createComponent(HomeComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ExampleService } from './ServerService'; 4 | 5 | @Component({ 6 | selector: 'app-home', 7 | standalone: true, 8 | imports: [CommonModule], 9 | templateUrl: './home.component.html', 10 | styleUrls: ['./home.component.css'] 11 | }) 12 | export default class HomeComponent { 13 | exampleService = inject(ExampleService); 14 | example = this.exampleService.getTodo({id: 1}); 15 | // @ts-ignore 16 | APP_VERSION = APP_VERSION; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/app/todos/todos.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnfrench3/angular-server-repos/7bf87941f0f07cbc15772e7a97fc3566d351d002/src/app/todos/todos.component.css -------------------------------------------------------------------------------- /src/app/todos/todos.component.html: -------------------------------------------------------------------------------- 1 |

Todos

2 | 3 | 15 | -------------------------------------------------------------------------------- /src/app/todos/todos.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TodosComponent } from './todos.component'; 4 | 5 | describe('TodosComponent', () => { 6 | let component: TodosComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [TodosComponent] 12 | }); 13 | fixture = TestBed.createComponent(TodosComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/app/todos/todos.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject, signal } from '@angular/core'; 2 | import { NgFor } from '@angular/common'; 3 | 4 | import { TodosService } from './todos.service'; 5 | 6 | @Component({ 7 | selector: 'app-todos', 8 | standalone: true, 9 | imports: [NgFor], 10 | templateUrl: './todos.component.html', 11 | styleUrls: ['./todos.component.css'] 12 | }) 13 | export default class TodosComponent { 14 | todos = signal([]); 15 | todosService = inject(TodosService); 16 | 17 | ngOnInit() { 18 | this.todosService.getTodos().then(todos => { 19 | console.log('todos', todos.length); 20 | this.todos.set(todos); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/todos/todos.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from "@angular/common/http"; 2 | import { Injectable, inject } from "@angular/core"; 3 | import { firstValueFrom } from "rxjs"; 4 | 5 | @Injectable({ 6 | providedIn: "root" 7 | }) 8 | export class TodosService { 9 | http = inject(HttpClient); 10 | 11 | getTodos() { 12 | return firstValueFrom(this.http.get("https://jsonplaceholder.typicode.com/todos")); 13 | } 14 | } -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnfrench3/angular-server-repos/7bf87941f0f07cbc15772e7a97fc3566d351d002/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/bootstrap.browser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import { bootstrapApplication } from '@angular/platform-browser'; 10 | import { AppComponent } from './app/app.component'; 11 | import { config } from './app/app.config.browser'; 12 | 13 | const bootstrap = () => bootstrapApplication(AppComponent, config).catch((err) => console.error(err)); 14 | 15 | export default bootstrap; 16 | -------------------------------------------------------------------------------- /src/bootstrap.server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import { bootstrapApplication } from '@angular/platform-browser'; 10 | import { AppComponent } from './app/app.component'; 11 | import { config } from './app/app.config.server'; 12 | 13 | const bootstrap = () => bootstrapApplication(AppComponent, config); 14 | 15 | export { injector, transferState } from './app/app.config.server'; 16 | 17 | export default bootstrap; 18 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- 1 | �PNG 2 |  3 | IHDR?�~� pHYs  ��~�fIDATH��WKLQ���̔�uG�� e�n�. 6qcb�l?���D`�F#� Ku�F 1Qc� 4 | ��!���� ��C�P�|B?$���ܱ3����I&}��}�̽s�[*�ɀU�A��K��yx�gY�Ajq��3L Š���˫�OD�4��3Ϗ:X�3��o�P J�ğo#IH�a����,{>1/�2$�R AR]�)w��?�sZw^��q�Y�m_��e���r[8�^� 5 | �&p��-���A}c��- ������!����2_) E�)㊪j���v�m��ZOi� g�nW�{�n.|�e2�a&�0aŸ����be�̀��C�fˤE%-{�ֺ��׮C��N��jXi�~c�C,t��T�����r�{� �L)s��V��6%�(�#ᤙ!�]��H�ҐH$R���^R�A�61(?Y舚�>���(Z����Qm�L2�K�ZIc�� 6 | ���̧�C��2!⅄�(����"�Go��>�q��=��$%�z`ѯ��T�&����PHh�Z!=���z��O��������,*VVV�1�f*CJ�]EE��K�k��d�#5���`2yT!�}7���߈~�,���zs�����y�T��V������D��C2�G��@%̑72Y�޾{oJ�"@��^h�~ ��fĬ�!a�D��6���Ha|�3��� [>�����]7U2п���]�ė 7 | ��PU� �.Wejq�in�g��+p<ߺQH����總j[������.��� Q���p _�K�� 1(��+��bB8�\ra 8 | �́�v.l���(���ǽ�w���L��w�8�C��IEND�B`� -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularUniversalStandalone 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import bootstrap from './bootstrap.browser'; 2 | 3 | // dom ready 4 | requestAnimationFrame(() => { 5 | bootstrap(); 6 | }); 7 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "resolveJsonModule": true, 6 | "baseUrl": "./", 7 | "outDir": "./dist/out-tsc", 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "noImplicitOverride": true, 11 | "noPropertyAccessFromIndexSignature": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "sourceMap": true, 15 | "declaration": false, 16 | "downlevelIteration": true, 17 | "experimentalDecorators": true, 18 | "moduleResolution": "node", 19 | "importHelpers": true, 20 | "target": "ES2022", 21 | "module": "ES2022", 22 | "useDefineForClassFields": false, 23 | "lib": [ 24 | "ES2022", 25 | "dom" 26 | ] 27 | }, 28 | "angularCompilerOptions": { 29 | "enableI18nLegacyMessageIdFormat": false, 30 | "strictInjectionParameters": true, 31 | "strictInputAccessModifiers": true, 32 | "strictTemplates": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.app.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/server", 6 | "types": [ 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/bootstrap.server.ts", 12 | "server/main.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, DefinePlugin } from 'webpack'; 2 | import { CustomWebpackBrowserSchema, TargetOptions } from '@angular-builders/custom-webpack'; 3 | 4 | import * as pkg from './package.json'; 5 | 6 | export default ( 7 | cfg: Configuration, 8 | opts: CustomWebpackBrowserSchema, 9 | targetOptions: TargetOptions 10 | ) => { 11 | cfg?.plugins?.push( 12 | new DefinePlugin({ 13 | APP_VERSION: JSON.stringify(pkg.version), 14 | }) 15 | ); 16 | 17 | return cfg; 18 | }; --------------------------------------------------------------------------------