@dooboostore/simple-boot-http-server-ssr 1.0.12 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.MD +1 -3
  2. package/dist/cjs/filters/IntentSchemeFilter.js.map +2 -2
  3. package/dist/cjs/filters/SSRDomParserFilter.js +186 -0
  4. package/dist/cjs/filters/SSRDomParserFilter.js.map +7 -0
  5. package/dist/cjs/filters/SSRFilter.js +1 -1
  6. package/dist/cjs/filters/SSRFilter.js.map +2 -2
  7. package/dist/cjs/filters/SSRLinkDomDomFilter.js +183 -0
  8. package/dist/cjs/filters/SSRLinkDomDomFilter.js.map +7 -0
  9. package/dist/cjs/initializers/DomParserInitializer.js +79 -0
  10. package/dist/cjs/initializers/DomParserInitializer.js.map +7 -0
  11. package/dist/cjs/initializers/LinkedomInitializer.js +81 -0
  12. package/dist/cjs/initializers/LinkedomInitializer.js.map +7 -0
  13. package/dist/cjs/proxy/SymbolIntentApiServiceProxy.js +85 -73
  14. package/dist/cjs/proxy/SymbolIntentApiServiceProxy.js.map +2 -2
  15. package/dist/cjs/proxy/SymbolIntentApiServiceProxyBase.js +2 -77
  16. package/dist/cjs/proxy/SymbolIntentApiServiceProxyBase.js.map +2 -2
  17. package/dist/esm/filters/IntentSchemeFilter.js.map +2 -2
  18. package/dist/esm/filters/SSRDomParserFilter.js +163 -0
  19. package/dist/esm/filters/SSRDomParserFilter.js.map +7 -0
  20. package/dist/esm/filters/SSRFilter.js +1 -1
  21. package/dist/esm/filters/SSRFilter.js.map +2 -2
  22. package/dist/esm/filters/SSRLinkDomDomFilter.js +160 -0
  23. package/dist/esm/filters/SSRLinkDomDomFilter.js.map +7 -0
  24. package/dist/esm/initializers/DomParserInitializer.js +46 -0
  25. package/dist/esm/initializers/DomParserInitializer.js.map +7 -0
  26. package/dist/esm/initializers/LinkedomInitializer.js +48 -0
  27. package/dist/esm/initializers/LinkedomInitializer.js.map +7 -0
  28. package/dist/esm/proxy/SymbolIntentApiServiceProxy.js +85 -73
  29. package/dist/esm/proxy/SymbolIntentApiServiceProxy.js.map +2 -2
  30. package/dist/esm/proxy/SymbolIntentApiServiceProxyBase.js +2 -77
  31. package/dist/esm/proxy/SymbolIntentApiServiceProxyBase.js.map +2 -2
  32. package/dist/esm-bundle/dooboostore-simple-boot-http-server-ssr.esm.js +1184 -525
  33. package/dist/esm-bundle/dooboostore-simple-boot-http-server-ssr.esm.js.map +4 -4
  34. package/dist/types/filters/IntentSchemeFilter.d.ts.map +1 -1
  35. package/dist/types/filters/SSRDomParserFilter.d.ts +57 -0
  36. package/dist/types/filters/SSRDomParserFilter.d.ts.map +1 -0
  37. package/dist/types/filters/SSRLinkDomDomFilter.d.ts +57 -0
  38. package/dist/types/filters/SSRLinkDomDomFilter.d.ts.map +1 -0
  39. package/dist/types/index.d.ts +0 -19
  40. package/dist/types/initializers/DomParserInitializer.d.ts +11 -0
  41. package/dist/types/initializers/DomParserInitializer.d.ts.map +1 -0
  42. package/dist/types/initializers/LinkedomInitializer.d.ts +11 -0
  43. package/dist/types/initializers/LinkedomInitializer.d.ts.map +1 -0
  44. package/dist/types/proxy/SymbolIntentApiServiceProxy.d.ts +2 -0
  45. package/dist/types/proxy/SymbolIntentApiServiceProxy.d.ts.map +1 -1
  46. package/dist/types/proxy/SymbolIntentApiServiceProxyBase.d.ts +0 -8
  47. package/dist/types/proxy/SymbolIntentApiServiceProxyBase.d.ts.map +1 -1
  48. package/dist/umd-bundle/dooboostore-simple-boot-http-server-ssr.umd.js +1184 -525
  49. package/dist/umd-bundle/dooboostore-simple-boot-http-server-ssr.umd.js.map +4 -4
  50. package/package.json +16 -14
  51. package/src/filters/IntentSchemeFilter.ts +51 -48
  52. package/src/filters/SSRDomParserFilter.ts +233 -0
  53. package/src/filters/SSRFilter.ts +2 -2
  54. package/src/filters/SSRLinkDomDomFilter.ts +227 -0
  55. package/src/initializers/DomParserInitializer.ts +70 -0
  56. package/src/initializers/LinkedomInitializer.ts +69 -0
  57. package/src/proxy/SymbolIntentApiServiceProxy.ts +100 -116
  58. package/src/proxy/SymbolIntentApiServiceProxyBase.ts +7 -85
  59. package/tsconfig.json +2 -1
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@dooboostore/simple-boot-http-server-ssr",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
7
7
  "module": "./dist/esm/index.js",
8
8
  "types": "./dist/types/index.d.ts",
9
9
  "typesVersions": {
10
- ">=4.2": {
10
+ "*": {
11
11
  "*": [
12
12
  "dist/types/*"
13
13
  ]
@@ -62,17 +62,7 @@
62
62
  "url": "https://github.com/dooboostore-develop/packages"
63
63
  },
64
64
  "peerDependencies": {
65
- "reflect-metadata": "^0.2.2",
66
- "@dooboostore/core": "1.0.22",
67
- "@dooboostore/core-node": "1.0.8",
68
- "@dooboostore/core-web": "1.0.7",
69
- "@dooboostore/dom-render": "1.0.109",
70
- "@dooboostore/simple-boot-front": "1.0.128",
71
- "@dooboostore/simple-boot-http-server": "1.0.21",
72
- "@dooboostore/simple-boot": "1.0.51"
73
- },
74
- "dependencies": {
75
- "jsdom": "^26.0.0"
65
+ "reflect-metadata": "^0.2.2"
76
66
  },
77
67
  "devDependencies": {
78
68
  "@types/jsdom": "^21.1.7",
@@ -91,7 +81,19 @@
91
81
  "typescript": "^5.9.2",
92
82
  "@types/node-gzip": "^1.1.0",
93
83
  "esbuild-plugin-tsc": "^0.5.0",
94
- "esbuild": "^0.23.0"
84
+ "esbuild": "^0.23.0",
85
+ "@dooboostore/core-node": "1.0.10",
86
+ "@dooboostore/core-web": "1.0.9",
87
+ "@dooboostore/dom-render": "1.0.112",
88
+ "@dooboostore/simple-boot": "1.0.53",
89
+ "@dooboostore/core": "1.0.28",
90
+ "@dooboostore/simple-boot-front": "1.0.130",
91
+ "@dooboostore/simple-boot-http-server": "1.0.23"
92
+ },
93
+ "dependencies": {
94
+ "jsdom": "^26.0.0",
95
+ "linkedom": "0.18.12",
96
+ "@dooboostore/dom-parser": "1.0.1"
95
97
  },
96
98
  "scripts": {
97
99
  "clean": "rm -rf dist",
@@ -6,61 +6,64 @@ import {Sim} from '@dooboostore/simple-boot/decorators/SimDecorator';
6
6
  import {HttpStatus} from '@dooboostore/simple-boot-http-server/codes/HttpStatus';
7
7
  import {RequestResponse} from '@dooboostore/simple-boot-http-server/models/RequestResponse';
8
8
  import {SimpleBootHttpServer} from '@dooboostore/simple-boot-http-server/SimpleBootHttpServer';
9
- import { Mimes } from '../codes/Mimes';
9
+ import {Mimes} from '../codes/Mimes';
10
+ import {Promises} from "@dooboostore/core/promise/Promises";
10
11
 
11
12
  @Sim
12
13
  export class IntentSchemeFilter implements Filter {
13
- constructor(private intentManager: IntentManager) {
14
- // (IntentManager as any).
15
- // console.log('intent constructor', this.intentManager);
16
- }
14
+ constructor(private intentManager: IntentManager) {
15
+ // (IntentManager as any).
16
+ // console.log('intent constructor', this.intentManager);
17
+ }
17
18
 
18
- async onInit(app: SimpleBootHttpServer) {
19
- }
19
+ async onInit(app: SimpleBootHttpServer) {
20
+ }
20
21
 
21
- async proceedBefore({rr, app}: {rr: RequestResponse, app: SimpleBootHttpServer, carrier: Map<string, any>}) {
22
- // const rr = rrr;
23
- const url = rr.reqUrl;
24
- const contentLength = Number(rr.reqHeader(HttpHeaders.ContentLength.toLowerCase() ?? '0'));
25
- const acceptType = rr.reqHeader(HttpHeaders.Accept);
26
- const contentType = rr.reqHeader(HttpHeaders.ContentType);
27
- const intentScheme = rr.reqHeader(HttpHeaders.XSimpleBootSsrIntentScheme);
28
- if (acceptType === Mimes.ApplicationJsonPostSimpleBootSsrIntentScheme && intentScheme) {
29
- let intent = new Intent(`${intentScheme}:/${url}`);
30
- intent.publishType = PublishType.INLINE_DATA_PARAMETERS;
31
- // const responseHeader = {} as any;
32
- // responseHeader[HttpHeaders.ContentType] = Mimes.ApplicationJson;
33
- if (contentLength > 0) {
34
- if (contentType.includes(Mimes.ApplicationJson)) {
35
- intent.data = [await rr.reqBodyJsonData(), rr];
36
- } else if (contentType.includes(Mimes.MultipartFormData)) {
37
- intent.data = [await rr.reqBodyMultipartFormDataObject(), rr];
38
- }
39
- const rdatas = await this.intentManager.publish(intent);
40
- const rdata = rdatas[0];
41
- const wdata = rdata instanceof Promise ? await rdata : rdata;
42
- rr.resStatusCode(HttpStatus.Ok);
43
- rr.resSetHeader(HttpHeaders.ContentType, [Mimes.ApplicationJson])
44
- await rr.resEnd(wdata ? JSON.stringify(wdata) : undefined);
45
- } else {
46
- intent.data = rr.reqUrlSearchParamTuples.length > 0 ? [rr.reqUrlSearchParamsObj, rr] : [rr];
47
- const rdatas = await this.intentManager.publish(intent);
48
- const rdata = rdatas[0];
49
- const wdata = rdata instanceof Promise ? await rdata : rdata;
50
- // const wdata =undefined;
51
- rr.resStatusCode(HttpStatus.Ok);
52
- rr.resSetHeader(HttpHeaders.ContentType, [Mimes.ApplicationJson])
53
- await rr.resEnd(wdata ? JSON.stringify(wdata) : undefined);
54
- }
55
- // console.log('---------3--intent request', rr.req.readable, rr.req.readableLength);
56
- return false;
57
- } else {
58
- return true;
22
+ async proceedBefore({rr, app}: { rr: RequestResponse, app: SimpleBootHttpServer, carrier: Map<string, any> }) {
23
+ // const rr = rrr;
24
+ const url = rr.reqUrl;
25
+ const contentLength = Number(rr.reqHeader(HttpHeaders.ContentLength.toLowerCase() ?? '0'));
26
+ const acceptType = rr.reqHeader(HttpHeaders.Accept);
27
+ const contentType = rr.reqHeader(HttpHeaders.ContentType);
28
+ const intentScheme = rr.reqHeader(HttpHeaders.XSimpleBootSsrIntentScheme);
29
+ if (acceptType === Mimes.ApplicationJsonPostSimpleBootSsrIntentScheme && intentScheme) {
30
+ let intent = new Intent(`${intentScheme}:/${url}`);
31
+ intent.publishType = PublishType.INLINE_DATA_PARAMETERS;
32
+ // console.log('---------1--intent request', intentScheme, url, contentLength, contentType );
33
+ // const responseHeader = {} as any;
34
+ // responseHeader[HttpHeaders.ContentType] = Mimes.ApplicationJson;
35
+ if (contentLength > 0) {
36
+ if (contentType.includes(Mimes.ApplicationJson)) {
37
+ intent.data = [await rr.reqBodyJsonData(), rr];
38
+ } else if (contentType.includes(Mimes.MultipartFormData)) {
39
+ intent.data = [await rr.reqBodyMultipartFormDataObject(), rr];
59
40
  }
41
+ // console.log('---------2--intent request',intent);
42
+ const rdatas = await this.intentManager.publish(intent);
43
+ const rdata = rdatas[0];
44
+ const wdata = rdata instanceof Promise ? await rdata : rdata;
45
+ rr.resStatusCode(HttpStatus.Ok);
46
+ rr.resSetHeader(HttpHeaders.ContentType, [Mimes.ApplicationJson])
47
+ await rr.resEnd(wdata ? JSON.stringify(wdata) : undefined);
48
+ } else {
49
+ intent.data = rr.reqUrlSearchParamTuples.length > 0 ? [rr.reqUrlSearchParamsObj, rr] : [rr];
50
+ const rdatas = await this.intentManager.publish(intent);
51
+ const rdata = rdatas[0];
52
+ const wdata = rdata instanceof Promise ? await rdata : rdata;
53
+ // const wdata =undefined;
54
+ rr.resStatusCode(HttpStatus.Ok);
55
+ rr.resSetHeader(HttpHeaders.ContentType, [Mimes.ApplicationJson])
56
+ await rr.resEnd(wdata ? JSON.stringify(wdata) : undefined);
57
+ }
58
+ // console.log('---------3--intent request', rr.req.readable, rr.req.readableLength);
59
+ return false;
60
+ } else {
61
+ return true;
60
62
  }
63
+ }
61
64
 
62
- async proceedAfter({rr, app}: {rr: RequestResponse, app: SimpleBootHttpServer, carrier: Map<string, any>}) {
63
- return true;
64
- }
65
+ async proceedAfter({rr, app}: { rr: RequestResponse, app: SimpleBootHttpServer, carrier: Map<string, any> }) {
66
+ return true;
67
+ }
65
68
 
66
69
  }
@@ -0,0 +1,233 @@
1
+ import { RequestResponse } from '@dooboostore/simple-boot-http-server/models/RequestResponse';
2
+ import { HttpHeaders } from '@dooboostore/simple-boot-http-server/codes/HttpHeaders';
3
+ import { SimpleBootHttpSSRFactory } from '../SimpleBootHttpSSRFactory';
4
+ import { ConstructorType } from '@dooboostore/core/types';
5
+ import { Filter } from '@dooboostore/simple-boot-http-server/filters/Filter';
6
+ import { Mimes } from '@dooboostore/simple-boot-http-server/codes/Mimes';
7
+ import { HttpStatus } from '@dooboostore/simple-boot-http-server/codes/HttpStatus';
8
+ import { SimpleBootHttpServer } from '@dooboostore/simple-boot-http-server/SimpleBootHttpServer';
9
+ import { SimFrontOption } from '@dooboostore/simple-boot-front/option/SimFrontOption';
10
+ import { SimpleBootFront } from '@dooboostore/simple-boot-front/SimpleBootFront';
11
+ import { AsyncBlockingQueue } from '@dooboostore/core/queues/AsyncBlockingQueue';
12
+ import { RandomUtils } from '@dooboostore/core/random/RandomUtils';
13
+ import * as JSDOM from 'jsdom';
14
+ import { NotFoundError } from '@dooboostore/simple-boot-http-server/errors/NotFoundError';
15
+ import { DomRenderProxy } from '@dooboostore/dom-render/DomRenderProxy';
16
+ import { filter } from '@dooboostore/core/message/operators/filter';
17
+ import { first } from '@dooboostore/core/message/operators/first';
18
+ import { delay } from '@dooboostore/core/message/operators/delay';
19
+ import { firstValueFrom } from '@dooboostore/core/message/internal/firstValueFrom';
20
+ import { parseHTML } from 'linkedom';
21
+ import {Promises} from '@dooboostore/core/promise/Promises';
22
+ import { DomParserInitializer } from '../initializers/DomParserInitializer';
23
+
24
+ export type FactoryAndParams = {
25
+ frontDistPath: string;
26
+ frontDistIndexFileName?: string;
27
+ factorySimFrontOption: (window: any) => SimFrontOption;
28
+ factory: SimpleBootHttpSSRFactory;
29
+ poolOption: {
30
+ max: number;
31
+ min: number;
32
+ clearIntervalTime?: number;
33
+ };
34
+ using: ConstructorType<any>[];
35
+ domExcludes?: ConstructorType<any>[];
36
+ ssrExcludeFilter?: (rr: RequestResponse) => boolean;
37
+ simpleBootFront?: {
38
+ notFoundError?: boolean;
39
+ };
40
+ };
41
+
42
+ export class SSRDomParserFilter implements Filter {
43
+ private simpleBootFrontPool: SimpleBootFront[] = [];
44
+ private simpleBootFrontQueue = new AsyncBlockingQueue<SimpleBootFront>();
45
+ // private indexHTML: string;
46
+ private welcomUrl = 'http://localhost';
47
+ private poolGeneration = 0;
48
+ private intervalId?: NodeJS.Timeout;
49
+
50
+ constructor(
51
+ public config: FactoryAndParams,
52
+ public otherInstanceSim?: Map<ConstructorType<any>, any>
53
+ ) {
54
+ config.frontDistIndexFileName = config.frontDistIndexFileName || 'index.html';
55
+ // this.indexHTML = JsdomInitializer.loadFile(this.config.frontDistPath, config.frontDistIndexFileName);
56
+ }
57
+
58
+ async onInit(app: SimpleBootHttpServer) {
59
+ for (let i = 0; i < this.config.poolOption.min; i++) {
60
+ await this.pushQueue();
61
+ }
62
+
63
+ if (this.config.poolOption.clearIntervalTime && this.config.poolOption.clearIntervalTime > 0) {
64
+ this.intervalId = setInterval(() => {
65
+ this.poolGeneration++;
66
+ }, this.config.poolOption.clearIntervalTime);
67
+ }
68
+ }
69
+
70
+ async onDestroy() {
71
+ if (this.intervalId) {
72
+ clearInterval(this.intervalId);
73
+ }
74
+ // this.simpleBootFrontQueue.clear();
75
+ this.simpleBootFrontPool.forEach(front => {
76
+ (front as any).jsdom?.window.close();
77
+ });
78
+ this.simpleBootFrontPool = [];
79
+ }
80
+
81
+ async makePage() {
82
+ let windowPromise = await new DomParserInitializer(
83
+ this.config.frontDistPath,
84
+ this.config.frontDistIndexFileName || 'index.html',
85
+ { url: this.welcomUrl }
86
+ ).run();
87
+ return windowPromise;
88
+ // const jsdom = await new JsdomInitializer(
89
+ // this.config.frontDistPath,
90
+ // this.config.frontDistIndexFileName || 'index.html',
91
+ // { url: this.welcomUrl }
92
+ // ).run();
93
+ // return jsdom;
94
+ }
95
+
96
+ async makeFront(window: Window) {
97
+ const name = RandomUtils.uuid();
98
+ (window as any).ssrUse = false;
99
+ const option = this.config.factorySimFrontOption(DomRenderProxy.final(window));
100
+ const simpleBootFront = await this.config.factory.create(option, this.config.using, this.config.domExcludes);
101
+ simpleBootFront.run(this.otherInstanceSim);
102
+ (simpleBootFront as any).generation = this.poolGeneration;
103
+ return simpleBootFront;
104
+ }
105
+
106
+ enqueueFrontApp(simpleBootFront: SimpleBootFront) {
107
+ // this.simpleBootFrontPool.set(simpleBootFront.option.name!, simpleBootFront);
108
+ this.simpleBootFrontPool.push(simpleBootFront);
109
+ this.simpleBootFrontQueue.enqueue(simpleBootFront);
110
+ }
111
+ async pushQueue() {
112
+ if (this.simpleBootFrontPool.length < this.config.poolOption.max) {
113
+ this.enqueueFrontApp(await this.makeFront(await this.makePage()));
114
+ }
115
+ }
116
+
117
+ async proceedBefore({ rr, app }: { rr: RequestResponse; app: SimpleBootHttpServer; carrier: Map<string, any> }) {
118
+ if (this.config.ssrExcludeFilter?.(rr)) {
119
+ return false;
120
+ }
121
+ if (rr.reqHasAcceptHeader(Mimes.TextHtml) || rr.reqHasAcceptHeader(Mimes.All)) {
122
+ if (this.simpleBootFrontQueue.isEmpty()) {
123
+ await this.pushQueue();
124
+ }
125
+ const simpleBootFront = await this.simpleBootFrontQueue.dequeue();
126
+ try {
127
+ (simpleBootFront.option.window as any).ssrUse = true;
128
+ delete (simpleBootFront.option.window as any).server_side_data;
129
+ const url = rr.reqUrlObj({ host: 'localhost' });
130
+ if (this.config.simpleBootFront?.notFoundError) {
131
+ // intent router check first
132
+ const intent = await simpleBootFront.getIntent(url.pathname);
133
+ // route를 못찾은상태에서 router path까지 안맞으면 404 처리한다. route랑 router랑 다르니깐 헛갈리지말도록
134
+ if (intent.module === undefined && !intent.getRouterPathData(rr.reqUrlPathName)) {
135
+ throw new NotFoundError({ message: `Not Found: ${rr.reqUrlPathName}` });
136
+ }
137
+ }
138
+
139
+ // runRouting!!
140
+ const targetUrl = url.toString();
141
+ // console.log('runRoutingrunRouting', targetUrl);
142
+ await simpleBootFront.goRouting(targetUrl);
143
+ await Promises.sleep(0); // <--중요: 이거 넣어야지 두번불러지는게 없어지는듯? 뭐지 event loop 변경된건가?
144
+
145
+ // console.log('runRout22222ingrunRouting', targetUrl);
146
+
147
+ // simpleBootFront.routingSubjectObservable.subscribe(it=>{
148
+ // console.log('zzzzzzzzzzzzzzzzzzz', it);
149
+ // if (it.triggerPoint==='end') {
150
+ // console.log('zzz@@', it.routerModule.intent);
151
+ // }
152
+ // })
153
+
154
+ const data = await firstValueFrom(
155
+ simpleBootFront.routingSubjectObservable.pipe(
156
+ filter(
157
+ (it) =>
158
+ it.triggerPoint === 'end' &&
159
+ typeof it.routerModule.intent.uri === 'string' &&
160
+ targetUrl.endsWith(it.routerModule.intent.uri)
161
+ ),
162
+ // delay(1000),
163
+ first()
164
+ )
165
+ );
166
+ // console.log('???????done');
167
+ await Promises.sleep(0)
168
+ const html = this.makeHTML(simpleBootFront);
169
+ // console.log('---------',html);
170
+ await this.writeOkHtmlAndEnd({ rr }, html);
171
+ } finally {
172
+ (simpleBootFront.option.window as any).ssrUse = false;
173
+ delete (simpleBootFront.option.window as any).server_side_data;
174
+ if ((simpleBootFront as any).generation < this.poolGeneration) {
175
+ // Stale instance, destroy it
176
+ const jsdom = (simpleBootFront as any).jsdom as JSDOM.JSDOM | undefined;
177
+ jsdom?.window.close();
178
+ const index = this.simpleBootFrontPool.indexOf(simpleBootFront);
179
+ if (index > -1) {
180
+ this.simpleBootFrontPool.splice(index, 1);
181
+ }
182
+ this.pushQueue();
183
+ } else {
184
+ // Current instance, return to queue
185
+ this.simpleBootFrontQueue.enqueue(simpleBootFront);
186
+ }
187
+ }
188
+ return false;
189
+ } else {
190
+ return true;
191
+ }
192
+ }
193
+
194
+ makeHTML(simpleBootFront: SimpleBootFront) {
195
+ (simpleBootFront.option.window as any).document.querySelectorAll('*').forEach((el:HTMLElement )=> {
196
+ Array.from(el.attributes).forEach(attr => {
197
+ if (/^dr-/.test(attr.name) || /^domstyle/.test(attr.name) || /this-path/.test(attr.name)) {
198
+ el.removeAttribute(attr.name);
199
+ }
200
+ });
201
+ });
202
+ let html = simpleBootFront.option.window.document.documentElement.outerHTML;
203
+ if (!/^<!DOCTYPE html>/i.test(html)) {
204
+ html = '<!DOCTYPE html>' + html;
205
+ }
206
+ html = html.replace(/\$\{[\s\S]*?\}\$/g, '');
207
+ const serverSideData = (simpleBootFront.option.window as any).server_side_data;
208
+ if (serverSideData) {
209
+ const data = Object.entries(serverSideData)
210
+ .map(([k, v]) => {
211
+ if (typeof v === 'string') {
212
+ return `window.server_side_data.${k} = ${v}`;
213
+ } else {
214
+ return `window.server_side_data.${k} = ${JSON.stringify(v)}`;
215
+ }
216
+ })
217
+ .join(';');
218
+ if (data) {
219
+ html = html.replace('</head>', `<script> window.server_side_data={}; ${data}; </script></head>`);
220
+ }
221
+ }
222
+ return html;
223
+ }
224
+ async writeOkHtmlAndEnd({ rr, status = HttpStatus.Ok }: { rr: RequestResponse; status?: HttpStatus }, html: string) {
225
+ rr.resStatusCode(status);
226
+ rr.resSetHeader(HttpHeaders.ContentType, Mimes.TextHtml);
227
+ await rr.resEnd(html);
228
+ }
229
+
230
+ async proceedAfter({ rr, app }: { rr: RequestResponse; app: SimpleBootHttpServer; carrier: Map<string, any> }) {
231
+ return true;
232
+ }
233
+ }
@@ -172,8 +172,8 @@ export class SSRFilter implements Filter {
172
172
  simpleBootFront.routingSubjectObservable.pipe(
173
173
  // @ts-ignore
174
174
  filter(
175
- (it: any) =>
176
- it.state === 'end' &&
175
+ (it) =>
176
+ it.triggerPoint === 'end' &&
177
177
  typeof it.routerModule.intent.uri === 'string' &&
178
178
  targetUrl.endsWith(it.routerModule.intent.uri)
179
179
  ),
@@ -0,0 +1,227 @@
1
+ import { RequestResponse } from '@dooboostore/simple-boot-http-server/models/RequestResponse';
2
+ import { HttpHeaders } from '@dooboostore/simple-boot-http-server/codes/HttpHeaders';
3
+ import { SimpleBootHttpSSRFactory } from '../SimpleBootHttpSSRFactory';
4
+ import { ConstructorType } from '@dooboostore/core/types';
5
+ import { JsdomInitializer } from '../initializers/JsdomInitializer';
6
+ import { Filter } from '@dooboostore/simple-boot-http-server/filters/Filter';
7
+ import { Mimes } from '@dooboostore/simple-boot-http-server/codes/Mimes';
8
+ import { HttpStatus } from '@dooboostore/simple-boot-http-server/codes/HttpStatus';
9
+ import { SimpleBootHttpServer } from '@dooboostore/simple-boot-http-server/SimpleBootHttpServer';
10
+ import { SimFrontOption } from '@dooboostore/simple-boot-front/option/SimFrontOption';
11
+ import { SimpleBootFront } from '@dooboostore/simple-boot-front/SimpleBootFront';
12
+ import { AsyncBlockingQueue } from '@dooboostore/core/queues/AsyncBlockingQueue';
13
+ import { RandomUtils } from '@dooboostore/core/random/RandomUtils';
14
+ import * as JSDOM from 'jsdom';
15
+ import { NotFoundError } from '@dooboostore/simple-boot-http-server/errors/NotFoundError';
16
+ import { DomRenderProxy } from '@dooboostore/dom-render/DomRenderProxy';
17
+ import { filter } from '@dooboostore/core/message/operators/filter';
18
+ import { first } from '@dooboostore/core/message/operators/first';
19
+ import { firstValueFrom } from '@dooboostore/core/message/internal/firstValueFrom';
20
+ import { parseHTML } from 'linkedom';
21
+ import { LinkedomInitializer } from '../initializers/LinkedomInitializer';
22
+
23
+ export type FactoryAndParams = {
24
+ frontDistPath: string;
25
+ frontDistIndexFileName?: string;
26
+ factorySimFrontOption: (window: any) => SimFrontOption;
27
+ factory: SimpleBootHttpSSRFactory;
28
+ poolOption: {
29
+ max: number;
30
+ min: number;
31
+ clearIntervalTime?: number;
32
+ };
33
+ using: ConstructorType<any>[];
34
+ domExcludes?: ConstructorType<any>[];
35
+ ssrExcludeFilter?: (rr: RequestResponse) => boolean;
36
+ simpleBootFront?: {
37
+ notFoundError?: boolean;
38
+ };
39
+ };
40
+
41
+ export class SSRLinkDomDomFilter implements Filter {
42
+ private simpleBootFrontPool: SimpleBootFront[] = [];
43
+ private simpleBootFrontQueue = new AsyncBlockingQueue<SimpleBootFront>();
44
+ // private indexHTML: string;
45
+ private welcomUrl = 'http://localhost';
46
+ private poolGeneration = 0;
47
+ private intervalId?: NodeJS.Timeout;
48
+
49
+ constructor(
50
+ public config: FactoryAndParams,
51
+ public otherInstanceSim?: Map<ConstructorType<any>, any>
52
+ ) {
53
+ config.frontDistIndexFileName = config.frontDistIndexFileName || 'index.html';
54
+ // this.indexHTML = JsdomInitializer.loadFile(this.config.frontDistPath, config.frontDistIndexFileName);
55
+ }
56
+
57
+ async onInit(app: SimpleBootHttpServer) {
58
+ for (let i = 0; i < this.config.poolOption.min; i++) {
59
+ await this.pushQueue();
60
+ }
61
+
62
+ if (this.config.poolOption.clearIntervalTime && this.config.poolOption.clearIntervalTime > 0) {
63
+ this.intervalId = setInterval(() => {
64
+ this.poolGeneration++;
65
+ }, this.config.poolOption.clearIntervalTime);
66
+ }
67
+ }
68
+
69
+ async onDestroy() {
70
+ if (this.intervalId) {
71
+ clearInterval(this.intervalId);
72
+ }
73
+ // this.simpleBootFrontQueue.clear();
74
+ this.simpleBootFrontPool.forEach(front => {
75
+ (front as any).jsdom?.window.close();
76
+ });
77
+ this.simpleBootFrontPool = [];
78
+ }
79
+
80
+ async makePage() {
81
+ return new LinkedomInitializer(
82
+ this.config.frontDistPath,
83
+ this.config.frontDistIndexFileName || 'index.html',
84
+ { url: this.welcomUrl }
85
+ ).run();
86
+ // const jsdom = await new JsdomInitializer(
87
+ // this.config.frontDistPath,
88
+ // this.config.frontDistIndexFileName || 'index.html',
89
+ // { url: this.welcomUrl }
90
+ // ).run();
91
+ // return jsdom;
92
+ }
93
+
94
+ async makeFront(window: Window) {
95
+ const name = RandomUtils.uuid();
96
+ (window as any).ssrUse = false;
97
+ const option = this.config.factorySimFrontOption(DomRenderProxy.final(window));
98
+ const simpleBootFront = await this.config.factory.create(option, this.config.using, this.config.domExcludes);
99
+ simpleBootFront.run(this.otherInstanceSim);
100
+ (simpleBootFront as any).generation = this.poolGeneration;
101
+ return simpleBootFront;
102
+ }
103
+
104
+ enqueueFrontApp(simpleBootFront: SimpleBootFront) {
105
+ // this.simpleBootFrontPool.set(simpleBootFront.option.name!, simpleBootFront);
106
+ this.simpleBootFrontPool.push(simpleBootFront);
107
+ this.simpleBootFrontQueue.enqueue(simpleBootFront);
108
+ }
109
+ async pushQueue() {
110
+ if (this.simpleBootFrontPool.length < this.config.poolOption.max) {
111
+ this.enqueueFrontApp(await this.makeFront(await this.makePage()));
112
+ }
113
+ }
114
+
115
+ async proceedBefore({ rr, app }: { rr: RequestResponse; app: SimpleBootHttpServer; carrier: Map<string, any> }) {
116
+ if (this.config.ssrExcludeFilter?.(rr)) {
117
+ return false;
118
+ }
119
+ if (rr.reqHasAcceptHeader(Mimes.TextHtml) || rr.reqHasAcceptHeader(Mimes.All)) {
120
+ if (this.simpleBootFrontQueue.isEmpty()) {
121
+ await this.pushQueue();
122
+ }
123
+ const simpleBootFront = await this.simpleBootFrontQueue.dequeue();
124
+ try {
125
+ (simpleBootFront.option.window as any).ssrUse = true;
126
+ delete (simpleBootFront.option.window as any).server_side_data;
127
+ const url = rr.reqUrlObj({ host: 'localhost' });
128
+ if (this.config.simpleBootFront?.notFoundError) {
129
+ // intent router check first
130
+ const intent = await simpleBootFront.getIntent(url.pathname);
131
+ // route를 못찾은상태에서 router path까지 안맞으면 404 처리한다. route랑 router랑 다르니깐 헛갈리지말도록
132
+ if (intent.module === undefined && !intent.getRouterPathData(rr.reqUrlPathName)) {
133
+ throw new NotFoundError({ message: `Not Found: ${rr.reqUrlPathName}` });
134
+ }
135
+ }
136
+
137
+ // runRouting!!
138
+ const targetUrl = url.toString();
139
+ await simpleBootFront.goRouting(targetUrl);
140
+ await new Promise(r => setTimeout(r, 0)); // <--중요: 이거 넣어야지 두번불러지는게 없어지는듯? 뭐지 event loop 변경된건가?
141
+
142
+
143
+ // simpleBootFront.routingSubjectObservable.subscribe(it=>{
144
+ // console.log('zzzzzzzzzzzzzzzzzzz', it);
145
+ // if (it.triggerPoint==='end') {
146
+ // console.log('zzz@@', it.routerModule.intent);
147
+ // }
148
+ // })
149
+
150
+ const data = await firstValueFrom(
151
+ // @ts-ignore
152
+ simpleBootFront.routingSubjectObservable.pipe(
153
+ // @ts-ignore
154
+ filter(
155
+ (it) =>
156
+ it.triggerPoint === 'end' &&
157
+ typeof it.routerModule.intent.uri === 'string' &&
158
+ targetUrl.endsWith(it.routerModule.intent.uri)
159
+ ),
160
+ // delay(1000),
161
+ first()
162
+ )
163
+ );
164
+ // console.log('???????done');
165
+ const html = this.makeHTML(simpleBootFront);
166
+ await this.writeOkHtmlAndEnd({ rr }, html);
167
+ } finally {
168
+ (simpleBootFront.option.window as any).ssrUse = false;
169
+ delete (simpleBootFront.option.window as any).server_side_data;
170
+ if ((simpleBootFront as any).generation < this.poolGeneration) {
171
+ // Stale instance, destroy it
172
+ const jsdom = (simpleBootFront as any).jsdom as JSDOM.JSDOM | undefined;
173
+ jsdom?.window.close();
174
+ const index = this.simpleBootFrontPool.indexOf(simpleBootFront);
175
+ if (index > -1) {
176
+ this.simpleBootFrontPool.splice(index, 1);
177
+ }
178
+ this.pushQueue();
179
+ } else {
180
+ // Current instance, return to queue
181
+ this.simpleBootFrontQueue.enqueue(simpleBootFront);
182
+ }
183
+ }
184
+ return false;
185
+ } else {
186
+ return true;
187
+ }
188
+ }
189
+
190
+ makeHTML(simpleBootFront: SimpleBootFront) {
191
+ (simpleBootFront.option.window as any).document.querySelectorAll('*').forEach((el:HTMLElement )=> {
192
+ Array.from(el.attributes).forEach(attr => {
193
+ if (/^dr-/.test(attr.name) || /^domstyle/.test(attr.name) || /this-path/.test(attr.name)) {
194
+ el.removeAttribute(attr.name);
195
+ }
196
+ });
197
+ });
198
+ let html = simpleBootFront.option.window.document.documentElement.outerHTML;
199
+ html = '<!DOCTYPE html>' + html;
200
+ html = html.replace(/\$\{[\s\S]*?\}\$/g, '');
201
+ const serverSideData = (simpleBootFront.option.window as any).server_side_data;
202
+ if (serverSideData) {
203
+ const data = Object.entries(serverSideData)
204
+ .map(([k, v]) => {
205
+ if (typeof v === 'string') {
206
+ return `window.server_side_data.${k} = ${v}`;
207
+ } else {
208
+ return `window.server_side_data.${k} = ${JSON.stringify(v)}`;
209
+ }
210
+ })
211
+ .join(';');
212
+ if (data) {
213
+ html = html.replace('</head>', `<script> window.server_side_data={}; ${data}; </script></head>`);
214
+ }
215
+ }
216
+ return html;
217
+ }
218
+ async writeOkHtmlAndEnd({ rr, status = HttpStatus.Ok }: { rr: RequestResponse; status?: HttpStatus }, html: string) {
219
+ rr.resStatusCode(status);
220
+ rr.resSetHeader(HttpHeaders.ContentType, Mimes.TextHtml);
221
+ await rr.resEnd(html);
222
+ }
223
+
224
+ async proceedAfter({ rr, app }: { rr: RequestResponse; app: SimpleBootHttpServer; carrier: Map<string, any> }) {
225
+ return true;
226
+ }
227
+ }