;
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/PatrickJS/angular-server-services/7a5cba314256915672f386239e094d683bb13622/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, TransferState, makeStateKey, APP_INITIALIZER, Injectable, inject } from '@angular/core';
2 | import { appConfig } from './app.config';
3 |
4 | import { ExampleService } from "@client/ExampleService";
5 | import { HttpClient } from '@angular/common/http';
6 | import { lastValueFrom } from "rxjs";
7 |
8 | // TODO: auto generate this
9 |
10 | @Injectable({
11 | providedIn: 'root'
12 | })
13 | export class BatchClientRequests {
14 | httpClient = inject(HttpClient);
15 | transferState = inject(TransferState);
16 | _queue: any[] = [];
17 | _promises = new Map();
18 | _timer: any = null;
19 | _processing = false;
20 | _delay = 50;
21 |
22 | init(delay: number = this._delay) {
23 | this._delay = delay;
24 | this.queue();
25 | }
26 | queue() {
27 | if (this._processing) return;
28 | this._processing = true;
29 | this._timer = setTimeout(() => {
30 | this.flush();
31 | }, this._delay);
32 | }
33 | async flush() {
34 | const body = this._queue;
35 | if (this._queue.length === 0) {
36 | this._processing = false;
37 | clearTimeout(this._timer);
38 | return;
39 | }
40 | this._queue = [];
41 | clearTimeout(this._timer);
42 | const ngServerService = `angular-server-services`;
43 | // TODO: support GET and use query params maybe
44 | const response = await lastValueFrom(
45 | this.httpClient.post(`/${ngServerService}`, body)
46 | ) as typeof body;
47 | let data = null;
48 |
49 | // TODO: use rxjs to ensure backpressure
50 | body.forEach((res: any, index) => {
51 | this._promises.get(res).resolve(response[index].value);
52 | });
53 | this._processing = false;
54 | if (this._queue.length) {
55 | this.queue();
56 | }
57 | return data;
58 | }
59 | _createDefer() {
60 | let resolve: any;
61 | let reject: any;
62 | const promise = new Promise((res, rej) => {
63 | resolve = res;
64 | reject = rej;
65 | });
66 | return {
67 | promise,
68 | resolve,
69 | reject
70 | };
71 | }
72 | pushQueue(data: any) {
73 | this._queue.push(data);
74 | // create defer promise
75 | const defer = this._createDefer();
76 | this._promises.set(data, defer);
77 | this.queue();
78 | return defer.promise;
79 | }
80 | createProxy(service: string) {
81 | // Proxy with httpClient api
82 | // batch requests with rxjs
83 | return new Proxy({}, {
84 | get: (target, method: string) => {
85 | return async (...args: any[]) => {
86 | // does httpClient deterministically stringify args??
87 | const params = JSON.stringify(args);
88 | const key = makeStateKey(`${service}.${method}(${params})`)
89 | if (this.transferState.hasKey(key)) {
90 | const res = this.transferState.get(key, null);
91 | if (res) {
92 | this.transferState.remove(key);
93 | return res;
94 | }
95 | }
96 | const req = {
97 | service: service,
98 | method: method,
99 | args: args
100 | };
101 | return this.pushQueue(req)
102 | };
103 | }
104 | });
105 | }
106 |
107 | }
108 |
109 | export const browserConfig: ApplicationConfig = {
110 | providers: [
111 | // TODO: auto generate this
112 | { provide: BatchClientRequests, useClass: BatchClientRequests, },
113 | {
114 | provide: APP_INITIALIZER,
115 | useFactory: (batch: BatchClientRequests) => {
116 | return () => batch.init();
117 | },
118 | deps: [BatchClientRequests],
119 | multi: true,
120 | },
121 | {
122 | provide: ExampleService,
123 | useFactory: (batch: BatchClientRequests) => batch.createProxy('ExampleService'),
124 | deps: [BatchClientRequests]
125 | }]
126 | };
127 | export const config = mergeApplicationConfig(appConfig, browserConfig);
--------------------------------------------------------------------------------
/src/app/app.config.server.ts:
--------------------------------------------------------------------------------
1 | import { mergeApplicationConfig, ApplicationConfig } from "@angular/core"
2 | import { provideServerRendering } from "@angular/platform-server"
3 |
4 | import { ExampleService } from "@client/ExampleService"
5 | import { ExampleService as ExampleServiceServer } from "@server/ExampleService"
6 |
7 | import { appConfig } from "./app.config"
8 |
9 | const serverConfig: ApplicationConfig = {
10 | providers: [
11 | provideServerRendering(),
12 | // TODO: auto generate @server/ services
13 | {
14 | provide: ExampleServiceServer,
15 | useClass: ExampleServiceServer,
16 | },
17 | {
18 | provide: ExampleService,
19 | useExisting: ExampleServiceServer,
20 | },
21 | ],
22 | }
23 |
24 | export const config = mergeApplicationConfig(appConfig, serverConfig)
25 |
--------------------------------------------------------------------------------
/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/home.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PatrickJS/angular-server-services/7a5cba314256915672f386239e094d683bb13622/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 }}
5 | {{ example1 | async | json }}
6 | {{ example2 | async | json }}
7 | {{ example3 | async | json }}
8 | {{ example4 | async | json }}
9 |
--------------------------------------------------------------------------------
/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 |
4 | import { ExampleService } from "@client/ExampleService";
5 |
6 |
7 | export function randomNumber(min: number, max: number) {
8 | return Math.floor(Math.random() * (max - min + 1)) + min;
9 | }
10 |
11 | @Component({
12 | selector: 'app-home',
13 | standalone: true,
14 | imports: [CommonModule],
15 | templateUrl: './home.component.html',
16 | styleUrls: ['./home.component.css']
17 | })
18 | export default class HomeComponent {
19 | exampleService = inject(ExampleService);
20 | // request data stream from service
21 | example = this.exampleService.getTodo({
22 | id: 1
23 | });
24 | example1 = this.exampleService.getTodo({
25 | id: randomNumber(1, 5)
26 | });
27 | example2 = this.exampleService.getTodo({
28 | id: randomNumber(5, 10)
29 | });
30 | example3 = this.exampleService.getTodo({
31 | id: randomNumber(10, 15)
32 | });
33 | example4 = this.exampleService.getTodo({
34 | id: randomNumber(15, 20)
35 | });
36 |
37 | // defined in webpack
38 | // @ts-ignore
39 | APP_VERSION = APP_VERSION;
40 | }
--------------------------------------------------------------------------------
/src/app/todos/todos.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PatrickJS/angular-server-services/7a5cba314256915672f386239e094d683bb13622/src/app/todos/todos.component.css
--------------------------------------------------------------------------------
/src/app/todos/todos.component.html:
--------------------------------------------------------------------------------
1 | Todos
2 |
3 |
4 | -
5 |
6 | {{todo.title}}
7 |
8 |
14 |
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/PatrickJS/angular-server-services/7a5cba314256915672f386239e094d683bb13622/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 default bootstrap;
16 |
--------------------------------------------------------------------------------
/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`�
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Angular Server Services
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(function ready () {
5 | return document.body ? bootstrap() : requestAnimationFrame(ready);
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 | "paths": {
28 | "@server": ["src/@server/index"],
29 | "@server/*": ["src/@server/*"],
30 | "@client/*": ["src/@client/*"]
31 | }
32 | },
33 | "angularCompilerOptions": {
34 | "enableI18nLegacyMessageIdFormat": false,
35 | "strictInjectionParameters": true,
36 | "strictInputAccessModifiers": true,
37 | "strictTemplates": true
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/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 | import * as path from 'path';
4 |
5 | import * as pkg from './package.json';
6 | import { AngularServerServicePlugin } from './webpack/AngularServerServicePlugin';
7 |
8 |
9 | export default (
10 | cfg: Configuration,
11 | opts: CustomWebpackBrowserSchema,
12 | targetOptions: TargetOptions
13 | ) => {
14 | const isServer = targetOptions.target === 'server';
15 | cfg?.plugins?.push(
16 | new DefinePlugin({
17 | APP_VERSION: JSON.stringify(pkg.version),
18 | }),
19 | );
20 | // if (!isServer) {
21 | // cfg?.plugins?.push(
22 | // new AngularServerServicePlugin({
23 | // "target": isServer ? 'server' : 'browser',
24 | // // TODO: grab server config from angular.json
25 | // "serverConfig": path.join(__dirname, 'src/app/app.config.server.ts'),
26 | // // TODO: grab all components in @server folder
27 | // "serverComponents": [
28 | // "ExampleService"
29 | // ]
30 | // })
31 | // );
32 | // }
33 |
34 | return cfg;
35 | };
--------------------------------------------------------------------------------
/webpack/AngularServerServicePlugin.ts:
--------------------------------------------------------------------------------
1 | import * as prettier from 'prettier';
2 | import { parse, ParserOptions } from '@babel/parser';
3 | import traverse, { NodePath } from '@babel/traverse';
4 | import * as t from '@babel/types';
5 | import generate from '@babel/generator';
6 | import * as fs from 'fs';
7 | import * as path from 'path';
8 |
9 | function getFilesInDirectory(directory: string, extension: string): string[] {
10 | let files: string[] = [];
11 |
12 | const items = fs.readdirSync(directory);
13 | for (const item of items) {
14 | const fullPath = path.join(directory, item);
15 | const stat = fs.statSync(fullPath);
16 |
17 | if (stat.isDirectory()) {
18 | files = files.concat(getFilesInDirectory(fullPath, extension));
19 | } else if (path.extname(item) === extension) {
20 | files.push(fullPath);
21 | }
22 | }
23 |
24 | return files;
25 | }
26 |
27 |
28 | interface Compiler {
29 | inputFileSystem: any;
30 | outputFileSystem: any;
31 | context: string;
32 | hooks: {
33 | beforeCompile: {
34 | tapAsync: (name: string, callback: (params: unknown, cb: () => void) => void) => void;
35 | };
36 | };
37 | }
38 |
39 | export class AngularServerServicePlugin {
40 | once = false;
41 | constructor(private options: { target: 'server' | 'browser', serverConfig: string, serverComponents: string[] }) {}
42 | apply(compiler: Compiler) {
43 | if (this.once) {
44 | return;
45 | }
46 | compiler.hooks.beforeCompile.tapAsync('AngularServerServicePlugin', (params, callback) => {
47 | if (this.once) {
48 | callback();
49 | return;
50 | }
51 | const serverComponents = this.options.serverComponents;
52 | const parserOptions: ParserOptions = {
53 | sourceType: 'module',
54 | plugins: ['typescript', 'decorators-legacy'],
55 | };
56 | this.generateClientComponent(serverComponents, compiler, parserOptions);
57 | this.replaceServerWithClientImports(
58 | path.resolve(compiler.context, './src/app'),
59 | compiler,
60 | parserOptions
61 | );
62 | this.generateServerConfig(serverComponents, compiler, parserOptions);
63 |
64 |
65 | this.once = true;
66 | callback();
67 | });
68 | }
69 |
70 | generateServerConfig(serverComponents: string[], compiler: Compiler, parserOptions: ParserOptions) {
71 | const filePath = path.resolve(compiler.context, this.options.serverConfig);
72 | const fileContent = fs.readFileSync(filePath, 'utf-8');
73 | const ast = parse(fileContent, parserOptions);
74 | // console.log('ast', ast);
75 |
76 | traverse(ast, {
77 | Program(path: NodePath) {
78 | const newImportStatements = [
79 | "import { ReflectiveInjector } from 'injection-js';",
80 | "import { ExampleService } from '@client/ExampleService';",
81 | "import { ExampleService as ExampleServiceServer } from '@server/ExampleService';",
82 | ].map((statement) => {
83 | const ast = parse(statement, parserOptions);
84 | // console.log('ast', ast);
85 | return ast.program.body[0] as t.ImportDeclaration;
86 | });
87 |
88 | path.node.body.unshift(...newImportStatements);
89 |
90 | const newVariableStatements = [
91 | `export const injector = ReflectiveInjector.resolveAndCreate([
92 | { provide: ExampleServiceServer, useClass: ExampleServiceServer },
93 | { provide: ExampleService, useExisting: ExampleServiceServer },
94 | { provide: 'ExampleService', useExisting: ExampleServiceServer }
95 | ]);`,
96 | ].map((statement) => {
97 | const ast = parse(statement, parserOptions);
98 | // console.log('ast', ast);
99 | return ast.program.body[0] as t.ExportNamedDeclaration;
100 | });
101 |
102 | path.node.body.push(...newVariableStatements);
103 | },
104 |
105 | VariableDeclaration(path: NodePath) {
106 | if (
107 | t.isIdentifier(path.node.declarations[0].id) &&
108 | path.node.declarations[0].id.name === 'serverConfig' &&
109 | t.isObjectExpression(path.node.declarations[0].init!)
110 | ) {
111 | const providersProperty = path.node.declarations[0].init!.properties.find(
112 | (property): property is t.ObjectProperty =>
113 | t.isObjectProperty(property) &&
114 | t.isIdentifier(property.key) &&
115 | property.key.name === 'providers'
116 | );
117 |
118 | if (
119 | providersProperty &&
120 | t.isArrayExpression(providersProperty.value)
121 | ) {
122 | // Iterate over each component in the serverComponents array
123 | for (const Service of serverComponents) {
124 | // Generate the provider string and parse it
125 | const provider = `{ provide: ${Service}, useFactory: () => injector.get(${Service}) }`;
126 | const newProvider = parse(
127 | `(${ provider })`,
128 | parserOptions
129 | ).program.body[0] as t.ExpressionStatement;
130 | // Add the generated provider to the providers array in the AST
131 | providersProperty.value.elements.push(newProvider.expression as t.ObjectExpression);
132 | }
133 | }
134 | }
135 | },
136 | });
137 |
138 | const output = generate(ast, {}, fileContent);
139 | const formattedOutput = prettier.format(output.code, { semi: false, parser: "babel" });
140 | // fs.writeFileSync(filePath, formattedOutput, 'utf-8');
141 | // const fileContent = compiler.inputFileSystem.readFileSync(filePath, 'utf-8');
142 | compiler.outputFileSystem.writeFileSync(filePath, formattedOutput, 'utf-8');
143 |
144 | }
145 | replaceServerWithClientImports(
146 | appFolderPath: string,
147 | compiler: Compiler,
148 | parserOptions: ParserOptions
149 | ) {
150 | // Get all .ts files in the app folder and subfolders
151 | const files = getFilesInDirectory(appFolderPath, '.ts');
152 |
153 | files.forEach((file) => {
154 | const fileContent = fs.readFileSync(file, 'utf-8');
155 |
156 | // Only edit files with @server and not .server files
157 | if (!fileContent.includes('@server') || file.includes('.server')) {
158 | return;
159 | }
160 |
161 | const ast = parse(fileContent, parserOptions);
162 |
163 | traverse(ast, {
164 | ImportDeclaration(path: NodePath) {
165 | if (t.isStringLiteral(path.node.source) && path.node.source.value.startsWith('@server/')) {
166 | // Replace '@server' with '@client'
167 | path.node.source.value = path.node.source.value.replace('@server', '@client');
168 | }
169 | },
170 | });
171 |
172 | const output = generate(ast, {}, fileContent);
173 | // fs.writeFileSync(file, output.code, 'utf-8');
174 | compiler.outputFileSystem.writeFileSync(file, output.code, 'utf-8');
175 | });
176 | }
177 |
178 | generateClientComponent(serverComponents: string[], compiler: Compiler, parserOptions: ParserOptions) {
179 | const serverServicePath = path.resolve(compiler.context, './src/@server/ExampleService.ts');
180 | const clientServicePath = path.resolve(compiler.context, './src/@client/ExampleService.ts');
181 |
182 | const serverServiceContent = fs.readFileSync(serverServicePath, 'utf-8');
183 | const ast = parse(serverServiceContent, parserOptions);
184 |
185 | traverse(ast, {
186 | ClassDeclaration(path: NodePath) {
187 | const injectableImport = parse(
188 | "import { Injectable } from '@angular/core';",
189 | parserOptions
190 | ).program.body[0] as t.ImportDeclaration;
191 |
192 | const injectableDecorator = t.decorator(
193 | t.callExpression(
194 | t.identifier("Injectable"),
195 | [t.objectExpression([t.objectProperty(t.identifier("providedIn"), t.stringLiteral("root"))])]
196 | )
197 | );
198 |
199 |
200 | path.node.decorators = [injectableDecorator];
201 |
202 | path.node.body.body = path.node.body.body.map((methodDefinition) => {
203 | if (t.isClassMethod(methodDefinition)) {
204 | methodDefinition.body.body = [];
205 | }
206 | return methodDefinition;
207 | });
208 |
209 | ast.program.body.unshift(injectableImport);
210 | }
211 | });
212 |
213 | const output = generate(ast, {}, serverServiceContent);
214 | const formattedOutput = prettier.format(output.code, { semi: false, parser: "babel" });
215 | // fs.writeFileSync(clientServicePath, formattedOutput, 'utf-8');
216 | compiler.outputFileSystem.writeFileSync(clientServicePath, formattedOutput, 'utf-8');
217 | }
218 | }
--------------------------------------------------------------------------------