├── src
├── assets
│ └── .gitkeep
├── app
│ ├── app.component.css
│ ├── about
│ │ ├── about.component.css
│ │ ├── about.component.html
│ │ ├── about.component.ts
│ │ └── about.component.spec.ts
│ ├── home
│ │ ├── home.component.css
│ │ ├── home.component.html
│ │ ├── ServerService
│ │ │ ├── example.service.browser.ts
│ │ │ ├── TransferState.ts
│ │ │ ├── index.ts
│ │ │ └── example.service.server.ts
│ │ ├── home.component.ts
│ │ └── home.component.spec.ts
│ ├── todos
│ │ ├── todos.component.css
│ │ ├── todos.component.html
│ │ ├── todos.service.ts
│ │ ├── todos.component.spec.ts
│ │ └── todos.component.ts
│ ├── app.component.html
│ ├── app.routes.ts
│ ├── app.config.ts
│ ├── app.component.ts
│ ├── app.component.spec.ts
│ ├── app.config.server.ts
│ └── app.config.browser.ts
├── styles.css
├── main.ts
├── @types
│ └── global.d.ts
├── index.html
├── bootstrap.browser.ts
├── bootstrap.server.ts
└── favicon.ico
├── .gitignore
├── tsconfig.app.json
├── tsconfig.spec.json
├── tsconfig.server.json
├── webpack.config.ts
├── tsconfig.json
├── package.json
├── README.md
├── server
└── main.ts
└── angular.json
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/app.component.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/about/about.component.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/home/home.component.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/todos/todos.component.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .DS_Store
4 | .angular
5 |
--------------------------------------------------------------------------------
/src/app/about/about.component.html:
--------------------------------------------------------------------------------
1 |
about works!
2 |
3 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
--------------------------------------------------------------------------------
/src/app/home/home.component.html:
--------------------------------------------------------------------------------
1 | home works!
2 | APP_VERSION: {{ APP_VERSION }}
3 |
4 | {{ example | async | json }}
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import bootstrap from './bootstrap.browser';
2 |
3 | // dom ready
4 | requestAnimationFrame(() => {
5 | bootstrap();
6 | });
7 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 | Home |
2 | About |
3 | Todos
4 |
5 |
6 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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.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/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AngularUniversalStandalone
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/app/todos/todos.component.html:
--------------------------------------------------------------------------------
1 | Todos
2 |
3 |
4 |
5 |
6 | {{todo.title}}
7 |
8 |
14 |
15 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 | };
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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�PJ�ğ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`�
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------
/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;
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------