@dooboostore/simple-boot-http-server-ssr 1.0.17 → 1.0.19
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.
- package/README.md +87 -38
- package/dist/cjs/filters/SSRSimpleWebComponentDomParserFilter.js +49 -1
- package/dist/cjs/filters/SSRSimpleWebComponentDomParserFilter.js.map +2 -2
- package/dist/cjs/filters/SSRSimpleWebComponentFilter.js.map +1 -1
- package/dist/esm/filters/SSRSimpleWebComponentDomParserFilter.js +49 -1
- package/dist/esm/filters/SSRSimpleWebComponentDomParserFilter.js.map +2 -2
- package/dist/esm/filters/SSRSimpleWebComponentFilter.js.map +1 -1
- package/dist/esm-bundle/dooboostore-simple-boot-http-server-ssr.esm.js +51652 -42226
- package/dist/esm-bundle/dooboostore-simple-boot-http-server-ssr.esm.js.map +4 -4
- package/dist/types/filters/SSRSimpleWebComponentDomParserFilter.d.ts +14 -2
- package/dist/types/filters/SSRSimpleWebComponentDomParserFilter.d.ts.map +1 -1
- package/dist/types/filters/SSRSimpleWebComponentFilter.d.ts +1 -1
- package/dist/types/filters/SSRSimpleWebComponentFilter.d.ts.map +1 -1
- package/dist/umd-bundle/dooboostore-simple-boot-http-server-ssr.umd.js +46175 -36748
- package/dist/umd-bundle/dooboostore-simple-boot-http-server-ssr.umd.js.map +4 -4
- package/package.json +12 -12
- package/src/filters/SSRSimpleWebComponentDomParserFilter.ts +80 -4
- package/src/filters/SSRSimpleWebComponentFilter.ts +1 -1
package/README.md
CHANGED
|
@@ -4,27 +4,39 @@
|
|
|
4
4
|
[](https://github.com/dooboostore-develop/packages/actions/workflows/main.yaml)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
7
|
-
`@dooboostore/simple-boot-http-server-ssr` provides a powerful **Server-Side Rendering (SSR)** environment by seamlessly integrating `@dooboostore/simple-boot-front` and `@dooboostore/simple-boot-http-server`.
|
|
7
|
+
`@dooboostore/simple-boot-http-server-ssr` provides a powerful **Server-Side Rendering (SSR)** environment by seamlessly integrating `@dooboostore/simple-boot-front` and `@dooboostore/simple-boot-http-server`.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
It ships **five interchangeable rendering strategies** as separate `Filter`s, so you pick the one that fits your app instead of being locked into one DOM backend:
|
|
10
|
+
|
|
11
|
+
| Filter | DOM backend | Instance reuse | Best for |
|
|
12
|
+
| --- | --- | --- | --- |
|
|
13
|
+
| `SSRFilter` | `jsdom` | Pooled (`min`/`max`, generation-based eviction) | High-traffic apps that want warm instances ready to render |
|
|
14
|
+
| `SSRDomParserFilter` | `@dooboostore/dom-parser` (this monorepo's own lightweight, dependency-free DOM) | Fresh window per request | Lower memory footprint, no jsdom dependency; pairs with the AOP-based data-hydration proxy below |
|
|
15
|
+
| `SSRLinkDomFilter` | `linkedom` | Pooled (`poolOption` `min`/`max`, generation-based eviction) | Alternative lightweight DOM backend |
|
|
16
|
+
| `SSRSimpleWebComponentDomParserFilter` | `@dooboostore/dom-parser` | Fresh window per request | `@dooboostore/simple-web-component` apps without a browser; `intentServices` injection and SWC `@property` hydration (below) |
|
|
17
|
+
| `SSRSimpleWebComponentFilter` | Real headless **Chromium via Playwright** | New browser context per request (browser instance itself is reused) | Pixel/spec-perfect rendering (declarative Shadow DOM, real browser APIs) for `@dooboostore/simple-web-component` apps, at the cost of spinning up a real browser |
|
|
18
|
+
|
|
19
|
+
All of them execute your existing frontend code (routing, components, services) on the server and return fully rendered HTML — no separate server-only rendering path to maintain.
|
|
10
20
|
|
|
11
21
|
---
|
|
12
22
|
|
|
13
23
|
## 🚀 Key Features
|
|
14
24
|
|
|
15
25
|
- **Seamless SSR**: Render your SPA on the server without code modifications to maximize SEO and First Contentful Paint (FCP).
|
|
16
|
-
- **
|
|
17
|
-
- **Component Pooling
|
|
18
|
-
- **Automatic Data Hydration**:
|
|
26
|
+
- **Pluggable DOM backend**: Choose `jsdom`, this monorepo's own `@dooboostore/dom-parser`, `linkedom`, or a real headless Chromium (Playwright) — swap the `Filter` without touching your app code.
|
|
27
|
+
- **Component/Instance Pooling** (`SSRFilter`): Optimize resource management by pooling `jsdom`-backed SSR instances (with `min`/`max` pool size and generation-based stale eviction) to handle concurrent requests efficiently.
|
|
28
|
+
- **Automatic Data Hydration**: `SSRDomParserFilterDataHydrationProxy` transparently wraps `@Sim`-registered service methods — on the server it caches the method's result keyed by `symbol.method(args)`, embeds it in the HTML, and on the client it's read back instead of re-fetching, so hydration requires no manual wiring in your services.
|
|
19
29
|
- **Isomorphic Architecture**: Maintain a single codebase for routing, services, and components shared between server and client.
|
|
20
|
-
- **SWC
|
|
30
|
+
- **SWC-native rendering**: `SSRSimpleWebComponentFilter` drives a real Chromium instance via Playwright so `@dooboostore/simple-web-component` apps (declarative Shadow DOM, native Custom Elements) render exactly as they would in a real browser.
|
|
21
31
|
|
|
22
32
|
---
|
|
23
33
|
|
|
24
34
|
## 📦 Installation
|
|
25
35
|
|
|
26
36
|
```bash
|
|
27
|
-
pnpm add @dooboostore/simple-boot-http-server-ssr
|
|
37
|
+
pnpm add @dooboostore/simple-boot-http-server-ssr reflect-metadata
|
|
38
|
+
# jsdom, linkedom, and playwright are regular dependencies; for SSRSimpleWebComponentFilter also run:
|
|
39
|
+
npx playwright install chromium
|
|
28
40
|
```
|
|
29
41
|
|
|
30
42
|
---
|
|
@@ -32,45 +44,68 @@ pnpm add @dooboostore/simple-boot-http-server-ssr @dooboostore/simple-web-compon
|
|
|
32
44
|
## 💻 Core Usage
|
|
33
45
|
|
|
34
46
|
### 1. Universal Bootfactory (bootfactory.ts)
|
|
35
|
-
Define a shared function to initialize the
|
|
47
|
+
Define a shared function to initialize the SWC app on both server and client.
|
|
36
48
|
|
|
37
49
|
```typescript
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
return appElement;
|
|
50
|
+
import swcRegister, { type SwcAppInterface } from '@dooboostore/simple-web-component';
|
|
51
|
+
|
|
52
|
+
export default async (window: Window, otherInstanceSim?: Map<symbol, any>, urlPath?: string) => {
|
|
53
|
+
// Register the built-in SWC elements (swc-app, ...) on this window
|
|
54
|
+
await swcRegister(window);
|
|
55
|
+
|
|
56
|
+
const app = window.document.querySelector('#app') as SwcAppInterface;
|
|
57
|
+
await app.connect({
|
|
58
|
+
window,
|
|
59
|
+
path: urlPath ?? '/',
|
|
60
|
+
routeType: 'path',
|
|
61
|
+
otherInstanceSim, // e.g. the `sim` Map from registerComponents
|
|
62
|
+
onStartedLazyDefineComponent: [/* component/page factories */]
|
|
63
|
+
});
|
|
64
|
+
return { app };
|
|
55
65
|
};
|
|
56
66
|
```
|
|
57
67
|
|
|
58
68
|
### 2. Backend Configuration (SSR Filter)
|
|
59
|
-
Add
|
|
69
|
+
Add one of the SSR filters to your server configuration. Example using `SSRSimpleWebComponentDomParserFilter`:
|
|
60
70
|
|
|
61
71
|
```typescript
|
|
62
|
-
const
|
|
72
|
+
const ssrFilter = new SSRSimpleWebComponentDomParserFilter({
|
|
63
73
|
frontDistPath: './dist-client',
|
|
64
74
|
frontDistIndexFileName: 'index.html',
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
// Services exposed to SSR: each request gets them with its own `rr` injected, passed as `sim`
|
|
76
|
+
intentServices: [/* SimConfig items with a symbol */],
|
|
77
|
+
registerComponents: async (window: any, rr: RequestResponse, sim: Map<symbol, any>) => {
|
|
78
|
+
const { app } = await bootfactory(window, sim, UrlUtils.getUrlPath(window.location));
|
|
79
|
+
return app; // returning the SwcAppInterface enables @property hydration
|
|
80
|
+
},
|
|
81
|
+
ssrExcludeFilter: rr => /\.(js|css|map|ico|png|json)$/.test(rr.reqUrlPathName)
|
|
69
82
|
});
|
|
70
83
|
|
|
71
84
|
const server = new SimpleBootHttpSSRServer(
|
|
72
85
|
new HttpSSRServerOption({
|
|
73
|
-
|
|
86
|
+
listen: { port: 8080 },
|
|
87
|
+
filters: [new ResourceFilter('./dist-client', ['/bundle.js', /\.map$/]), ssrFilter]
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
server.run();
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`SSRSimpleWebComponentFilter` (real Chromium via Playwright) takes the same `frontDistPath` / `frontDistIndexFileName` / `ssrExcludeFilter` plus `playwright: { waitUntil, waitForSelector, waitForTimeout, timeout, ignoreTags }`. Playwright loads your real bundle, so Custom Elements register themselves and `registerComponents` is usually unnecessary.
|
|
94
|
+
|
|
95
|
+
For `@dooboostore/simple-boot-front` apps, use `SSRDomParserFilter` (or `SSRFilter` for `jsdom` / `SSRLinkDomFilter` for `linkedom`, both pooled via `poolOption: { min, max, clearIntervalTime? }`) — same `Filter` interface:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
const domParserFilter = new SSRDomParserFilter({
|
|
99
|
+
frontDistPath: './dist-client',
|
|
100
|
+
frontDistIndexFileName: 'index.html',
|
|
101
|
+
factorySimFrontOption: (window) => new SimFrontOption({ window /* ... */ }),
|
|
102
|
+
factory: mySimpleBootHttpSSRFactory,
|
|
103
|
+
using: [/* components/pages to register */]
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const server = new SimpleBootHttpSSRServer(
|
|
107
|
+
new HttpSSRServerOption({
|
|
108
|
+
filters: [new ResourceFilter('./dist-client'), domParserFilter]
|
|
74
109
|
})
|
|
75
110
|
);
|
|
76
111
|
server.run();
|
|
@@ -79,18 +114,32 @@ server.run();
|
|
|
79
114
|
---
|
|
80
115
|
|
|
81
116
|
## 🌊 Data Hydration
|
|
82
|
-
|
|
117
|
+
`SSRDomParserFilter` pairs with `SSRDomParserFilterDataHydrationProxy` (`@Sim`-injected, wraps `@Sim`-registered service methods) to automate transferring async data from server to client with no manual wiring in your services:
|
|
118
|
+
|
|
119
|
+
1. **Server Side**: The first call to a wrapped service method runs normally, then its result is cached via `simpleBootFront.saveDataHydration(key, data)` (`key` = `<service symbol>.<method>(<args>)`) and serialized into the HTML.
|
|
120
|
+
2. **Client Side**: On hydration, the same call is intercepted and `simpleBootFront.cutDataHydration(key)` returns the cached value instead of re-running the (async) call.
|
|
121
|
+
3. **Result**: The client shows the exact data the server already rendered, with no redundant fetch.
|
|
122
|
+
|
|
123
|
+
`SSRFilter`/`SSRWorker` (the `jsdom`-based filters) use a simpler, manual mechanism instead: whatever you assign onto `window.server_side_data` on the server is serialized as an inline `<script>` and available on `window.server_side_data` on the client.
|
|
124
|
+
|
|
125
|
+
### SWC `@property` hydration (`SSRSimpleWebComponentDomParserFilter`)
|
|
83
126
|
|
|
84
|
-
1.
|
|
85
|
-
2.
|
|
86
|
-
|
|
127
|
+
1. `registerComponents` returns the `SwcAppInterface` (host + `connectedElements()`).
|
|
128
|
+
2. `@property` metadata is collected from the host and connected elements into
|
|
129
|
+
`{ sel, prop, value }` items (`sel` = `[swc-use-ssr="<id>"]`, JSON-serializable values only).
|
|
130
|
+
3. Embedded as a `<script>` that runs before the bundle: finds each node with
|
|
131
|
+
`querySelector` and sets `el[prop] = value`.
|
|
132
|
+
4. The JSON is escaped for inline scripts (`<` → `\u003c`, U+2028/U+2029), so a value
|
|
133
|
+
containing `</script>` can't break out of the tag. `JSON.parse` restores it exactly.
|
|
134
|
+
5. Works because a custom element keeps own properties set **before** `customElements.define` —
|
|
135
|
+
the upgrade doesn't reset them. Declare hydrated fields with `declare` (see the SWC README).
|
|
87
136
|
|
|
88
137
|
---
|
|
89
138
|
|
|
90
139
|
## 📖 Learn More
|
|
91
140
|
Check out the detailed guides and tutorials in the `document` folder.
|
|
92
|
-
- [SSR Basics & JSDOM Usage](https://github.com/dooboostore-develop/packages/tree/main/simple-boot-http-server-ssr/document/Create%20a%20SSR%20Server%20Application%20Framework/02_chapter1_ssr_basics_jsdom.md)
|
|
93
|
-
- [Data Hydration Guide](https://github.com/dooboostore-develop/packages/tree/main/simple-boot-http-server-ssr/document/Create%20a%20SSR%20Server%20Application%20Framework/04_chapter3_data_hydration.md)
|
|
141
|
+
- [SSR Basics & JSDOM Usage](https://github.com/dooboostore-develop/packages/tree/main/@dooboostore/simple-boot-http-server-ssr/document/Create%20a%20SSR%20Server%20Application%20Framework/02_chapter1_ssr_basics_jsdom.md)
|
|
142
|
+
- [Data Hydration Guide](https://github.com/dooboostore-develop/packages/tree/main/@dooboostore/simple-boot-http-server-ssr/document/Create%20a%20SSR%20Server%20Application%20Framework/04_chapter3_data_hydration.md)
|
|
94
143
|
|
|
95
144
|
---
|
|
96
145
|
|
|
@@ -23,7 +23,9 @@ module.exports = __toCommonJS(SSRSimpleWebComponentDomParserFilter_exports);
|
|
|
23
23
|
var import_HttpHeaders = require("@dooboostore/simple-boot-http-server/codes/HttpHeaders");
|
|
24
24
|
var import_Mimes = require("@dooboostore/simple-boot-http-server/codes/Mimes");
|
|
25
25
|
var import_HttpStatus = require("@dooboostore/simple-boot-http-server/codes/HttpStatus");
|
|
26
|
+
var import_RequestResponseInjectProxy = require("@dooboostore/simple-boot-http-server/proxy/RequestResponseInjectProxy");
|
|
26
27
|
var import_DomParserInitializer = require("../initializers/DomParserInitializer");
|
|
28
|
+
var import_simple_web_component = require("@dooboostore/simple-web-component");
|
|
27
29
|
class SSRSimpleWebComponentDomParserFilter {
|
|
28
30
|
constructor(config) {
|
|
29
31
|
this.config = config;
|
|
@@ -31,6 +33,7 @@ class SSRSimpleWebComponentDomParserFilter {
|
|
|
31
33
|
this.welcomUrl = config.welcomUrl || this.welcomUrl;
|
|
32
34
|
}
|
|
33
35
|
async onInit(app) {
|
|
36
|
+
this.app = app;
|
|
34
37
|
}
|
|
35
38
|
async onDestroy() {
|
|
36
39
|
}
|
|
@@ -45,8 +48,53 @@ class SSRSimpleWebComponentDomParserFilter {
|
|
|
45
48
|
const domParserInitializer = new import_DomParserInitializer.DomParserInitializer(this.config.frontDistPath, this.config.frontDistIndexFileName || "index.html", { url: targetUrl });
|
|
46
49
|
const window = await domParserInitializer.run();
|
|
47
50
|
try {
|
|
51
|
+
let app;
|
|
48
52
|
if (this.config.registerComponents) {
|
|
49
|
-
|
|
53
|
+
const sim = /* @__PURE__ */ new Map();
|
|
54
|
+
for (const service of (this.config.intentServices ?? []).filter(Boolean)) {
|
|
55
|
+
if (!service.symbol) continue;
|
|
56
|
+
for (const sym of Array.isArray(service.symbol) ? service.symbol : [service.symbol]) {
|
|
57
|
+
const raw = this.app?.sim(sym);
|
|
58
|
+
if (raw) sim.set(sym, (0, import_RequestResponseInjectProxy.injectRequestResponse)(raw, rr));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
app = await this.config.registerComponents(window, rr, sim);
|
|
62
|
+
}
|
|
63
|
+
if (app) {
|
|
64
|
+
const items = [];
|
|
65
|
+
const targets = [app, ...(app.connectedElements?.() ?? []).filter((el) => el !== app)];
|
|
66
|
+
for (const el of targets) {
|
|
67
|
+
const swcId = el._swcId;
|
|
68
|
+
if (!swcId) continue;
|
|
69
|
+
const sel = `[swc-use-ssr="${swcId}"]`;
|
|
70
|
+
for (const m of (0, import_simple_web_component.findAllPropertyMetadata)(el)) {
|
|
71
|
+
let value;
|
|
72
|
+
try {
|
|
73
|
+
value = el[m.propertyKey];
|
|
74
|
+
} catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (value === void 0 || typeof value === "function") continue;
|
|
78
|
+
try {
|
|
79
|
+
JSON.stringify(value);
|
|
80
|
+
} catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
items.push({ sel, prop: String(m.propertyKey), value });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (items.length > 0) {
|
|
87
|
+
const script = window.document.createElement("script");
|
|
88
|
+
const hydrationJson = JSON.stringify(items).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
89
|
+
script.textContent = ` window.__swc_hydration=${hydrationJson};for(const h of window.__swc_hydration){
|
|
90
|
+
const el=document.querySelector(h.sel);
|
|
91
|
+
if(el){
|
|
92
|
+
el[h.prop]=h.value;
|
|
93
|
+
}
|
|
94
|
+
}`;
|
|
95
|
+
window.document.body.appendChild(script);
|
|
96
|
+
console.log(`[ssr-hydration] ${items.length} props embedded`);
|
|
97
|
+
}
|
|
50
98
|
}
|
|
51
99
|
const html = this.makeHTML(window);
|
|
52
100
|
await this.writeOkHtmlAndEnd({ rr }, html);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/filters/SSRSimpleWebComponentDomParserFilter.ts"],
|
|
4
|
-
"sourcesContent": ["import {RequestResponse} from '@dooboostore/simple-boot-http-server/models/RequestResponse';\nimport {HttpHeaders} from '@dooboostore/simple-boot-http-server/codes/HttpHeaders';\nimport {Filter} from '@dooboostore/simple-boot-http-server/filters/Filter';\nimport {Mimes} from '@dooboostore/simple-boot-http-server/codes/Mimes';\nimport {HttpStatus} from '@dooboostore/simple-boot-http-server/codes/HttpStatus';\nimport {SimpleBootHttpServer} from '@dooboostore/simple-boot-http-server/SimpleBootHttpServer';\nimport {DomParserInitializer} from '../initializers/DomParserInitializer';\n\nexport type SWCSSRDomParserConfig = {\n frontDistPath: string;\n frontDistIndexFileName?: string;\n welcomUrl?: string;\n ssrExcludeFilter?: (rr: RequestResponse) => boolean;\n /**\n *
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,yBAA0B;AAE1B,mBAAoB;AACpB,wBAAyB;AAEzB,kCAAmC;
|
|
4
|
+
"sourcesContent": ["import {RequestResponse} from '@dooboostore/simple-boot-http-server/models/RequestResponse';\nimport {HttpHeaders} from '@dooboostore/simple-boot-http-server/codes/HttpHeaders';\nimport {Filter} from '@dooboostore/simple-boot-http-server/filters/Filter';\nimport {Mimes} from '@dooboostore/simple-boot-http-server/codes/Mimes';\nimport {HttpStatus} from '@dooboostore/simple-boot-http-server/codes/HttpStatus';\nimport {SimpleBootHttpServer} from '@dooboostore/simple-boot-http-server/SimpleBootHttpServer';\nimport {injectRequestResponse} from '@dooboostore/simple-boot-http-server/proxy/RequestResponseInjectProxy';\nimport {SimConfig} from '@dooboostore/simple-boot';\nimport {DomParserInitializer} from '../initializers/DomParserInitializer';\nimport type { SwcAppInterface } from '@dooboostore/simple-web-component';\nimport { findAllPropertyMetadata } from '@dooboostore/simple-web-component';\n\nexport type SWCSSRDomParserConfig = {\n frontDistPath: string;\n frontDistIndexFileName?: string;\n welcomUrl?: string;\n ssrExcludeFilter?: (rr: RequestResponse) => boolean;\n /**\n * SSR \uC5D0 \uB178\uCD9C\uD560 \uBC31\uC5D4\uB4DC \uC11C\uBE44\uC2A4 \uBAA9\uB85D(\uAC01 \uD56D\uBAA9\uC740 symbol \uC744 \uAC00\uC9C4\uB2E4, \uC608: pairServices).\n * \uD504\uB808\uC784\uC6CC\uD06C\uAC00 \uC694\uCCAD\uB9C8\uB2E4 \uAC01 \uC11C\uBE44\uC2A4\uB97C \uAEBC\uB0B4 \uC774 \uC694\uCCAD\uC758 rr \uC744 \uC8FC\uC785\uD55C \uB4A4 Map \uC73C\uB85C \uBB36\uC5B4\n * registerComponents \uC758 3\uBC88\uC9F8 \uC778\uC790(sim)\uB85C \uB118\uAE34\uB2E4. \u2192 \uC571\uC740 \uADF8 Map \uC744 bootfactory \uC5D0 \uADF8\uB300\uB85C \uC804\uB2EC\uB9CC \uD558\uBA74 \uB418\uACE0\n * \uC9C1\uC811 Proxy \uB85C \uAC10\uC2F8\uAC70\uB098 rr \uC744 \uB123\uC744 \uD544\uC694\uAC00 \uC5C6\uB2E4(\uCFE0\uD0A4 \uB4F1 \uC694\uCCAD \uCEE8\uD14D\uC2A4\uD2B8 \uC790\uB3D9 \uC804\uB2EC).\n */\n intentServices?: SimConfig[];\n /**\n * \uC694\uCCAD\uB9C8\uB2E4 \uCEF4\uD3EC\uB10C\uD2B8\uB97C \uB4F1\uB85D\uD55C\uB2E4. \uC571 bootfactory\uB97C \uADF8\uB300\uB85C \uB118\uAE30\uBA74 \uB41C\uB2E4.\n * @param sim intentServices \uB97C rr \uC8FC\uC785\uD574 \uBB36\uC740 Map<symbol, service>. bootfactory \uC5D0 \uADF8\uB300\uB85C \uB118\uAE30\uBA74 \uB41C\uB2E4.\n * @returns \uCD08\uAE30\uD654\uB41C SwcApp (\uD558\uC774\uB4DC\uB808\uC774\uC158 \uB370\uC774\uD130 \uBC15\uC81C \uB4F1\uC5D0 \uC0AC\uC6A9)\n */\n registerComponents?: (window: any, rr: RequestResponse, sim: Map<symbol, any>) => Promise<SwcAppInterface | void> | SwcAppInterface | void;\n};\n\n/**\n * SSR Filter specifically for Simple Web Component (SWC).\n * It utilizes @dooboostore/dom-parser for rendering and DSD support.\n */\nexport class SSRSimpleWebComponentDomParserFilter implements Filter {\n private welcomUrl = 'http://localhost';\n private app?: SimpleBootHttpServer;\n\n constructor(public config: SWCSSRDomParserConfig) {\n this.welcomUrl = config.welcomUrl || this.welcomUrl;\n }\n\n async onInit(app: SimpleBootHttpServer) {\n this.app = app;\n }\n\n async onDestroy() {}\n\n async proceedBefore({ rr }: { rr: RequestResponse; app: SimpleBootHttpServer; carrier: Map<string, any> }) {\n if (this.config.ssrExcludeFilter?.(rr)) {\n return false;\n }\n\n if (rr.reqHasAcceptHeader(Mimes.TextHtml) || rr.reqHasAcceptHeader(Mimes.All)) {\n const url = rr.reqUrlObj({ host: 'localhost' });\n const targetUrl = url.toString() ?? this.welcomUrl;\n console.log('SSRSimpleWebComponentDomParserFilter start');\n\n // 1. Initialize Virtual DOM Environment\n const domParserInitializer = new DomParserInitializer(this.config.frontDistPath, this.config.frontDistIndexFileName || 'index.html', { url: targetUrl });\n const window = await domParserInitializer.run();\n\n // web component \uACBD\uC6B0 \uC790\uAE30 tagName\uC744 \uC0DD\uC131\uC790 \uC5D0\uAC8C HTMLElementBase\uC5D0\uB118\uACA8\uC918\uC57C\uB418\uAE30\uB584\uBB38\uC5D0\n // const getTagName = (type: ConstructorType<any>) => {\n // const zz = getElementConfig(type);\n // return zz.name;\n // };\n // (window as any).HTMLElement = class extends HTMLElementBase {\n // constructor(...args: any[]) {\n // const ctor = new.target as any; // \uC774\uB7F0 \uC288\uAC00 \uAE30\uB2A5\uC774...\n // const resolvedTagName = getTagName(ctor);\n // super(resolvedTagName, args[0]);\n // }\n // }\n\n\n try {\n // 2. Register Components if provided\n // We use SwcApplication inside the callback or directly here.\n let app: SwcAppInterface | void;\n if (this.config.registerComponents) {\n // intentServices \uB97C rr \uC8FC\uC785\uD574 Map \uC73C\uB85C \uBB36\uC5B4 \uB118\uAE34\uB2E4 (\uC571\uC740 bootfactory \uC5D0 \uADF8\uB300\uB85C \uC804\uB2EC\uB9CC).\n const sim = new Map<symbol, any>();\n for (const service of (this.config.intentServices ?? []).filter(Boolean)) {\n if (!service.symbol) continue;\n for (const sym of (Array.isArray(service.symbol) ? service.symbol : [service.symbol])) {\n const raw = this.app?.sim<any>(sym as any);\n if (raw) sim.set(sym, injectRequestResponse(raw, rr));\n }\n }\n app = await this.config.registerComponents(window, rr, sim);\n }\n\n // [\uD558\uC774\uB4DC\uB808\uC774\uC158] swcApp \uBCF8\uCCB4 + \uC5F0\uACB0 \uC5D8\uB9AC\uBA3C\uD2B8\uC758 @property \uAC12\uC744 script\uB85C \uBC15\uC81C.\n // \uD074\uB77C\uC5D0\uC11C \uBC88\uB4E4\uBCF4\uB2E4 \uBA3C\uC800 \uC2E4\uD589 \u2192 querySelector\uB85C \uCC3E\uC544 .prop = \uAC12 \uC138\uD305 (\uB124\uC774\uD2F0\uBE0C\n // \uC5C5\uADF8\uB808\uC774\uB4DC\uB294 own property \uC720\uC9C0\uB77C upgrade \uD6C4\uC5D0\uB3C4 \uAC12 \uC0B4\uC544\uC788\uC74C).\n if (app) {\n const items: Array<{ sel: string; prop: string; value: unknown }> = [];\n const targets: any[] = [app, ...(app.connectedElements?.() ?? []).filter((el: any) => el !== app)];\n for (const el of targets) {\n const swcId = (el as any)._swcId;\n if (!swcId) continue;\n const sel = `[swc-use-ssr=\"${swcId}\"]`;\n for (const m of findAllPropertyMetadata(el)) {\n let value: unknown;\n try {\n value = el[m.propertyKey];\n } catch {\n continue;\n }\n if (value === undefined || typeof value === 'function') continue;\n try {\n JSON.stringify(value);\n } catch {\n continue;\n }\n items.push({ sel, prop: String(m.propertyKey), value });\n }\n }\n if (items.length > 0) {\n // \uC5C5\uADF8\uB808\uC774\uB4DC \uB54C \uB178\uB4DC\uAC00 \uAC08\uC544\uC5CE\uC5B4\uC9C0\uBA74 JS \uD504\uB85C\uD37C\uD2F0\uB294 \uB0A0\uC544\uAC10. attribute\uB294 \uBCF5\uC0AC\uB418\uB2C8\n // data-hyd-* \uB85C \uBC15\uACE0, \uAC19\uC740 \uB178\uB4DC\uBA74 \uD504\uB85C\uD37C\uD2F0\uB3C4 \uBC14\uB85C \uC138\uD305.\n const script = window.document.createElement('script');\n // <script> \uB0B4\uC6A9\uC740 \uC774\uC2A4\uCF00\uC774\uD504 \uC5C6\uC774 \uC9C1\uB82C\uD654\uB41C\uB2E4 \u2192 \uAC12\uC5D0 '</script>'\uAC00 \uC788\uC73C\uBA74 HTML \uD30C\uC11C\uAC00 \uAC70\uAE30\uC11C \uD0DC\uADF8\uB97C \uB2EB\uC544 XSS.\n // JSON.stringify\uB294 '<'\uB97C \uC774\uC2A4\uCF00\uC774\uD504\uD558\uC9C0 \uC54A\uC73C\uBBC0\uB85C '\\u003c'\uB85C \uBC14\uAFBC\uB2E4 (JS\uC5D0\uC120 \uAC19\uC740 \uBB38\uC790\uC5F4).\n // U+2028/2029\uB294 \uAD6C\uD615 JS \uC5D4\uC9C4\uC5D0\uC11C \uBB38\uC790\uC5F4 \uB9AC\uD130\uB7F4 \uC548\uC758 \uC904\uBC14\uAFC8\uC73C\uB85C \uD574\uC11D\uB3FC \uBB38\uBC95 \uC624\uB958\uAC00 \uB098\uBBC0\uB85C \uD568\uAED8 \uC774\uC2A4\uCF00\uC774\uD504.\n const hydrationJson = JSON.stringify(items)\n .replace(/</g, '\\\\u003c')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n script.textContent =\n ` window.__swc_hydration=${hydrationJson};` +\n `for(const h of window.__swc_hydration){\n const el=document.querySelector(h.sel);\n if(el){\n el[h.prop]=h.value;\n }\n }`;\n window.document.body.appendChild(script);\n console.log(`[ssr-hydration] ${items.length} props embedded`);\n }\n }\n\n // window.document.querySelector('.sidebar-space').innerHTML = '\uC528\uBC1C\uB188\uC544.';\n // await new Promise<void>((resolve, reject) => setTimeout(resolve, 100));\n // await new Promise<void>((resolve, reject) => setTimeout(resolve, 5000));\n // window.document.body.innerHTML = '';\n // window.document.body.setAttribute('ssr-use', 'true');\n // 3. Generate Final HTML\n const html = this.makeHTML(window);\n // console.log('html: \uD83D\uDC88', html);\n await this.writeOkHtmlAndEnd({ rr }, html);\n } finally {\n // 4. Cleanup\n window.close();\n domParserInitializer.destroy();\n }\n return false;\n }\n return true;\n }\n\n makeHTML(window: any) {\n let html = window.document.documentElement.outerHTML;\n if (!/^<!DOCTYPE html>/i.test(html)) {\n html = '<!DOCTYPE html>\\n' + html;\n }\n return html;\n }\n\n async writeOkHtmlAndEnd({ rr, status = HttpStatus.Ok }: { rr: RequestResponse; status?: HttpStatus }, html: string) {\n rr.resStatusCode(status);\n rr.resSetHeader(HttpHeaders.ContentType, Mimes.TextHtml);\n await rr.resEnd(html);\n }\n\n async proceedAfter() {\n return true;\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,yBAA0B;AAE1B,mBAAoB;AACpB,wBAAyB;AAEzB,wCAAoC;AAEpC,kCAAmC;AAEnC,kCAAwC;AA0BjC,MAAM,qCAAuD;AAAA,EAIlE,YAAmB,QAA+B;AAA/B;AAHnB,SAAQ,YAAY;AAIlB,SAAK,YAAY,OAAO,aAAa,KAAK;AAAA,EAC5C;AAAA,EAEA,MAAM,OAAO,KAA2B;AACtC,SAAK,MAAM;AAAA,EACb;AAAA,EAEA,MAAM,YAAY;AAAA,EAAC;AAAA,EAEnB,MAAM,cAAc,EAAE,GAAG,GAAkF;AACzG,QAAI,KAAK,OAAO,mBAAmB,EAAE,GAAG;AACtC,aAAO;AAAA,IACT;AAEA,QAAI,GAAG,mBAAmB,mBAAM,QAAQ,KAAK,GAAG,mBAAmB,mBAAM,GAAG,GAAG;AAC7E,YAAM,MAAM,GAAG,UAAU,EAAE,MAAM,YAAY,CAAC;AAC9C,YAAM,YAAY,IAAI,SAAS,KAAK,KAAK;AACzC,cAAQ,IAAI,4CAA4C;AAGxD,YAAM,uBAAuB,IAAI,iDAAqB,KAAK,OAAO,eAAe,KAAK,OAAO,0BAA0B,cAAc,EAAE,KAAK,UAAU,CAAC;AACvJ,YAAM,SAAS,MAAM,qBAAqB,IAAI;AAgB9C,UAAI;AAGF,YAAI;AACJ,YAAI,KAAK,OAAO,oBAAoB;AAElC,gBAAM,MAAM,oBAAI,IAAiB;AACjC,qBAAW,YAAY,KAAK,OAAO,kBAAkB,CAAC,GAAG,OAAO,OAAO,GAAG;AACxE,gBAAI,CAAC,QAAQ,OAAQ;AACrB,uBAAW,OAAQ,MAAM,QAAQ,QAAQ,MAAM,IAAI,QAAQ,SAAS,CAAC,QAAQ,MAAM,GAAI;AACrF,oBAAM,MAAM,KAAK,KAAK,IAAS,GAAU;AACzC,kBAAI,IAAK,KAAI,IAAI,SAAK,yDAAsB,KAAK,EAAE,CAAC;AAAA,YACtD;AAAA,UACF;AACA,gBAAM,MAAM,KAAK,OAAO,mBAAmB,QAAQ,IAAI,GAAG;AAAA,QAC5D;AAKA,YAAI,KAAK;AACP,gBAAM,QAA8D,CAAC;AACrE,gBAAM,UAAiB,CAAC,KAAK,IAAI,IAAI,oBAAoB,KAAK,CAAC,GAAG,OAAO,CAAC,OAAY,OAAO,GAAG,CAAC;AACjG,qBAAW,MAAM,SAAS;AACxB,kBAAM,QAAS,GAAW;AAC1B,gBAAI,CAAC,MAAO;AACZ,kBAAM,MAAM,iBAAiB,KAAK;AAClC,uBAAW,SAAK,qDAAwB,EAAE,GAAG;AAC3C,kBAAI;AACJ,kBAAI;AACF,wBAAQ,GAAG,EAAE,WAAW;AAAA,cAC1B,QAAQ;AACN;AAAA,cACF;AACA,kBAAI,UAAU,UAAa,OAAO,UAAU,WAAY;AACxD,kBAAI;AACF,qBAAK,UAAU,KAAK;AAAA,cACtB,QAAQ;AACN;AAAA,cACF;AACA,oBAAM,KAAK,EAAE,KAAK,MAAM,OAAO,EAAE,WAAW,GAAG,MAAM,CAAC;AAAA,YACxD;AAAA,UACF;AACA,cAAI,MAAM,SAAS,GAAG;AAGpB,kBAAM,SAAS,OAAO,SAAS,cAAc,QAAQ;AAIrD,kBAAM,gBAAgB,KAAK,UAAU,KAAK,EACvC,QAAQ,MAAM,SAAS,EACvB,QAAQ,WAAW,SAAS,EAC5B,QAAQ,WAAW,SAAS;AAC/B,mBAAO,cACL,2BAA2B,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAO1C,mBAAO,SAAS,KAAK,YAAY,MAAM;AACvC,oBAAQ,IAAI,mBAAmB,MAAM,MAAM,iBAAiB;AAAA,UAC9D;AAAA,QACF;AAQA,cAAM,OAAO,KAAK,SAAS,MAAM;AAEjC,cAAM,KAAK,kBAAkB,EAAE,GAAG,GAAG,IAAI;AAAA,MAC3C,UAAE;AAEA,eAAO,MAAM;AACb,6BAAqB,QAAQ;AAAA,MAC/B;AACA,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA,EAEA,SAAS,QAAa;AACpB,QAAI,OAAO,OAAO,SAAS,gBAAgB;AAC3C,QAAI,CAAC,oBAAoB,KAAK,IAAI,GAAG;AACnC,aAAO,sBAAsB;AAAA,IAC/B;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,kBAAkB,EAAE,IAAI,SAAS,6BAAW,GAAG,GAAiD,MAAc;AAClH,OAAG,cAAc,MAAM;AACvB,OAAG,aAAa,+BAAY,aAAa,mBAAM,QAAQ;AACvD,UAAM,GAAG,OAAO,IAAI;AAAA,EACtB;AAAA,EAEA,MAAM,eAAe;AACnB,WAAO;AAAA,EACT;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/filters/SSRSimpleWebComponentFilter.ts"],
|
|
4
|
-
"sourcesContent": ["import { RequestResponse } from '@dooboostore/simple-boot-http-server/models/RequestResponse';\nimport { HttpHeaders } from '@dooboostore/simple-boot-http-server/codes/HttpHeaders';\nimport { Filter } from '@dooboostore/simple-boot-http-server/filters/Filter';\nimport { Mimes } from '@dooboostore/simple-boot-http-server/codes/Mimes';\nimport { HttpStatus } from '@dooboostore/simple-boot-http-server/codes/HttpStatus';\nimport { SimpleBootHttpServer } from '@dooboostore/simple-boot-http-server/SimpleBootHttpServer';\nimport path from 'path';\nimport fs from 'fs';\nimport {HttpMethod} from \"@dooboostore/simple-boot-http-server\";\n\nexport type SWCSSRConfig = {\n frontDistPath: string;\n frontDistIndexFileName?: string;\n welcomUrl?: string;\n ssrExcludeFilter?: (rr: RequestResponse) => boolean;\n /**\n * (Deprecated for Playwright)\n * Function to register components for each request.\n * Playwright evaluates real JS files loaded from your index.html,\n * so component registration happens automatically in the browser context.\n */\n registerComponents?: (window: any) => Promise<void> | void;\n playwright?: {\n waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' | 'commit';\n waitForSelector?: string;\n waitForTimeout?: number;\n timeout?: number;\n ignoreTags?: string[];\n };\n};\n\n/**\n * SSR Filter specifically for Simple Web Component (SWC).\n * It utilizes Playwright to render the page robustly and perfectly matching browser specs.\n *\n * ### Playwright Setup & Requirements\n *\n * To use this filter, Playwright must be installed in your server environment:\n * 1. Install playwright package: `npm install playwright` or `pnpm add playwright`\n * 2. Install browsers: `npx playwright install chromium`\n *\n * If running inside a Docker container (e.g., Alpine Linux), you must use an image that supports Playwright\n * or manually install OS dependencies. Playwright provides official Docker images: `mcr.microsoft.com/playwright:v1.x.x-focal`.\n */\nexport class SSRSimpleWebComponentFilter implements Filter {\n private welcomUrl = 'http://localhost';\n private browser?: any;\n\n constructor(public config: SWCSSRConfig) {\n this.welcomUrl = config.welcomUrl || this.welcomUrl;\n }\n\n async onInit(app: SimpleBootHttpServer) {\n // 1. Initialize Playwright browser when the server starts\n const { chromium } = await import('playwright');\n this.browser = await chromium.launch({\n headless: true,\n args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-web-security'] // Allow CORS for local dev\n });\n console.log('\uD83D\uDE80 Playwright Browser initialized for SSR Filter');\n }\n\n async onDestroy() {\n if (this.browser) {\n await this.browser.close();\n console.log('\uD83D\uDED1 Playwright Browser closed');\n }\n }\n\n async proceedBefore({ rr, app, carrier }: { rr: RequestResponse; app: SimpleBootHttpServer; carrier: Map<string, any> }) {\n if (this.config.ssrExcludeFilter?.(rr)) {\n return true; // Bypass SSR and continue server chain\n }\n\n console.log('playwright2222222', rr.reqHeader('x-ssr-browser'));\n\n // 2. Prevent Infinite Loop:\n // If the request comes from Playwright itself, bypass SSR and serve the original static SPA file.\n if (rr.reqHeader('x-ssr-browser') === 'playwright') {\n return true;\n }\n\n if (rr.reqIsMethod(HttpMethod.GET) && rr.reqHasContentTypeHeader(Mimes.TextHtml)) {\n // Build target URL for Playwright to visit\n // Use the server's actual listening address and port if available\n const protocol = app.option.protocol || 'http';\n const host = app.option.hostname || '127.0.0.1';\n const port = app.option.port || 80;\n\n const url = rr.reqUrlObj({ host: `${host}:${port}` });\n url.protocol = protocol;\n\n const targetUrl = url.toString() ?? this.welcomUrl;\n\n if (!this.browser) {\n console.error('Playwright browser is not initialized. Bypassing SSR.');\n return true;\n }\n\n // Create a fresh context for isolation\n // IMPORTANT: We do NOT use `extraHTTPHeaders` globally because it attaches the header to EVERY request\n // Playwright makes (including CORS requests to FontAwesome or API requests).\n // Instead, we will set the header ONLY for the initial navigation request to our server.\n const context = await this.browser.newContext();\n const page = await context.newPage();\n\n try {\n // Intercept ONLY requests going to our local server and append the bypass header\n await page.route('**/*', route => {\n const request = route.request();\n\n // Only append the x-ssr-browser header for requests to the local server that are NOT API requests\n const isLocal = request.url().startsWith(targetUrl) || request.url().includes('localhost') || request.url().includes('127.0.0.1');\n const isApi = request.url().includes('/api/');\n\n if (isLocal && !isApi) {\n const headers = {\n ...request.headers(),\n 'x-ssr-browser': 'playwright'\n };\n route.continue({ headers }).catch(() => {});\n } else {\n // For external requests (like font-awesome CORS) or API calls, do not add the custom header\n route.continue().catch(() => {});\n }\n });\n\n // 3. Navigate to the page and wait for the framework to finish rendering\n // but normally Playwright just navigates to the targetUrl and loads the real SPA resources.\n\n // \uD83D\uDCA1 Playwright \u2194 Framework \uD1B5\uC2E0 \uC124\uC815\n // \uD504\uB860\uD2B8\uC5D4\uB4DC\uC758 `bootfactory.ts` \uC548\uC758 `onRouteChanged` \uB4F1\uC5D0\uC11C\n // `if (window.onPlaywrightReady) window.onPlaywrightReady()` \uB77C\uACE0 \uD638\uCD9C\uD574\uC8FC\uBA74,\n // \uC544\uB798\uC758 Promise\uAC00 \uC989\uC2DC resolve \uB418\uB3C4\uB85D \uC5F0\uACB0\uD569\uB2C8\uB2E4.\n let resolvePlaywrightReady: () => void;\n const playwrightReadyPromise = new Promise<void>(resolve => {\n resolvePlaywrightReady = resolve;\n });\n\n await page.exposeFunction('onPlaywrightReady', () => {\n console.log('\u26A1\uFE0F [SSR:Playwright] Received \"onPlaywrightReady\" signal from framework!');\n resolvePlaywrightReady();\n });\n\n // 3. Navigate to the page and wait for the framework to finish rendering\n // Since the server will return the `index.html` (due to the bypass header), Playwright will download\n // your main.js / bundle.js, evaluate it, and the Custom Elements will register themselves naturally!\n await page.goto(targetUrl, {\n waitUntil: this.config.playwright?.waitUntil ?? 'networkidle',\n timeout: this.config.playwright?.timeout ?? 15000\n });\n\n // \uD83D\uDCA1 Framework Load Wait (Crucial for SWC)\n // Wait for EITHER the framework to call `window.onPlaywrightReady()` OR a specific innerHTML condition\n await Promise.race([\n playwrightReadyPromise, // \uC9C1\uC811 exposeFunction\uC5D0\uC11C \uD2B8\uB9AC\uAC70\uB41C Promise\n\n page.waitForFunction(\n () => {\n // Fallback 1: If explicit signal variable is set\n return (window as any)._playwrightReadySignalFired === true;\n },\n { timeout: this.config.playwright?.timeout ?? 15000 }\n ),\n\n page.waitForFunction(\n () => {\n // Fallback 2: If no signal, just wait until body has content\n return document.body.innerHTML.trim().length > 100;\n },\n { timeout: this.config.playwright?.timeout ?? 15000 }\n )\n ]).catch(() => console.log('Wait for app mount timeout... proceeding anyway'));\n\n // \uAC15\uC81C\uB85C \uCEF4\uD3EC\uB10C\uD2B8 \uB80C\uB354\uB9C1 \uBC0F \uD3F0\uD2B8\uC5B4\uC378 \uB4F1 \uB9AC\uC18C\uC2A4 \uB85C\uB529 \uB300\uAE30 \uC2DC\uAC04 \uCD94\uAC00 (\uC548\uC804 \uC7A5\uCE58)\n // onPlaywrightReady \uC2E0\uD638\uAC00 \uB5A8\uC5B4\uC838\uB3C4, \uBE44\uB3D9\uAE30 \uB80C\uB354\uB9C1(SwcLoop \uB4F1)\uC774 DOM\uC5D0 \uC644\uC804\uD788 \uBC18\uC601\uB418\uB294 \uB370\n // \uC57D\uAC04\uC758 Microtask \uC9C0\uC5F0\uC774 \uC788\uC744 \uC218 \uC788\uC73C\uBBC0\uB85C \uC544\uC8FC \uC9E7\uAC8C\uB9CC \uAE30\uB2E4\uB824 \uC90D\uB2C8\uB2E4.\n // await new Promise(resolve => setTimeout(resolve, 300));\n\n // Optional: Wait for a specific component to be visible\n if (this.config.playwright?.waitForSelector) {\n await page.waitForSelector(this.config.playwright.waitForSelector, {\n timeout: this.config.playwright?.timeout ?? 15000\n });\n }\n\n // Optional: Hardcoded wait time if explicitly set (fallback)\n if (this.config.playwright?.waitForTimeout) {\n await page.waitForTimeout(this.config.playwright.waitForTimeout);\n }\n\n // 4. Set SSR Attribute before extracting HTML\n await page.evaluate(() => {\n document.body.setAttribute('ssr-use', 'true');\n });\n\n // 5. Generate Final HTML\n const html = await page.evaluate(ignoreTags => {\n const skipTags = new Set(ignoreTags || []);\n\n function serializeNode(node: Node, inBody: boolean = false): string {\n if (node.nodeType === Node.TEXT_NODE) {\n const text = node.textContent || '';\n return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n }\n if (node.nodeType === Node.COMMENT_NODE) {\n return `<!--${node.textContent}-->`;\n }\n if (node.nodeType !== Node.ELEMENT_NODE) {\n return '';\n }\n\n const el = node as Element;\n const tag = el.tagName.toLowerCase();\n const isBody = tag === 'body';\n const isInsideBody = inBody || isBody;\n\n // \uD83D\uDEAB Skip ignored tags (like <style>) ONLY if we are inside the <body>.\n // This ensures <style> tags in the <head> (global styles) are preserved!\n if (isInsideBody && skipTags.has(tag)) {\n return '';\n }\n\n let s = `<${tag}`;\n\n // 1. Serialize Attributes\n for (let i = 0; i < el.attributes.length; i++) {\n const attr = el.attributes[i];\n s += ` ${attr.name}=\"${attr.value.replace(/\"/g, '"')}\"`;\n }\n\n // Handle void elements (self-closing)\n const voidElements = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);\n if (voidElements.has(tag)) {\n s += '>';\n return s;\n }\n s += '>';\n\n // 2. Serialize Shadow DOM (Declarative Shadow DOM)\n // MUST KEEP for SSR: Without this, the user receives an empty shell until JS loads and re-renders everything.\n // This allows the browser to paint the full UI (including styles inside the component) immediately.\n if (el.shadowRoot) {\n s += `<template shadowrootmode=\"${el.shadowRoot.mode}\">`;\n for (let i = 0; i < el.shadowRoot.childNodes.length; i++) {\n s += serializeNode(el.shadowRoot.childNodes[i], isInsideBody);\n }\n s += '</template>';\n }\n\n // 3. Serialize Light DOM Children\n for (let i = 0; i < el.childNodes.length; i++) {\n s += serializeNode(el.childNodes[i], isInsideBody);\n }\n\n s += `</${tag}>`;\n return s;\n }\n\n let result = '<!DOCTYPE html>\\n<html';\n const htmlNode = document.documentElement;\n for (let i = 0; i < htmlNode.attributes.length; i++) {\n const attr = htmlNode.attributes[i];\n result += ` ${attr.name}=\"${attr.value.replace(/\"/g, '"')}\"`;\n }\n result += '>\\n';\n\n if (document.head) result += serializeNode(document.head, false);\n result += '\\n';\n if (document.body) result += serializeNode(document.body, true);\n\n result += '\\n</html>';\n return result;\n }, this.config.playwright?.ignoreTags || []);\n console.log(`\uD83D\uDC88 [SSR:Playwright] Successfully rendered ${targetUrl}`);\n\n await this.writeOkHtmlAndEnd({ rr }, this.makeHTML(html));\n } catch (e) {\n console.error(`Playwright SSR Error for ${targetUrl}:`, e);\n // If Playwright fails (e.g., timeout), we bypass SSR so the client at least gets the CSR app\n return true;\n } finally {\n await context.close();\n }\n return false; // Tell SimpleBootHttpServer to stop routing because we've handled the response\n }\n return true;\n }\n\n makeHTML(html: string) {\n if (!/^<!DOCTYPE html>/i.test(html)) {\n html = '<!DOCTYPE html>\\n' + html;\n }\n return html;\n }\n\n async writeOkHtmlAndEnd({ rr, status = HttpStatus.Ok }: { rr: RequestResponse; status?: HttpStatus }, html: string) {\n rr.resStatusCode(status);\n rr.resSetHeader(HttpHeaders.ContentType, Mimes.TextHtml);\n await rr.resEnd(html);\n }\n\n async proceedAfter() {\n return true;\n }\n}\n"],
|
|
4
|
+
"sourcesContent": ["import { RequestResponse } from '@dooboostore/simple-boot-http-server/models/RequestResponse';\nimport { HttpHeaders } from '@dooboostore/simple-boot-http-server/codes/HttpHeaders';\nimport { Filter } from '@dooboostore/simple-boot-http-server/filters/Filter';\nimport { Mimes } from '@dooboostore/simple-boot-http-server/codes/Mimes';\nimport { HttpStatus } from '@dooboostore/simple-boot-http-server/codes/HttpStatus';\nimport { SimpleBootHttpServer } from '@dooboostore/simple-boot-http-server/SimpleBootHttpServer';\nimport path from 'path';\nimport fs from 'fs';\nimport {HttpMethod} from \"@dooboostore/simple-boot-http-server\";\n\nexport type SWCSSRConfig = {\n frontDistPath: string;\n frontDistIndexFileName?: string;\n welcomUrl?: string;\n ssrExcludeFilter?: (rr: RequestResponse) => boolean;\n /**\n * (Deprecated for Playwright)\n * Function to register components for each request.\n * Playwright evaluates real JS files loaded from your index.html,\n * so component registration happens automatically in the browser context.\n */\n registerComponents?: (window: any, rr?: RequestResponse) => Promise<void> | void;\n playwright?: {\n waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' | 'commit';\n waitForSelector?: string;\n waitForTimeout?: number;\n timeout?: number;\n ignoreTags?: string[];\n };\n};\n\n/**\n * SSR Filter specifically for Simple Web Component (SWC).\n * It utilizes Playwright to render the page robustly and perfectly matching browser specs.\n *\n * ### Playwright Setup & Requirements\n *\n * To use this filter, Playwright must be installed in your server environment:\n * 1. Install playwright package: `npm install playwright` or `pnpm add playwright`\n * 2. Install browsers: `npx playwright install chromium`\n *\n * If running inside a Docker container (e.g., Alpine Linux), you must use an image that supports Playwright\n * or manually install OS dependencies. Playwright provides official Docker images: `mcr.microsoft.com/playwright:v1.x.x-focal`.\n */\nexport class SSRSimpleWebComponentFilter implements Filter {\n private welcomUrl = 'http://localhost';\n private browser?: any;\n\n constructor(public config: SWCSSRConfig) {\n this.welcomUrl = config.welcomUrl || this.welcomUrl;\n }\n\n async onInit(app: SimpleBootHttpServer) {\n // 1. Initialize Playwright browser when the server starts\n const { chromium } = await import('playwright');\n this.browser = await chromium.launch({\n headless: true,\n args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-web-security'] // Allow CORS for local dev\n });\n console.log('\uD83D\uDE80 Playwright Browser initialized for SSR Filter');\n }\n\n async onDestroy() {\n if (this.browser) {\n await this.browser.close();\n console.log('\uD83D\uDED1 Playwright Browser closed');\n }\n }\n\n async proceedBefore({ rr, app, carrier }: { rr: RequestResponse; app: SimpleBootHttpServer; carrier: Map<string, any> }) {\n if (this.config.ssrExcludeFilter?.(rr)) {\n return true; // Bypass SSR and continue server chain\n }\n\n console.log('playwright2222222', rr.reqHeader('x-ssr-browser'));\n\n // 2. Prevent Infinite Loop:\n // If the request comes from Playwright itself, bypass SSR and serve the original static SPA file.\n if (rr.reqHeader('x-ssr-browser') === 'playwright') {\n return true;\n }\n\n if (rr.reqIsMethod(HttpMethod.GET) && rr.reqHasContentTypeHeader(Mimes.TextHtml)) {\n // Build target URL for Playwright to visit\n // Use the server's actual listening address and port if available\n const protocol = app.option.protocol || 'http';\n const host = app.option.hostname || '127.0.0.1';\n const port = app.option.port || 80;\n\n const url = rr.reqUrlObj({ host: `${host}:${port}` });\n url.protocol = protocol;\n\n const targetUrl = url.toString() ?? this.welcomUrl;\n\n if (!this.browser) {\n console.error('Playwright browser is not initialized. Bypassing SSR.');\n return true;\n }\n\n // Create a fresh context for isolation\n // IMPORTANT: We do NOT use `extraHTTPHeaders` globally because it attaches the header to EVERY request\n // Playwright makes (including CORS requests to FontAwesome or API requests).\n // Instead, we will set the header ONLY for the initial navigation request to our server.\n const context = await this.browser.newContext();\n const page = await context.newPage();\n\n try {\n // Intercept ONLY requests going to our local server and append the bypass header\n await page.route('**/*', route => {\n const request = route.request();\n\n // Only append the x-ssr-browser header for requests to the local server that are NOT API requests\n const isLocal = request.url().startsWith(targetUrl) || request.url().includes('localhost') || request.url().includes('127.0.0.1');\n const isApi = request.url().includes('/api/');\n\n if (isLocal && !isApi) {\n const headers = {\n ...request.headers(),\n 'x-ssr-browser': 'playwright'\n };\n route.continue({ headers }).catch(() => {});\n } else {\n // For external requests (like font-awesome CORS) or API calls, do not add the custom header\n route.continue().catch(() => {});\n }\n });\n\n // 3. Navigate to the page and wait for the framework to finish rendering\n // but normally Playwright just navigates to the targetUrl and loads the real SPA resources.\n\n // \uD83D\uDCA1 Playwright \u2194 Framework \uD1B5\uC2E0 \uC124\uC815\n // \uD504\uB860\uD2B8\uC5D4\uB4DC\uC758 `bootfactory.ts` \uC548\uC758 `onRouteChanged` \uB4F1\uC5D0\uC11C\n // `if (window.onPlaywrightReady) window.onPlaywrightReady()` \uB77C\uACE0 \uD638\uCD9C\uD574\uC8FC\uBA74,\n // \uC544\uB798\uC758 Promise\uAC00 \uC989\uC2DC resolve \uB418\uB3C4\uB85D \uC5F0\uACB0\uD569\uB2C8\uB2E4.\n let resolvePlaywrightReady: () => void;\n const playwrightReadyPromise = new Promise<void>(resolve => {\n resolvePlaywrightReady = resolve;\n });\n\n await page.exposeFunction('onPlaywrightReady', () => {\n console.log('\u26A1\uFE0F [SSR:Playwright] Received \"onPlaywrightReady\" signal from framework!');\n resolvePlaywrightReady();\n });\n\n // 3. Navigate to the page and wait for the framework to finish rendering\n // Since the server will return the `index.html` (due to the bypass header), Playwright will download\n // your main.js / bundle.js, evaluate it, and the Custom Elements will register themselves naturally!\n await page.goto(targetUrl, {\n waitUntil: this.config.playwright?.waitUntil ?? 'networkidle',\n timeout: this.config.playwright?.timeout ?? 15000\n });\n\n // \uD83D\uDCA1 Framework Load Wait (Crucial for SWC)\n // Wait for EITHER the framework to call `window.onPlaywrightReady()` OR a specific innerHTML condition\n await Promise.race([\n playwrightReadyPromise, // \uC9C1\uC811 exposeFunction\uC5D0\uC11C \uD2B8\uB9AC\uAC70\uB41C Promise\n\n page.waitForFunction(\n () => {\n // Fallback 1: If explicit signal variable is set\n return (window as any)._playwrightReadySignalFired === true;\n },\n { timeout: this.config.playwright?.timeout ?? 15000 }\n ),\n\n page.waitForFunction(\n () => {\n // Fallback 2: If no signal, just wait until body has content\n return document.body.innerHTML.trim().length > 100;\n },\n { timeout: this.config.playwright?.timeout ?? 15000 }\n )\n ]).catch(() => console.log('Wait for app mount timeout... proceeding anyway'));\n\n // \uAC15\uC81C\uB85C \uCEF4\uD3EC\uB10C\uD2B8 \uB80C\uB354\uB9C1 \uBC0F \uD3F0\uD2B8\uC5B4\uC378 \uB4F1 \uB9AC\uC18C\uC2A4 \uB85C\uB529 \uB300\uAE30 \uC2DC\uAC04 \uCD94\uAC00 (\uC548\uC804 \uC7A5\uCE58)\n // onPlaywrightReady \uC2E0\uD638\uAC00 \uB5A8\uC5B4\uC838\uB3C4, \uBE44\uB3D9\uAE30 \uB80C\uB354\uB9C1(SwcLoop \uB4F1)\uC774 DOM\uC5D0 \uC644\uC804\uD788 \uBC18\uC601\uB418\uB294 \uB370\n // \uC57D\uAC04\uC758 Microtask \uC9C0\uC5F0\uC774 \uC788\uC744 \uC218 \uC788\uC73C\uBBC0\uB85C \uC544\uC8FC \uC9E7\uAC8C\uB9CC \uAE30\uB2E4\uB824 \uC90D\uB2C8\uB2E4.\n // await new Promise(resolve => setTimeout(resolve, 300));\n\n // Optional: Wait for a specific component to be visible\n if (this.config.playwright?.waitForSelector) {\n await page.waitForSelector(this.config.playwright.waitForSelector, {\n timeout: this.config.playwright?.timeout ?? 15000\n });\n }\n\n // Optional: Hardcoded wait time if explicitly set (fallback)\n if (this.config.playwright?.waitForTimeout) {\n await page.waitForTimeout(this.config.playwright.waitForTimeout);\n }\n\n // 4. Set SSR Attribute before extracting HTML\n await page.evaluate(() => {\n document.body.setAttribute('ssr-use', 'true');\n });\n\n // 5. Generate Final HTML\n const html = await page.evaluate(ignoreTags => {\n const skipTags = new Set(ignoreTags || []);\n\n function serializeNode(node: Node, inBody: boolean = false): string {\n if (node.nodeType === Node.TEXT_NODE) {\n const text = node.textContent || '';\n return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n }\n if (node.nodeType === Node.COMMENT_NODE) {\n return `<!--${node.textContent}-->`;\n }\n if (node.nodeType !== Node.ELEMENT_NODE) {\n return '';\n }\n\n const el = node as Element;\n const tag = el.tagName.toLowerCase();\n const isBody = tag === 'body';\n const isInsideBody = inBody || isBody;\n\n // \uD83D\uDEAB Skip ignored tags (like <style>) ONLY if we are inside the <body>.\n // This ensures <style> tags in the <head> (global styles) are preserved!\n if (isInsideBody && skipTags.has(tag)) {\n return '';\n }\n\n let s = `<${tag}`;\n\n // 1. Serialize Attributes\n for (let i = 0; i < el.attributes.length; i++) {\n const attr = el.attributes[i];\n s += ` ${attr.name}=\"${attr.value.replace(/\"/g, '"')}\"`;\n }\n\n // Handle void elements (self-closing)\n const voidElements = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);\n if (voidElements.has(tag)) {\n s += '>';\n return s;\n }\n s += '>';\n\n // 2. Serialize Shadow DOM (Declarative Shadow DOM)\n // MUST KEEP for SSR: Without this, the user receives an empty shell until JS loads and re-renders everything.\n // This allows the browser to paint the full UI (including styles inside the component) immediately.\n if (el.shadowRoot) {\n s += `<template shadowrootmode=\"${el.shadowRoot.mode}\">`;\n for (let i = 0; i < el.shadowRoot.childNodes.length; i++) {\n s += serializeNode(el.shadowRoot.childNodes[i], isInsideBody);\n }\n s += '</template>';\n }\n\n // 3. Serialize Light DOM Children\n for (let i = 0; i < el.childNodes.length; i++) {\n s += serializeNode(el.childNodes[i], isInsideBody);\n }\n\n s += `</${tag}>`;\n return s;\n }\n\n let result = '<!DOCTYPE html>\\n<html';\n const htmlNode = document.documentElement;\n for (let i = 0; i < htmlNode.attributes.length; i++) {\n const attr = htmlNode.attributes[i];\n result += ` ${attr.name}=\"${attr.value.replace(/\"/g, '"')}\"`;\n }\n result += '>\\n';\n\n if (document.head) result += serializeNode(document.head, false);\n result += '\\n';\n if (document.body) result += serializeNode(document.body, true);\n\n result += '\\n</html>';\n return result;\n }, this.config.playwright?.ignoreTags || []);\n console.log(`\uD83D\uDC88 [SSR:Playwright] Successfully rendered ${targetUrl}`);\n\n await this.writeOkHtmlAndEnd({ rr }, this.makeHTML(html));\n } catch (e) {\n console.error(`Playwright SSR Error for ${targetUrl}:`, e);\n // If Playwright fails (e.g., timeout), we bypass SSR so the client at least gets the CSR app\n return true;\n } finally {\n await context.close();\n }\n return false; // Tell SimpleBootHttpServer to stop routing because we've handled the response\n }\n return true;\n }\n\n makeHTML(html: string) {\n if (!/^<!DOCTYPE html>/i.test(html)) {\n html = '<!DOCTYPE html>\\n' + html;\n }\n return html;\n }\n\n async writeOkHtmlAndEnd({ rr, status = HttpStatus.Ok }: { rr: RequestResponse; status?: HttpStatus }, html: string) {\n rr.resStatusCode(status);\n rr.resSetHeader(HttpHeaders.ContentType, Mimes.TextHtml);\n await rr.resEnd(html);\n }\n\n async proceedAfter() {\n return true;\n }\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { HttpHeaders } from "@dooboostore/simple-boot-http-server/codes/HttpHeaders";
|
|
2
2
|
import { Mimes } from "@dooboostore/simple-boot-http-server/codes/Mimes";
|
|
3
3
|
import { HttpStatus } from "@dooboostore/simple-boot-http-server/codes/HttpStatus";
|
|
4
|
+
import { injectRequestResponse } from "@dooboostore/simple-boot-http-server/proxy/RequestResponseInjectProxy";
|
|
4
5
|
import { DomParserInitializer } from "../initializers/DomParserInitializer.js";
|
|
6
|
+
import { findAllPropertyMetadata } from "@dooboostore/simple-web-component";
|
|
5
7
|
class SSRSimpleWebComponentDomParserFilter {
|
|
6
8
|
constructor(config) {
|
|
7
9
|
this.config = config;
|
|
@@ -9,6 +11,7 @@ class SSRSimpleWebComponentDomParserFilter {
|
|
|
9
11
|
this.welcomUrl = config.welcomUrl || this.welcomUrl;
|
|
10
12
|
}
|
|
11
13
|
async onInit(app) {
|
|
14
|
+
this.app = app;
|
|
12
15
|
}
|
|
13
16
|
async onDestroy() {
|
|
14
17
|
}
|
|
@@ -23,8 +26,53 @@ class SSRSimpleWebComponentDomParserFilter {
|
|
|
23
26
|
const domParserInitializer = new DomParserInitializer(this.config.frontDistPath, this.config.frontDistIndexFileName || "index.html", { url: targetUrl });
|
|
24
27
|
const window = await domParserInitializer.run();
|
|
25
28
|
try {
|
|
29
|
+
let app;
|
|
26
30
|
if (this.config.registerComponents) {
|
|
27
|
-
|
|
31
|
+
const sim = /* @__PURE__ */ new Map();
|
|
32
|
+
for (const service of (this.config.intentServices ?? []).filter(Boolean)) {
|
|
33
|
+
if (!service.symbol) continue;
|
|
34
|
+
for (const sym of Array.isArray(service.symbol) ? service.symbol : [service.symbol]) {
|
|
35
|
+
const raw = this.app?.sim(sym);
|
|
36
|
+
if (raw) sim.set(sym, injectRequestResponse(raw, rr));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
app = await this.config.registerComponents(window, rr, sim);
|
|
40
|
+
}
|
|
41
|
+
if (app) {
|
|
42
|
+
const items = [];
|
|
43
|
+
const targets = [app, ...(app.connectedElements?.() ?? []).filter((el) => el !== app)];
|
|
44
|
+
for (const el of targets) {
|
|
45
|
+
const swcId = el._swcId;
|
|
46
|
+
if (!swcId) continue;
|
|
47
|
+
const sel = `[swc-use-ssr="${swcId}"]`;
|
|
48
|
+
for (const m of findAllPropertyMetadata(el)) {
|
|
49
|
+
let value;
|
|
50
|
+
try {
|
|
51
|
+
value = el[m.propertyKey];
|
|
52
|
+
} catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (value === void 0 || typeof value === "function") continue;
|
|
56
|
+
try {
|
|
57
|
+
JSON.stringify(value);
|
|
58
|
+
} catch {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
items.push({ sel, prop: String(m.propertyKey), value });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (items.length > 0) {
|
|
65
|
+
const script = window.document.createElement("script");
|
|
66
|
+
const hydrationJson = JSON.stringify(items).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
67
|
+
script.textContent = ` window.__swc_hydration=${hydrationJson};for(const h of window.__swc_hydration){
|
|
68
|
+
const el=document.querySelector(h.sel);
|
|
69
|
+
if(el){
|
|
70
|
+
el[h.prop]=h.value;
|
|
71
|
+
}
|
|
72
|
+
}`;
|
|
73
|
+
window.document.body.appendChild(script);
|
|
74
|
+
console.log(`[ssr-hydration] ${items.length} props embedded`);
|
|
75
|
+
}
|
|
28
76
|
}
|
|
29
77
|
const html = this.makeHTML(window);
|
|
30
78
|
await this.writeOkHtmlAndEnd({ rr }, html);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/filters/SSRSimpleWebComponentDomParserFilter.ts"],
|
|
4
|
-
"sourcesContent": ["import {RequestResponse} from '@dooboostore/simple-boot-http-server/models/RequestResponse';\nimport {HttpHeaders} from '@dooboostore/simple-boot-http-server/codes/HttpHeaders';\nimport {Filter} from '@dooboostore/simple-boot-http-server/filters/Filter';\nimport {Mimes} from '@dooboostore/simple-boot-http-server/codes/Mimes';\nimport {HttpStatus} from '@dooboostore/simple-boot-http-server/codes/HttpStatus';\nimport {SimpleBootHttpServer} from '@dooboostore/simple-boot-http-server/SimpleBootHttpServer';\nimport {DomParserInitializer} from '../initializers/DomParserInitializer';\n\nexport type SWCSSRDomParserConfig = {\n frontDistPath: string;\n frontDistIndexFileName?: string;\n welcomUrl?: string;\n ssrExcludeFilter?: (rr: RequestResponse) => boolean;\n /**\n *
|
|
5
|
-
"mappings": "AACA,SAAQ,mBAAkB;AAE1B,SAAQ,aAAY;AACpB,SAAQ,kBAAiB;AAEzB,SAAQ,4BAA2B;
|
|
4
|
+
"sourcesContent": ["import {RequestResponse} from '@dooboostore/simple-boot-http-server/models/RequestResponse';\nimport {HttpHeaders} from '@dooboostore/simple-boot-http-server/codes/HttpHeaders';\nimport {Filter} from '@dooboostore/simple-boot-http-server/filters/Filter';\nimport {Mimes} from '@dooboostore/simple-boot-http-server/codes/Mimes';\nimport {HttpStatus} from '@dooboostore/simple-boot-http-server/codes/HttpStatus';\nimport {SimpleBootHttpServer} from '@dooboostore/simple-boot-http-server/SimpleBootHttpServer';\nimport {injectRequestResponse} from '@dooboostore/simple-boot-http-server/proxy/RequestResponseInjectProxy';\nimport {SimConfig} from '@dooboostore/simple-boot';\nimport {DomParserInitializer} from '../initializers/DomParserInitializer';\nimport type { SwcAppInterface } from '@dooboostore/simple-web-component';\nimport { findAllPropertyMetadata } from '@dooboostore/simple-web-component';\n\nexport type SWCSSRDomParserConfig = {\n frontDistPath: string;\n frontDistIndexFileName?: string;\n welcomUrl?: string;\n ssrExcludeFilter?: (rr: RequestResponse) => boolean;\n /**\n * SSR \uC5D0 \uB178\uCD9C\uD560 \uBC31\uC5D4\uB4DC \uC11C\uBE44\uC2A4 \uBAA9\uB85D(\uAC01 \uD56D\uBAA9\uC740 symbol \uC744 \uAC00\uC9C4\uB2E4, \uC608: pairServices).\n * \uD504\uB808\uC784\uC6CC\uD06C\uAC00 \uC694\uCCAD\uB9C8\uB2E4 \uAC01 \uC11C\uBE44\uC2A4\uB97C \uAEBC\uB0B4 \uC774 \uC694\uCCAD\uC758 rr \uC744 \uC8FC\uC785\uD55C \uB4A4 Map \uC73C\uB85C \uBB36\uC5B4\n * registerComponents \uC758 3\uBC88\uC9F8 \uC778\uC790(sim)\uB85C \uB118\uAE34\uB2E4. \u2192 \uC571\uC740 \uADF8 Map \uC744 bootfactory \uC5D0 \uADF8\uB300\uB85C \uC804\uB2EC\uB9CC \uD558\uBA74 \uB418\uACE0\n * \uC9C1\uC811 Proxy \uB85C \uAC10\uC2F8\uAC70\uB098 rr \uC744 \uB123\uC744 \uD544\uC694\uAC00 \uC5C6\uB2E4(\uCFE0\uD0A4 \uB4F1 \uC694\uCCAD \uCEE8\uD14D\uC2A4\uD2B8 \uC790\uB3D9 \uC804\uB2EC).\n */\n intentServices?: SimConfig[];\n /**\n * \uC694\uCCAD\uB9C8\uB2E4 \uCEF4\uD3EC\uB10C\uD2B8\uB97C \uB4F1\uB85D\uD55C\uB2E4. \uC571 bootfactory\uB97C \uADF8\uB300\uB85C \uB118\uAE30\uBA74 \uB41C\uB2E4.\n * @param sim intentServices \uB97C rr \uC8FC\uC785\uD574 \uBB36\uC740 Map<symbol, service>. bootfactory \uC5D0 \uADF8\uB300\uB85C \uB118\uAE30\uBA74 \uB41C\uB2E4.\n * @returns \uCD08\uAE30\uD654\uB41C SwcApp (\uD558\uC774\uB4DC\uB808\uC774\uC158 \uB370\uC774\uD130 \uBC15\uC81C \uB4F1\uC5D0 \uC0AC\uC6A9)\n */\n registerComponents?: (window: any, rr: RequestResponse, sim: Map<symbol, any>) => Promise<SwcAppInterface | void> | SwcAppInterface | void;\n};\n\n/**\n * SSR Filter specifically for Simple Web Component (SWC).\n * It utilizes @dooboostore/dom-parser for rendering and DSD support.\n */\nexport class SSRSimpleWebComponentDomParserFilter implements Filter {\n private welcomUrl = 'http://localhost';\n private app?: SimpleBootHttpServer;\n\n constructor(public config: SWCSSRDomParserConfig) {\n this.welcomUrl = config.welcomUrl || this.welcomUrl;\n }\n\n async onInit(app: SimpleBootHttpServer) {\n this.app = app;\n }\n\n async onDestroy() {}\n\n async proceedBefore({ rr }: { rr: RequestResponse; app: SimpleBootHttpServer; carrier: Map<string, any> }) {\n if (this.config.ssrExcludeFilter?.(rr)) {\n return false;\n }\n\n if (rr.reqHasAcceptHeader(Mimes.TextHtml) || rr.reqHasAcceptHeader(Mimes.All)) {\n const url = rr.reqUrlObj({ host: 'localhost' });\n const targetUrl = url.toString() ?? this.welcomUrl;\n console.log('SSRSimpleWebComponentDomParserFilter start');\n\n // 1. Initialize Virtual DOM Environment\n const domParserInitializer = new DomParserInitializer(this.config.frontDistPath, this.config.frontDistIndexFileName || 'index.html', { url: targetUrl });\n const window = await domParserInitializer.run();\n\n // web component \uACBD\uC6B0 \uC790\uAE30 tagName\uC744 \uC0DD\uC131\uC790 \uC5D0\uAC8C HTMLElementBase\uC5D0\uB118\uACA8\uC918\uC57C\uB418\uAE30\uB584\uBB38\uC5D0\n // const getTagName = (type: ConstructorType<any>) => {\n // const zz = getElementConfig(type);\n // return zz.name;\n // };\n // (window as any).HTMLElement = class extends HTMLElementBase {\n // constructor(...args: any[]) {\n // const ctor = new.target as any; // \uC774\uB7F0 \uC288\uAC00 \uAE30\uB2A5\uC774...\n // const resolvedTagName = getTagName(ctor);\n // super(resolvedTagName, args[0]);\n // }\n // }\n\n\n try {\n // 2. Register Components if provided\n // We use SwcApplication inside the callback or directly here.\n let app: SwcAppInterface | void;\n if (this.config.registerComponents) {\n // intentServices \uB97C rr \uC8FC\uC785\uD574 Map \uC73C\uB85C \uBB36\uC5B4 \uB118\uAE34\uB2E4 (\uC571\uC740 bootfactory \uC5D0 \uADF8\uB300\uB85C \uC804\uB2EC\uB9CC).\n const sim = new Map<symbol, any>();\n for (const service of (this.config.intentServices ?? []).filter(Boolean)) {\n if (!service.symbol) continue;\n for (const sym of (Array.isArray(service.symbol) ? service.symbol : [service.symbol])) {\n const raw = this.app?.sim<any>(sym as any);\n if (raw) sim.set(sym, injectRequestResponse(raw, rr));\n }\n }\n app = await this.config.registerComponents(window, rr, sim);\n }\n\n // [\uD558\uC774\uB4DC\uB808\uC774\uC158] swcApp \uBCF8\uCCB4 + \uC5F0\uACB0 \uC5D8\uB9AC\uBA3C\uD2B8\uC758 @property \uAC12\uC744 script\uB85C \uBC15\uC81C.\n // \uD074\uB77C\uC5D0\uC11C \uBC88\uB4E4\uBCF4\uB2E4 \uBA3C\uC800 \uC2E4\uD589 \u2192 querySelector\uB85C \uCC3E\uC544 .prop = \uAC12 \uC138\uD305 (\uB124\uC774\uD2F0\uBE0C\n // \uC5C5\uADF8\uB808\uC774\uB4DC\uB294 own property \uC720\uC9C0\uB77C upgrade \uD6C4\uC5D0\uB3C4 \uAC12 \uC0B4\uC544\uC788\uC74C).\n if (app) {\n const items: Array<{ sel: string; prop: string; value: unknown }> = [];\n const targets: any[] = [app, ...(app.connectedElements?.() ?? []).filter((el: any) => el !== app)];\n for (const el of targets) {\n const swcId = (el as any)._swcId;\n if (!swcId) continue;\n const sel = `[swc-use-ssr=\"${swcId}\"]`;\n for (const m of findAllPropertyMetadata(el)) {\n let value: unknown;\n try {\n value = el[m.propertyKey];\n } catch {\n continue;\n }\n if (value === undefined || typeof value === 'function') continue;\n try {\n JSON.stringify(value);\n } catch {\n continue;\n }\n items.push({ sel, prop: String(m.propertyKey), value });\n }\n }\n if (items.length > 0) {\n // \uC5C5\uADF8\uB808\uC774\uB4DC \uB54C \uB178\uB4DC\uAC00 \uAC08\uC544\uC5CE\uC5B4\uC9C0\uBA74 JS \uD504\uB85C\uD37C\uD2F0\uB294 \uB0A0\uC544\uAC10. attribute\uB294 \uBCF5\uC0AC\uB418\uB2C8\n // data-hyd-* \uB85C \uBC15\uACE0, \uAC19\uC740 \uB178\uB4DC\uBA74 \uD504\uB85C\uD37C\uD2F0\uB3C4 \uBC14\uB85C \uC138\uD305.\n const script = window.document.createElement('script');\n // <script> \uB0B4\uC6A9\uC740 \uC774\uC2A4\uCF00\uC774\uD504 \uC5C6\uC774 \uC9C1\uB82C\uD654\uB41C\uB2E4 \u2192 \uAC12\uC5D0 '</script>'\uAC00 \uC788\uC73C\uBA74 HTML \uD30C\uC11C\uAC00 \uAC70\uAE30\uC11C \uD0DC\uADF8\uB97C \uB2EB\uC544 XSS.\n // JSON.stringify\uB294 '<'\uB97C \uC774\uC2A4\uCF00\uC774\uD504\uD558\uC9C0 \uC54A\uC73C\uBBC0\uB85C '\\u003c'\uB85C \uBC14\uAFBC\uB2E4 (JS\uC5D0\uC120 \uAC19\uC740 \uBB38\uC790\uC5F4).\n // U+2028/2029\uB294 \uAD6C\uD615 JS \uC5D4\uC9C4\uC5D0\uC11C \uBB38\uC790\uC5F4 \uB9AC\uD130\uB7F4 \uC548\uC758 \uC904\uBC14\uAFC8\uC73C\uB85C \uD574\uC11D\uB3FC \uBB38\uBC95 \uC624\uB958\uAC00 \uB098\uBBC0\uB85C \uD568\uAED8 \uC774\uC2A4\uCF00\uC774\uD504.\n const hydrationJson = JSON.stringify(items)\n .replace(/</g, '\\\\u003c')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n script.textContent =\n ` window.__swc_hydration=${hydrationJson};` +\n `for(const h of window.__swc_hydration){\n const el=document.querySelector(h.sel);\n if(el){\n el[h.prop]=h.value;\n }\n }`;\n window.document.body.appendChild(script);\n console.log(`[ssr-hydration] ${items.length} props embedded`);\n }\n }\n\n // window.document.querySelector('.sidebar-space').innerHTML = '\uC528\uBC1C\uB188\uC544.';\n // await new Promise<void>((resolve, reject) => setTimeout(resolve, 100));\n // await new Promise<void>((resolve, reject) => setTimeout(resolve, 5000));\n // window.document.body.innerHTML = '';\n // window.document.body.setAttribute('ssr-use', 'true');\n // 3. Generate Final HTML\n const html = this.makeHTML(window);\n // console.log('html: \uD83D\uDC88', html);\n await this.writeOkHtmlAndEnd({ rr }, html);\n } finally {\n // 4. Cleanup\n window.close();\n domParserInitializer.destroy();\n }\n return false;\n }\n return true;\n }\n\n makeHTML(window: any) {\n let html = window.document.documentElement.outerHTML;\n if (!/^<!DOCTYPE html>/i.test(html)) {\n html = '<!DOCTYPE html>\\n' + html;\n }\n return html;\n }\n\n async writeOkHtmlAndEnd({ rr, status = HttpStatus.Ok }: { rr: RequestResponse; status?: HttpStatus }, html: string) {\n rr.resStatusCode(status);\n rr.resSetHeader(HttpHeaders.ContentType, Mimes.TextHtml);\n await rr.resEnd(html);\n }\n\n async proceedAfter() {\n return true;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/filters/SSRSimpleWebComponentFilter.ts"],
|
|
4
|
-
"sourcesContent": ["import { RequestResponse } from '@dooboostore/simple-boot-http-server/models/RequestResponse';\nimport { HttpHeaders } from '@dooboostore/simple-boot-http-server/codes/HttpHeaders';\nimport { Filter } from '@dooboostore/simple-boot-http-server/filters/Filter';\nimport { Mimes } from '@dooboostore/simple-boot-http-server/codes/Mimes';\nimport { HttpStatus } from '@dooboostore/simple-boot-http-server/codes/HttpStatus';\nimport { SimpleBootHttpServer } from '@dooboostore/simple-boot-http-server/SimpleBootHttpServer';\nimport path from 'path';\nimport fs from 'fs';\nimport {HttpMethod} from \"@dooboostore/simple-boot-http-server\";\n\nexport type SWCSSRConfig = {\n frontDistPath: string;\n frontDistIndexFileName?: string;\n welcomUrl?: string;\n ssrExcludeFilter?: (rr: RequestResponse) => boolean;\n /**\n * (Deprecated for Playwright)\n * Function to register components for each request.\n * Playwright evaluates real JS files loaded from your index.html,\n * so component registration happens automatically in the browser context.\n */\n registerComponents?: (window: any) => Promise<void> | void;\n playwright?: {\n waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' | 'commit';\n waitForSelector?: string;\n waitForTimeout?: number;\n timeout?: number;\n ignoreTags?: string[];\n };\n};\n\n/**\n * SSR Filter specifically for Simple Web Component (SWC).\n * It utilizes Playwright to render the page robustly and perfectly matching browser specs.\n *\n * ### Playwright Setup & Requirements\n *\n * To use this filter, Playwright must be installed in your server environment:\n * 1. Install playwright package: `npm install playwright` or `pnpm add playwright`\n * 2. Install browsers: `npx playwright install chromium`\n *\n * If running inside a Docker container (e.g., Alpine Linux), you must use an image that supports Playwright\n * or manually install OS dependencies. Playwright provides official Docker images: `mcr.microsoft.com/playwright:v1.x.x-focal`.\n */\nexport class SSRSimpleWebComponentFilter implements Filter {\n private welcomUrl = 'http://localhost';\n private browser?: any;\n\n constructor(public config: SWCSSRConfig) {\n this.welcomUrl = config.welcomUrl || this.welcomUrl;\n }\n\n async onInit(app: SimpleBootHttpServer) {\n // 1. Initialize Playwright browser when the server starts\n const { chromium } = await import('playwright');\n this.browser = await chromium.launch({\n headless: true,\n args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-web-security'] // Allow CORS for local dev\n });\n console.log('\uD83D\uDE80 Playwright Browser initialized for SSR Filter');\n }\n\n async onDestroy() {\n if (this.browser) {\n await this.browser.close();\n console.log('\uD83D\uDED1 Playwright Browser closed');\n }\n }\n\n async proceedBefore({ rr, app, carrier }: { rr: RequestResponse; app: SimpleBootHttpServer; carrier: Map<string, any> }) {\n if (this.config.ssrExcludeFilter?.(rr)) {\n return true; // Bypass SSR and continue server chain\n }\n\n console.log('playwright2222222', rr.reqHeader('x-ssr-browser'));\n\n // 2. Prevent Infinite Loop:\n // If the request comes from Playwright itself, bypass SSR and serve the original static SPA file.\n if (rr.reqHeader('x-ssr-browser') === 'playwright') {\n return true;\n }\n\n if (rr.reqIsMethod(HttpMethod.GET) && rr.reqHasContentTypeHeader(Mimes.TextHtml)) {\n // Build target URL for Playwright to visit\n // Use the server's actual listening address and port if available\n const protocol = app.option.protocol || 'http';\n const host = app.option.hostname || '127.0.0.1';\n const port = app.option.port || 80;\n\n const url = rr.reqUrlObj({ host: `${host}:${port}` });\n url.protocol = protocol;\n\n const targetUrl = url.toString() ?? this.welcomUrl;\n\n if (!this.browser) {\n console.error('Playwright browser is not initialized. Bypassing SSR.');\n return true;\n }\n\n // Create a fresh context for isolation\n // IMPORTANT: We do NOT use `extraHTTPHeaders` globally because it attaches the header to EVERY request\n // Playwright makes (including CORS requests to FontAwesome or API requests).\n // Instead, we will set the header ONLY for the initial navigation request to our server.\n const context = await this.browser.newContext();\n const page = await context.newPage();\n\n try {\n // Intercept ONLY requests going to our local server and append the bypass header\n await page.route('**/*', route => {\n const request = route.request();\n\n // Only append the x-ssr-browser header for requests to the local server that are NOT API requests\n const isLocal = request.url().startsWith(targetUrl) || request.url().includes('localhost') || request.url().includes('127.0.0.1');\n const isApi = request.url().includes('/api/');\n\n if (isLocal && !isApi) {\n const headers = {\n ...request.headers(),\n 'x-ssr-browser': 'playwright'\n };\n route.continue({ headers }).catch(() => {});\n } else {\n // For external requests (like font-awesome CORS) or API calls, do not add the custom header\n route.continue().catch(() => {});\n }\n });\n\n // 3. Navigate to the page and wait for the framework to finish rendering\n // but normally Playwright just navigates to the targetUrl and loads the real SPA resources.\n\n // \uD83D\uDCA1 Playwright \u2194 Framework \uD1B5\uC2E0 \uC124\uC815\n // \uD504\uB860\uD2B8\uC5D4\uB4DC\uC758 `bootfactory.ts` \uC548\uC758 `onRouteChanged` \uB4F1\uC5D0\uC11C\n // `if (window.onPlaywrightReady) window.onPlaywrightReady()` \uB77C\uACE0 \uD638\uCD9C\uD574\uC8FC\uBA74,\n // \uC544\uB798\uC758 Promise\uAC00 \uC989\uC2DC resolve \uB418\uB3C4\uB85D \uC5F0\uACB0\uD569\uB2C8\uB2E4.\n let resolvePlaywrightReady: () => void;\n const playwrightReadyPromise = new Promise<void>(resolve => {\n resolvePlaywrightReady = resolve;\n });\n\n await page.exposeFunction('onPlaywrightReady', () => {\n console.log('\u26A1\uFE0F [SSR:Playwright] Received \"onPlaywrightReady\" signal from framework!');\n resolvePlaywrightReady();\n });\n\n // 3. Navigate to the page and wait for the framework to finish rendering\n // Since the server will return the `index.html` (due to the bypass header), Playwright will download\n // your main.js / bundle.js, evaluate it, and the Custom Elements will register themselves naturally!\n await page.goto(targetUrl, {\n waitUntil: this.config.playwright?.waitUntil ?? 'networkidle',\n timeout: this.config.playwright?.timeout ?? 15000\n });\n\n // \uD83D\uDCA1 Framework Load Wait (Crucial for SWC)\n // Wait for EITHER the framework to call `window.onPlaywrightReady()` OR a specific innerHTML condition\n await Promise.race([\n playwrightReadyPromise, // \uC9C1\uC811 exposeFunction\uC5D0\uC11C \uD2B8\uB9AC\uAC70\uB41C Promise\n\n page.waitForFunction(\n () => {\n // Fallback 1: If explicit signal variable is set\n return (window as any)._playwrightReadySignalFired === true;\n },\n { timeout: this.config.playwright?.timeout ?? 15000 }\n ),\n\n page.waitForFunction(\n () => {\n // Fallback 2: If no signal, just wait until body has content\n return document.body.innerHTML.trim().length > 100;\n },\n { timeout: this.config.playwright?.timeout ?? 15000 }\n )\n ]).catch(() => console.log('Wait for app mount timeout... proceeding anyway'));\n\n // \uAC15\uC81C\uB85C \uCEF4\uD3EC\uB10C\uD2B8 \uB80C\uB354\uB9C1 \uBC0F \uD3F0\uD2B8\uC5B4\uC378 \uB4F1 \uB9AC\uC18C\uC2A4 \uB85C\uB529 \uB300\uAE30 \uC2DC\uAC04 \uCD94\uAC00 (\uC548\uC804 \uC7A5\uCE58)\n // onPlaywrightReady \uC2E0\uD638\uAC00 \uB5A8\uC5B4\uC838\uB3C4, \uBE44\uB3D9\uAE30 \uB80C\uB354\uB9C1(SwcLoop \uB4F1)\uC774 DOM\uC5D0 \uC644\uC804\uD788 \uBC18\uC601\uB418\uB294 \uB370\n // \uC57D\uAC04\uC758 Microtask \uC9C0\uC5F0\uC774 \uC788\uC744 \uC218 \uC788\uC73C\uBBC0\uB85C \uC544\uC8FC \uC9E7\uAC8C\uB9CC \uAE30\uB2E4\uB824 \uC90D\uB2C8\uB2E4.\n // await new Promise(resolve => setTimeout(resolve, 300));\n\n // Optional: Wait for a specific component to be visible\n if (this.config.playwright?.waitForSelector) {\n await page.waitForSelector(this.config.playwright.waitForSelector, {\n timeout: this.config.playwright?.timeout ?? 15000\n });\n }\n\n // Optional: Hardcoded wait time if explicitly set (fallback)\n if (this.config.playwright?.waitForTimeout) {\n await page.waitForTimeout(this.config.playwright.waitForTimeout);\n }\n\n // 4. Set SSR Attribute before extracting HTML\n await page.evaluate(() => {\n document.body.setAttribute('ssr-use', 'true');\n });\n\n // 5. Generate Final HTML\n const html = await page.evaluate(ignoreTags => {\n const skipTags = new Set(ignoreTags || []);\n\n function serializeNode(node: Node, inBody: boolean = false): string {\n if (node.nodeType === Node.TEXT_NODE) {\n const text = node.textContent || '';\n return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n }\n if (node.nodeType === Node.COMMENT_NODE) {\n return `<!--${node.textContent}-->`;\n }\n if (node.nodeType !== Node.ELEMENT_NODE) {\n return '';\n }\n\n const el = node as Element;\n const tag = el.tagName.toLowerCase();\n const isBody = tag === 'body';\n const isInsideBody = inBody || isBody;\n\n // \uD83D\uDEAB Skip ignored tags (like <style>) ONLY if we are inside the <body>.\n // This ensures <style> tags in the <head> (global styles) are preserved!\n if (isInsideBody && skipTags.has(tag)) {\n return '';\n }\n\n let s = `<${tag}`;\n\n // 1. Serialize Attributes\n for (let i = 0; i < el.attributes.length; i++) {\n const attr = el.attributes[i];\n s += ` ${attr.name}=\"${attr.value.replace(/\"/g, '"')}\"`;\n }\n\n // Handle void elements (self-closing)\n const voidElements = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);\n if (voidElements.has(tag)) {\n s += '>';\n return s;\n }\n s += '>';\n\n // 2. Serialize Shadow DOM (Declarative Shadow DOM)\n // MUST KEEP for SSR: Without this, the user receives an empty shell until JS loads and re-renders everything.\n // This allows the browser to paint the full UI (including styles inside the component) immediately.\n if (el.shadowRoot) {\n s += `<template shadowrootmode=\"${el.shadowRoot.mode}\">`;\n for (let i = 0; i < el.shadowRoot.childNodes.length; i++) {\n s += serializeNode(el.shadowRoot.childNodes[i], isInsideBody);\n }\n s += '</template>';\n }\n\n // 3. Serialize Light DOM Children\n for (let i = 0; i < el.childNodes.length; i++) {\n s += serializeNode(el.childNodes[i], isInsideBody);\n }\n\n s += `</${tag}>`;\n return s;\n }\n\n let result = '<!DOCTYPE html>\\n<html';\n const htmlNode = document.documentElement;\n for (let i = 0; i < htmlNode.attributes.length; i++) {\n const attr = htmlNode.attributes[i];\n result += ` ${attr.name}=\"${attr.value.replace(/\"/g, '"')}\"`;\n }\n result += '>\\n';\n\n if (document.head) result += serializeNode(document.head, false);\n result += '\\n';\n if (document.body) result += serializeNode(document.body, true);\n\n result += '\\n</html>';\n return result;\n }, this.config.playwright?.ignoreTags || []);\n console.log(`\uD83D\uDC88 [SSR:Playwright] Successfully rendered ${targetUrl}`);\n\n await this.writeOkHtmlAndEnd({ rr }, this.makeHTML(html));\n } catch (e) {\n console.error(`Playwright SSR Error for ${targetUrl}:`, e);\n // If Playwright fails (e.g., timeout), we bypass SSR so the client at least gets the CSR app\n return true;\n } finally {\n await context.close();\n }\n return false; // Tell SimpleBootHttpServer to stop routing because we've handled the response\n }\n return true;\n }\n\n makeHTML(html: string) {\n if (!/^<!DOCTYPE html>/i.test(html)) {\n html = '<!DOCTYPE html>\\n' + html;\n }\n return html;\n }\n\n async writeOkHtmlAndEnd({ rr, status = HttpStatus.Ok }: { rr: RequestResponse; status?: HttpStatus }, html: string) {\n rr.resStatusCode(status);\n rr.resSetHeader(HttpHeaders.ContentType, Mimes.TextHtml);\n await rr.resEnd(html);\n }\n\n async proceedAfter() {\n return true;\n }\n}\n"],
|
|
4
|
+
"sourcesContent": ["import { RequestResponse } from '@dooboostore/simple-boot-http-server/models/RequestResponse';\nimport { HttpHeaders } from '@dooboostore/simple-boot-http-server/codes/HttpHeaders';\nimport { Filter } from '@dooboostore/simple-boot-http-server/filters/Filter';\nimport { Mimes } from '@dooboostore/simple-boot-http-server/codes/Mimes';\nimport { HttpStatus } from '@dooboostore/simple-boot-http-server/codes/HttpStatus';\nimport { SimpleBootHttpServer } from '@dooboostore/simple-boot-http-server/SimpleBootHttpServer';\nimport path from 'path';\nimport fs from 'fs';\nimport {HttpMethod} from \"@dooboostore/simple-boot-http-server\";\n\nexport type SWCSSRConfig = {\n frontDistPath: string;\n frontDistIndexFileName?: string;\n welcomUrl?: string;\n ssrExcludeFilter?: (rr: RequestResponse) => boolean;\n /**\n * (Deprecated for Playwright)\n * Function to register components for each request.\n * Playwright evaluates real JS files loaded from your index.html,\n * so component registration happens automatically in the browser context.\n */\n registerComponents?: (window: any, rr?: RequestResponse) => Promise<void> | void;\n playwright?: {\n waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' | 'commit';\n waitForSelector?: string;\n waitForTimeout?: number;\n timeout?: number;\n ignoreTags?: string[];\n };\n};\n\n/**\n * SSR Filter specifically for Simple Web Component (SWC).\n * It utilizes Playwright to render the page robustly and perfectly matching browser specs.\n *\n * ### Playwright Setup & Requirements\n *\n * To use this filter, Playwright must be installed in your server environment:\n * 1. Install playwright package: `npm install playwright` or `pnpm add playwright`\n * 2. Install browsers: `npx playwright install chromium`\n *\n * If running inside a Docker container (e.g., Alpine Linux), you must use an image that supports Playwright\n * or manually install OS dependencies. Playwright provides official Docker images: `mcr.microsoft.com/playwright:v1.x.x-focal`.\n */\nexport class SSRSimpleWebComponentFilter implements Filter {\n private welcomUrl = 'http://localhost';\n private browser?: any;\n\n constructor(public config: SWCSSRConfig) {\n this.welcomUrl = config.welcomUrl || this.welcomUrl;\n }\n\n async onInit(app: SimpleBootHttpServer) {\n // 1. Initialize Playwright browser when the server starts\n const { chromium } = await import('playwright');\n this.browser = await chromium.launch({\n headless: true,\n args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-web-security'] // Allow CORS for local dev\n });\n console.log('\uD83D\uDE80 Playwright Browser initialized for SSR Filter');\n }\n\n async onDestroy() {\n if (this.browser) {\n await this.browser.close();\n console.log('\uD83D\uDED1 Playwright Browser closed');\n }\n }\n\n async proceedBefore({ rr, app, carrier }: { rr: RequestResponse; app: SimpleBootHttpServer; carrier: Map<string, any> }) {\n if (this.config.ssrExcludeFilter?.(rr)) {\n return true; // Bypass SSR and continue server chain\n }\n\n console.log('playwright2222222', rr.reqHeader('x-ssr-browser'));\n\n // 2. Prevent Infinite Loop:\n // If the request comes from Playwright itself, bypass SSR and serve the original static SPA file.\n if (rr.reqHeader('x-ssr-browser') === 'playwright') {\n return true;\n }\n\n if (rr.reqIsMethod(HttpMethod.GET) && rr.reqHasContentTypeHeader(Mimes.TextHtml)) {\n // Build target URL for Playwright to visit\n // Use the server's actual listening address and port if available\n const protocol = app.option.protocol || 'http';\n const host = app.option.hostname || '127.0.0.1';\n const port = app.option.port || 80;\n\n const url = rr.reqUrlObj({ host: `${host}:${port}` });\n url.protocol = protocol;\n\n const targetUrl = url.toString() ?? this.welcomUrl;\n\n if (!this.browser) {\n console.error('Playwright browser is not initialized. Bypassing SSR.');\n return true;\n }\n\n // Create a fresh context for isolation\n // IMPORTANT: We do NOT use `extraHTTPHeaders` globally because it attaches the header to EVERY request\n // Playwright makes (including CORS requests to FontAwesome or API requests).\n // Instead, we will set the header ONLY for the initial navigation request to our server.\n const context = await this.browser.newContext();\n const page = await context.newPage();\n\n try {\n // Intercept ONLY requests going to our local server and append the bypass header\n await page.route('**/*', route => {\n const request = route.request();\n\n // Only append the x-ssr-browser header for requests to the local server that are NOT API requests\n const isLocal = request.url().startsWith(targetUrl) || request.url().includes('localhost') || request.url().includes('127.0.0.1');\n const isApi = request.url().includes('/api/');\n\n if (isLocal && !isApi) {\n const headers = {\n ...request.headers(),\n 'x-ssr-browser': 'playwright'\n };\n route.continue({ headers }).catch(() => {});\n } else {\n // For external requests (like font-awesome CORS) or API calls, do not add the custom header\n route.continue().catch(() => {});\n }\n });\n\n // 3. Navigate to the page and wait for the framework to finish rendering\n // but normally Playwright just navigates to the targetUrl and loads the real SPA resources.\n\n // \uD83D\uDCA1 Playwright \u2194 Framework \uD1B5\uC2E0 \uC124\uC815\n // \uD504\uB860\uD2B8\uC5D4\uB4DC\uC758 `bootfactory.ts` \uC548\uC758 `onRouteChanged` \uB4F1\uC5D0\uC11C\n // `if (window.onPlaywrightReady) window.onPlaywrightReady()` \uB77C\uACE0 \uD638\uCD9C\uD574\uC8FC\uBA74,\n // \uC544\uB798\uC758 Promise\uAC00 \uC989\uC2DC resolve \uB418\uB3C4\uB85D \uC5F0\uACB0\uD569\uB2C8\uB2E4.\n let resolvePlaywrightReady: () => void;\n const playwrightReadyPromise = new Promise<void>(resolve => {\n resolvePlaywrightReady = resolve;\n });\n\n await page.exposeFunction('onPlaywrightReady', () => {\n console.log('\u26A1\uFE0F [SSR:Playwright] Received \"onPlaywrightReady\" signal from framework!');\n resolvePlaywrightReady();\n });\n\n // 3. Navigate to the page and wait for the framework to finish rendering\n // Since the server will return the `index.html` (due to the bypass header), Playwright will download\n // your main.js / bundle.js, evaluate it, and the Custom Elements will register themselves naturally!\n await page.goto(targetUrl, {\n waitUntil: this.config.playwright?.waitUntil ?? 'networkidle',\n timeout: this.config.playwright?.timeout ?? 15000\n });\n\n // \uD83D\uDCA1 Framework Load Wait (Crucial for SWC)\n // Wait for EITHER the framework to call `window.onPlaywrightReady()` OR a specific innerHTML condition\n await Promise.race([\n playwrightReadyPromise, // \uC9C1\uC811 exposeFunction\uC5D0\uC11C \uD2B8\uB9AC\uAC70\uB41C Promise\n\n page.waitForFunction(\n () => {\n // Fallback 1: If explicit signal variable is set\n return (window as any)._playwrightReadySignalFired === true;\n },\n { timeout: this.config.playwright?.timeout ?? 15000 }\n ),\n\n page.waitForFunction(\n () => {\n // Fallback 2: If no signal, just wait until body has content\n return document.body.innerHTML.trim().length > 100;\n },\n { timeout: this.config.playwright?.timeout ?? 15000 }\n )\n ]).catch(() => console.log('Wait for app mount timeout... proceeding anyway'));\n\n // \uAC15\uC81C\uB85C \uCEF4\uD3EC\uB10C\uD2B8 \uB80C\uB354\uB9C1 \uBC0F \uD3F0\uD2B8\uC5B4\uC378 \uB4F1 \uB9AC\uC18C\uC2A4 \uB85C\uB529 \uB300\uAE30 \uC2DC\uAC04 \uCD94\uAC00 (\uC548\uC804 \uC7A5\uCE58)\n // onPlaywrightReady \uC2E0\uD638\uAC00 \uB5A8\uC5B4\uC838\uB3C4, \uBE44\uB3D9\uAE30 \uB80C\uB354\uB9C1(SwcLoop \uB4F1)\uC774 DOM\uC5D0 \uC644\uC804\uD788 \uBC18\uC601\uB418\uB294 \uB370\n // \uC57D\uAC04\uC758 Microtask \uC9C0\uC5F0\uC774 \uC788\uC744 \uC218 \uC788\uC73C\uBBC0\uB85C \uC544\uC8FC \uC9E7\uAC8C\uB9CC \uAE30\uB2E4\uB824 \uC90D\uB2C8\uB2E4.\n // await new Promise(resolve => setTimeout(resolve, 300));\n\n // Optional: Wait for a specific component to be visible\n if (this.config.playwright?.waitForSelector) {\n await page.waitForSelector(this.config.playwright.waitForSelector, {\n timeout: this.config.playwright?.timeout ?? 15000\n });\n }\n\n // Optional: Hardcoded wait time if explicitly set (fallback)\n if (this.config.playwright?.waitForTimeout) {\n await page.waitForTimeout(this.config.playwright.waitForTimeout);\n }\n\n // 4. Set SSR Attribute before extracting HTML\n await page.evaluate(() => {\n document.body.setAttribute('ssr-use', 'true');\n });\n\n // 5. Generate Final HTML\n const html = await page.evaluate(ignoreTags => {\n const skipTags = new Set(ignoreTags || []);\n\n function serializeNode(node: Node, inBody: boolean = false): string {\n if (node.nodeType === Node.TEXT_NODE) {\n const text = node.textContent || '';\n return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n }\n if (node.nodeType === Node.COMMENT_NODE) {\n return `<!--${node.textContent}-->`;\n }\n if (node.nodeType !== Node.ELEMENT_NODE) {\n return '';\n }\n\n const el = node as Element;\n const tag = el.tagName.toLowerCase();\n const isBody = tag === 'body';\n const isInsideBody = inBody || isBody;\n\n // \uD83D\uDEAB Skip ignored tags (like <style>) ONLY if we are inside the <body>.\n // This ensures <style> tags in the <head> (global styles) are preserved!\n if (isInsideBody && skipTags.has(tag)) {\n return '';\n }\n\n let s = `<${tag}`;\n\n // 1. Serialize Attributes\n for (let i = 0; i < el.attributes.length; i++) {\n const attr = el.attributes[i];\n s += ` ${attr.name}=\"${attr.value.replace(/\"/g, '"')}\"`;\n }\n\n // Handle void elements (self-closing)\n const voidElements = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);\n if (voidElements.has(tag)) {\n s += '>';\n return s;\n }\n s += '>';\n\n // 2. Serialize Shadow DOM (Declarative Shadow DOM)\n // MUST KEEP for SSR: Without this, the user receives an empty shell until JS loads and re-renders everything.\n // This allows the browser to paint the full UI (including styles inside the component) immediately.\n if (el.shadowRoot) {\n s += `<template shadowrootmode=\"${el.shadowRoot.mode}\">`;\n for (let i = 0; i < el.shadowRoot.childNodes.length; i++) {\n s += serializeNode(el.shadowRoot.childNodes[i], isInsideBody);\n }\n s += '</template>';\n }\n\n // 3. Serialize Light DOM Children\n for (let i = 0; i < el.childNodes.length; i++) {\n s += serializeNode(el.childNodes[i], isInsideBody);\n }\n\n s += `</${tag}>`;\n return s;\n }\n\n let result = '<!DOCTYPE html>\\n<html';\n const htmlNode = document.documentElement;\n for (let i = 0; i < htmlNode.attributes.length; i++) {\n const attr = htmlNode.attributes[i];\n result += ` ${attr.name}=\"${attr.value.replace(/\"/g, '"')}\"`;\n }\n result += '>\\n';\n\n if (document.head) result += serializeNode(document.head, false);\n result += '\\n';\n if (document.body) result += serializeNode(document.body, true);\n\n result += '\\n</html>';\n return result;\n }, this.config.playwright?.ignoreTags || []);\n console.log(`\uD83D\uDC88 [SSR:Playwright] Successfully rendered ${targetUrl}`);\n\n await this.writeOkHtmlAndEnd({ rr }, this.makeHTML(html));\n } catch (e) {\n console.error(`Playwright SSR Error for ${targetUrl}:`, e);\n // If Playwright fails (e.g., timeout), we bypass SSR so the client at least gets the CSR app\n return true;\n } finally {\n await context.close();\n }\n return false; // Tell SimpleBootHttpServer to stop routing because we've handled the response\n }\n return true;\n }\n\n makeHTML(html: string) {\n if (!/^<!DOCTYPE html>/i.test(html)) {\n html = '<!DOCTYPE html>\\n' + html;\n }\n return html;\n }\n\n async writeOkHtmlAndEnd({ rr, status = HttpStatus.Ok }: { rr: RequestResponse; status?: HttpStatus }, html: string) {\n rr.resStatusCode(status);\n rr.resSetHeader(HttpHeaders.ContentType, Mimes.TextHtml);\n await rr.resEnd(html);\n }\n\n async proceedAfter() {\n return true;\n }\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|