@symbiotejs/symbiote 3.0.0-next.3 → 3.0.2
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/AI_REFERENCE.md +46 -19
- package/CHANGELOG.md +81 -79
- package/LICENSE +1 -1
- package/README.md +13 -10
- package/core/itemizeProcessor-keyed.js +2 -1
- package/core/itemizeProcessor.js +2 -1
- package/core/ownElements.js +38 -0
- package/core/tpl-processors.js +7 -2
- package/package.json +12 -8
- package/types/core/itemizeProcessor-keyed.d.ts.map +1 -1
- package/types/core/itemizeProcessor.d.ts.map +1 -1
- package/types/core/ownElements.d.ts +3 -0
- package/types/core/ownElements.d.ts.map +1 -0
- package/types/core/tpl-processors.d.ts.map +1 -1
- package/types/node/SSR.d.ts +16 -0
- package/types/node/SSR.d.ts.map +1 -0
- package/core/README.md +0 -31
- package/core/ssr.js +0 -313
- package/types/core/ssr.d.ts +0 -12
- package/types/core/ssr.d.ts.map +0 -1
package/AI_REFERENCE.md
CHANGED
|
@@ -509,43 +509,67 @@ Usage:
|
|
|
509
509
|
|
|
510
510
|
## Server-Side Rendering (SSR)
|
|
511
511
|
|
|
512
|
-
Import `
|
|
512
|
+
Import `node/SSR.js` to render components to HTML strings on the server. Requires `linkedom` (optional peer dependency).
|
|
513
|
+
|
|
514
|
+
### Basic usage — `processHtml`
|
|
513
515
|
|
|
514
516
|
```js
|
|
515
|
-
import {
|
|
517
|
+
import { SSR } from '@symbiotejs/symbiote/node/SSR.js';
|
|
518
|
+
|
|
519
|
+
await SSR.init(); // patches globals with linkedom env
|
|
520
|
+
await import('./my-component.js'); // component reg() works normally
|
|
521
|
+
|
|
522
|
+
let html = await SSR.processHtml('<div><my-component></my-component></div>');
|
|
523
|
+
// => '<div><my-component><style>...</style><template shadowrootmode="open">...</template>content</my-component></div>'
|
|
524
|
+
|
|
525
|
+
SSR.destroy(); // cleanup globals
|
|
526
|
+
```
|
|
516
527
|
|
|
517
|
-
|
|
528
|
+
`processHtml` takes any HTML string, renders all Symbiote components found within, and returns the processed HTML. If `SSR.init()` was already called, it reuses the existing environment; otherwise it auto-initializes (and auto-destroys after).
|
|
518
529
|
|
|
519
|
-
|
|
530
|
+
### Advanced — `renderToString` / `renderToStream`
|
|
520
531
|
|
|
521
|
-
|
|
532
|
+
```js
|
|
533
|
+
import { SSR } from '@symbiotejs/symbiote/node/SSR.js';
|
|
534
|
+
|
|
535
|
+
await SSR.init();
|
|
536
|
+
await import('./my-component.js');
|
|
537
|
+
|
|
538
|
+
let html = SSR.renderToString('my-component', { title: 'Hello' });
|
|
522
539
|
// => '<my-component title="Hello"><h1>Hello</h1></my-component>'
|
|
523
540
|
|
|
524
|
-
|
|
541
|
+
SSR.destroy();
|
|
525
542
|
```
|
|
526
543
|
|
|
527
544
|
### API
|
|
528
545
|
|
|
529
|
-
|
|
|
530
|
-
|
|
531
|
-
| `
|
|
532
|
-
| `
|
|
533
|
-
| `
|
|
534
|
-
| `
|
|
546
|
+
| Method | Description |
|
|
547
|
+
|--------|-------------|
|
|
548
|
+
| `SSR.init()` | `async` — creates linkedom document, polyfills CSSStyleSheet/NodeFilter/MutationObserver/adoptedStyleSheets, patches globals |
|
|
549
|
+
| `SSR.processHtml(html)` | `async` — parses HTML string, renders all custom elements, returns processed HTML. Auto-inits if needed |
|
|
550
|
+
| `SSR.renderToString(tagName, attrs?)` | Creates element, triggers `connectedCallback`, serializes to HTML string |
|
|
551
|
+
| `SSR.renderToStream(tagName, attrs?)` | Async generator — yields HTML chunks. Same output as `renderToString`, but streamed for lower TTFB |
|
|
552
|
+
| `SSR.destroy()` | Removes global patches, cleans up document |
|
|
553
|
+
|
|
554
|
+
### Styles in SSR output
|
|
555
|
+
|
|
556
|
+
- **rootStyles** → `<style>` tag as first child of the component (light DOM, deduplicated per constructor)
|
|
557
|
+
- **shadowStyles** → `<style>` inside the Declarative Shadow DOM `<template>`
|
|
558
|
+
- Both are supported simultaneously on the same component
|
|
535
559
|
|
|
536
560
|
### Streaming usage
|
|
537
561
|
|
|
538
562
|
```js
|
|
539
563
|
import http from 'node:http';
|
|
540
|
-
import {
|
|
564
|
+
import { SSR } from '@symbiotejs/symbiote/node/SSR.js';
|
|
541
565
|
|
|
542
|
-
await
|
|
566
|
+
await SSR.init();
|
|
543
567
|
import './my-app.js';
|
|
544
568
|
|
|
545
569
|
http.createServer(async (req, res) => {
|
|
546
570
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
547
571
|
res.write('<!DOCTYPE html><html><body>');
|
|
548
|
-
for await (let chunk of renderToStream('my-app')) {
|
|
572
|
+
for await (let chunk of SSR.renderToStream('my-app')) {
|
|
549
573
|
res.write(chunk);
|
|
550
574
|
}
|
|
551
575
|
res.end('</body></html>');
|
|
@@ -554,19 +578,22 @@ http.createServer(async (req, res) => {
|
|
|
554
578
|
|
|
555
579
|
### Shadow DOM output
|
|
556
580
|
|
|
557
|
-
Shadow components produce Declarative Shadow DOM markup with styles inlined:
|
|
581
|
+
Shadow components produce Declarative Shadow DOM markup with styles inlined. Light DOM content is preserved alongside the DSD template:
|
|
558
582
|
```html
|
|
559
583
|
<my-shadow>
|
|
584
|
+
<style>my-shadow { display: block; }</style>
|
|
560
585
|
<template shadowrootmode="open">
|
|
561
|
-
<style>:host {
|
|
586
|
+
<style>:host { color: red; }</style>
|
|
562
587
|
<h1>Content</h1>
|
|
588
|
+
<slot></slot>
|
|
563
589
|
</template>
|
|
590
|
+
Light DOM content here
|
|
564
591
|
</my-shadow>
|
|
565
592
|
```
|
|
566
593
|
|
|
567
594
|
### SSR context detection
|
|
568
595
|
|
|
569
|
-
`
|
|
596
|
+
`SSR.init()` sets `globalThis.__SYMBIOTE_SSR = true`. This is separate from the instance `ssrMode` flag:
|
|
570
597
|
|
|
571
598
|
| Flag | Scope | Purpose |
|
|
572
599
|
|------|-------|-------|
|
|
@@ -575,7 +602,7 @@ Shadow components produce Declarative Shadow DOM markup with styles inlined:
|
|
|
575
602
|
|
|
576
603
|
### Hydration flow
|
|
577
604
|
|
|
578
|
-
1. **Server**: `renderToString()` produces HTML with `bind=` / `itemize=` attributes preserved
|
|
605
|
+
1. **Server**: `SSR.processHtml()` / `SSR.renderToString()` produces HTML with `bind=` / `itemize=` attributes preserved
|
|
579
606
|
2. **Client**: component with `ssrMode = true` skips template injection, attaches bindings to pre-rendered DOM
|
|
580
607
|
3. State mutations on client update DOM reactively
|
|
581
608
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,43 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 3.0.0
|
|
3
|
+
## 3.0.0
|
|
4
4
|
|
|
5
5
|
### ⚠️ Breaking Changes
|
|
6
6
|
|
|
7
|
-
- **`tplProcessors`
|
|
8
|
-
The
|
|
7
|
+
- **`tplProcessors` → `templateProcessors`.**
|
|
8
|
+
The `addTemplateProcessor()` method is removed — use native `Set` methods:
|
|
9
9
|
```js
|
|
10
|
-
//
|
|
10
|
+
// 2.x:
|
|
11
11
|
this.addTemplateProcessor(myProcessor);
|
|
12
|
-
|
|
13
|
-
// After (3.x):
|
|
12
|
+
// 3.x:
|
|
14
13
|
this.templateProcessors.add(myProcessor);
|
|
15
14
|
```
|
|
16
15
|
|
|
17
|
-
- **`AppRouter.applyRoute()`
|
|
16
|
+
- **`AppRouter.applyRoute()` → `AppRouter.navigate()`.**
|
|
18
17
|
|
|
19
18
|
- **`AppRouter` removed from main entry point.**
|
|
20
|
-
Now imported directly from its module:
|
|
21
19
|
```js
|
|
22
|
-
//
|
|
20
|
+
// 2.x:
|
|
23
21
|
import { AppRouter } from '@symbiotejs/symbiote';
|
|
24
|
-
|
|
25
|
-
// After (3.x):
|
|
22
|
+
// 3.x:
|
|
26
23
|
import { AppRouter } from '@symbiotejs/symbiote/core/AppRouter.js';
|
|
27
24
|
```
|
|
28
25
|
|
|
29
|
-
- **`#disconnectTimeout` renamed to `#destroyTimeout`.**
|
|
30
|
-
Internal field renamed for clarity. No public API impact.
|
|
31
|
-
|
|
32
26
|
- **Computed properties: cross-context requires explicit deps.**
|
|
33
|
-
|
|
27
|
+
Local computeds (same-context) keep working unchanged. Cross-context now uses object syntax:
|
|
34
28
|
```js
|
|
35
|
-
//
|
|
29
|
+
// 2.x — implicit via global scan:
|
|
36
30
|
init$ = {
|
|
37
31
|
'+total': () => this.$['APP/score'] + this.$.local,
|
|
38
32
|
};
|
|
39
|
-
|
|
40
|
-
// After (3.x) — explicit deps required:
|
|
33
|
+
// 3.x — explicit deps:
|
|
41
34
|
init$ = {
|
|
42
35
|
'+total': {
|
|
43
36
|
deps: ['APP/score'],
|
|
@@ -45,57 +38,73 @@
|
|
|
45
38
|
},
|
|
46
39
|
};
|
|
47
40
|
```
|
|
48
|
-
|
|
41
|
+
|
|
42
|
+
- **Shared context (`*prop`) simplified.**
|
|
43
|
+
Removed `ctxOwner` / `ctx-owner`. First-registered value always wins. Dev-mode warnings when `*prop` used without `ctx` attribute.
|
|
49
44
|
|
|
50
45
|
### Performance
|
|
51
46
|
|
|
52
47
|
- **Computed properties: per-instance dependency tracking.**
|
|
53
|
-
Replaced global
|
|
48
|
+
Replaced global scan with per-instance auto-tracking. Up to **676× faster** for local computeds, **14×** for sparse scenarios.
|
|
54
49
|
|
|
55
|
-
- **Microtask batching
|
|
56
|
-
|
|
50
|
+
- **Microtask batching.**
|
|
51
|
+
All computed recalculation and internal scheduling uses `queueMicrotask` instead of `setTimeout`.
|
|
57
52
|
|
|
58
|
-
|
|
53
|
+
- **`#parseProp` fast path.**
|
|
54
|
+
`charCodeAt` checks skip full string parsing for common local properties — the most frequent case.
|
|
59
55
|
|
|
60
|
-
-
|
|
61
|
-
|
|
56
|
+
- **`$` proxy inlined fast path.**
|
|
57
|
+
Both `get` and `set` traps bypass `#parseProp` entirely for local props.
|
|
62
58
|
|
|
63
|
-
-
|
|
64
|
-
|
|
59
|
+
- **`PubSub.pub()` direct value pass.**
|
|
60
|
+
Eliminates redundant `read()` on every state update.
|
|
65
61
|
|
|
66
|
-
-
|
|
67
|
-
|
|
62
|
+
- **Dev warnings gated by `PubSub.devMode`.**
|
|
63
|
+
Type-mismatch checks have zero overhead in production.
|
|
68
64
|
|
|
69
|
-
- **`
|
|
70
|
-
|
|
65
|
+
- **`txtNodesProcessor` early exit.**
|
|
66
|
+
Skips text-node scanning when template contains no `{{` tokens.
|
|
71
67
|
|
|
72
|
-
-
|
|
73
|
-
|
|
68
|
+
- **`localCtx` direct construction.**
|
|
69
|
+
Uses `new PubSub({})` instead of `PubSub.registerCtx({})`, bypassing global store for component-scoped state.
|
|
74
70
|
|
|
75
|
-
- **
|
|
76
|
-
New SSR module: `initSSR()` creates a linkedom-backed DOM environment with polyfills (CSSStyleSheet, NodeFilter, MutationObserver) and sets `globalThis.__SYMBIOTE_SSR`. `renderToString(tagName, attrs?)` renders components to HTML with Declarative Shadow DOM and inlined `<style>`. `renderToStream(tagName, attrs?)` async generator yields HTML chunks for lower TTFB and memory usage. Binding attributes (`bind`, `ref`, `itemize`) are preserved in output for client-side hydration. `linkedom` is an optional peer dependency.
|
|
71
|
+
- **`hasOwnProperty` → `in` operator** across `PubSub` internals.
|
|
77
72
|
|
|
78
|
-
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
- **`itemizeProcessor-keyed.js` — optional optimized itemize processor.**
|
|
74
|
+
Drop-in replacement with reference-equality fast paths and key-based reconciliation. Up to **3× faster** for appends, **2×** for in-place updates, **32×** for no-ops:
|
|
75
|
+
```js
|
|
76
|
+
import { itemizeProcessor } from '@symbiotejs/symbiote/core/itemizeProcessor-keyed.js';
|
|
77
|
+
import { itemizeProcessor as defaultProcessor } from '@symbiotejs/symbiote/core/itemizeProcessor.js';
|
|
82
78
|
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
class BigList extends Symbiote {
|
|
80
|
+
constructor() {
|
|
81
|
+
super();
|
|
82
|
+
this.templateProcessors.delete(defaultProcessor);
|
|
83
|
+
this.templateProcessors = new Set([itemizeProcessor, ...this.templateProcessors]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
Template `innerHTML` writes now use a `'symbiote'` Trusted Types policy when the API is available. Compatible with `require-trusted-types-for 'script'` CSP headers. Zero overhead when Trusted Types not enabled.
|
|
88
|
+
### Added
|
|
88
89
|
|
|
89
|
-
|
|
90
|
+
- **Server-side rendering (`node/SSR.js`).**
|
|
91
|
+
`SSR` class with static methods for server-side rendering. `SSR.processHtml(html)` renders any HTML string with embedded components. `SSR.renderToString(tagName, attrs?)` renders a single component. `SSR.renderToStream(tagName, attrs?)` async generator yields HTML chunks. Declarative Shadow DOM with inlined styles. rootStyles emitted as `<style>` tags in light DOM. Light DOM content preserved. Binding attributes preserved for client hydration. `linkedom` is an optional peer dependency.
|
|
92
|
+
```js
|
|
93
|
+
import { SSR } from '@symbiotejs/symbiote/node/SSR.js';
|
|
94
|
+
await SSR.init();
|
|
95
|
+
await import('./my-app.js');
|
|
96
|
+
let html = await SSR.processHtml('<my-app>content</my-app>');
|
|
97
|
+
SSR.destroy();
|
|
98
|
+
```
|
|
90
99
|
|
|
91
|
-
-
|
|
92
|
-
`
|
|
100
|
+
- **Declarative Shadow DOM hydration (`ssrMode`).**
|
|
101
|
+
`ssrMode = true` hydrates pre-rendered content (light DOM + `<template shadowrootmode>`). Template injection skipped; bindings attach to existing DOM. Shadow styles applied via `adoptedStyleSheets`.
|
|
93
102
|
|
|
94
|
-
-
|
|
95
|
-
|
|
103
|
+
- **Exit animation hook (`animateOut`).**
|
|
104
|
+
Sets `[leaving]` attribute, waits for CSS `transitionend`, removes element. Integrated into itemize processors — items with transitions animate out automatically. Enter animations use `@starting-style`.
|
|
96
105
|
|
|
97
106
|
- **`AppRouter`: path-based routing.**
|
|
98
|
-
Routes with
|
|
107
|
+
Routes with `pattern` key use path-based URLs with `:param` extraction:
|
|
99
108
|
```js
|
|
100
109
|
AppRouter.initRoutingCtx('R', {
|
|
101
110
|
home: { pattern: '/', title: 'Home', default: true },
|
|
@@ -103,54 +112,47 @@
|
|
|
103
112
|
});
|
|
104
113
|
// /users/42 → { route: 'user', options: { id: '42' } }
|
|
105
114
|
```
|
|
106
|
-
|
|
115
|
+
Query-string routes remain fully backward compatible.
|
|
107
116
|
|
|
108
|
-
-
|
|
109
|
-
|
|
117
|
+
- **Route guards — `AppRouter.beforeRoute(fn)`.**
|
|
118
|
+
Return `false` to cancel, a route string to redirect:
|
|
110
119
|
```js
|
|
111
120
|
let unsub = AppRouter.beforeRoute((to, from) => {
|
|
112
121
|
if (!isAuth && to.route === 'settings') return 'login';
|
|
113
122
|
});
|
|
114
123
|
```
|
|
115
124
|
|
|
116
|
-
- **Lazy loaded
|
|
117
|
-
|
|
125
|
+
- **Lazy loaded routes.**
|
|
126
|
+
`load` in route descriptors for dynamic imports, cached automatically:
|
|
118
127
|
```js
|
|
119
128
|
{ pattern: '/settings', load: () => import('./pages/settings.js') }
|
|
120
129
|
```
|
|
121
130
|
|
|
122
|
-
- **AI_REFERENCE.md** — comprehensive AI context file for code assistants, covering full API surface, template syntax, state management, lifecycle, styling, routing, itemize, and common mistakes.
|
|
123
|
-
|
|
124
131
|
- **Event handler method fallback.**
|
|
125
|
-
`on*` bindings
|
|
132
|
+
`on*` bindings fall back to class methods when no `init$` property found:
|
|
126
133
|
```js
|
|
127
|
-
// Works without init$ entry — class method is used as fallback:
|
|
128
134
|
onSubmit() { console.log('submitted'); }
|
|
129
135
|
```
|
|
130
136
|
|
|
137
|
+
- **`Symbiote.devMode` flag.**
|
|
138
|
+
Enables verbose warnings (unresolved bindings, tag names, available contexts). Also wires `PubSub.devMode`.
|
|
139
|
+
|
|
140
|
+
- **`reg()` returns the class itself.**
|
|
141
|
+
Enables `export default MyComponent.reg('my-component')`.
|
|
142
|
+
|
|
131
143
|
- **`destructionDelay` instance property.**
|
|
132
|
-
Configurable delay (default `100`ms) before
|
|
133
|
-
```js
|
|
134
|
-
class MyComponent extends Symbiote {
|
|
135
|
-
destructionDelay = 0; // instant cleanup
|
|
136
|
-
}
|
|
137
|
-
```
|
|
144
|
+
Configurable delay (default `100`ms) before cleanup in `disconnectedCallback`.
|
|
138
145
|
|
|
139
|
-
-
|
|
140
|
-
|
|
141
|
-
```js
|
|
142
|
-
import { itemizeProcessor } from '@symbiotejs/symbiote/core/itemizeProcessor-keyed.js';
|
|
143
|
-
import { itemizeProcessor as defaultProcessor } from '@symbiotejs/symbiote/core/itemizeProcessor.js';
|
|
146
|
+
- **Trusted Types support.**
|
|
147
|
+
Template writes use `'symbiote'` Trusted Types policy when available. Zero overhead otherwise.
|
|
144
148
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
this.templateProcessors = new Set([itemizeProcessor, ...this.templateProcessors]);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
```
|
|
149
|
+
- **`this` in template detection.**
|
|
150
|
+
`html` fires `console.error` when `${this.…}` used in template (templates are context-free).
|
|
151
|
+
|
|
152
|
+
- **AI_REFERENCE.md** — comprehensive context file for code assistants.
|
|
153
153
|
|
|
154
|
-
###
|
|
154
|
+
### Fixed
|
|
155
155
|
|
|
156
|
-
-
|
|
156
|
+
- `css()` trailing `undefined` when no interpolations exist.
|
|
157
|
+
- `new DocumentFragment()` → `document.createDocumentFragment()` for linkedom compatibility.
|
|
158
|
+
- `txtNodesProcessor` null check for `fr.textContent` in SSR environments.
|
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2021-present
|
|
3
|
+
Copyright (c) 2021-present rnd-pro.com. All rights reserved.
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Symbiote.js gives you the convenience of a modern framework while staying close
|
|
|
11
11
|
|
|
12
12
|
## What's new in 3.x
|
|
13
13
|
|
|
14
|
-
- **Server-Side Rendering** — render components to HTML
|
|
14
|
+
- **Server-Side Rendering** — render components to HTML with `SSR.processHtml()` or stream chunks with `SSR.renderToStream()`. Client-side hydration via `ssrMode` attaches bindings to existing DOM without re-rendering.
|
|
15
15
|
- **Computed properties** — reactive derived state with microtask batching.
|
|
16
16
|
- **Path-based router** — optional `AppRouter` module with `:param` extraction, route guards, and lazy loading.
|
|
17
17
|
- **Exit animations** — `animateOut(el)` for CSS-driven exit transitions, integrated into itemize API.
|
|
@@ -60,16 +60,16 @@ import Symbiote, { html, css } from '@symbiotejs/symbiote';
|
|
|
60
60
|
|
|
61
61
|
## SSR — simpler than you'd expect
|
|
62
62
|
|
|
63
|
-
Symbiote's SSR doesn't need a virtual DOM, a reconciler, or framework-specific server packages. It's
|
|
63
|
+
Symbiote's SSR doesn't need a virtual DOM, a reconciler, or framework-specific server packages. It's one class:
|
|
64
64
|
|
|
65
65
|
```js
|
|
66
|
-
import {
|
|
66
|
+
import { SSR } from '@symbiotejs/symbiote/node/SSR.js';
|
|
67
67
|
|
|
68
|
-
await
|
|
69
|
-
await import('./my-app.js');
|
|
68
|
+
await SSR.init(); // patches globals with linkedom
|
|
69
|
+
await import('./my-app.js'); // your components register normally
|
|
70
70
|
|
|
71
|
-
let html =
|
|
72
|
-
|
|
71
|
+
let html = await SSR.processHtml('<my-app>slot content</my-app>');
|
|
72
|
+
SSR.destroy(); // cleanup
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
On the client, components with `ssrMode = true` skip template injection and attach bindings to the existing DOM. State mutations work immediately — no hydration step, no reconciliation, no diffing.
|
|
@@ -79,12 +79,15 @@ On the client, components with `ssrMode = true` skip template injection and atta
|
|
|
79
79
|
For large pages, stream HTML chunks instead of building a string:
|
|
80
80
|
|
|
81
81
|
```js
|
|
82
|
-
import {
|
|
82
|
+
import { SSR } from '@symbiotejs/symbiote/node/SSR.js';
|
|
83
|
+
|
|
84
|
+
await SSR.init();
|
|
85
|
+
await import('./my-app.js');
|
|
83
86
|
|
|
84
87
|
http.createServer(async (req, res) => {
|
|
85
88
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
86
89
|
res.write('<!DOCTYPE html><html><body>');
|
|
87
|
-
for await (let chunk of renderToStream('my-app')) {
|
|
90
|
+
for await (let chunk of SSR.renderToStream('my-app')) {
|
|
88
91
|
res.write(chunk);
|
|
89
92
|
}
|
|
90
93
|
res.end('</body></html>');
|
|
@@ -97,7 +100,7 @@ http.createServer(async (req, res) => {
|
|
|
97
100
|
|--|----------------|---------------------|--------------------------|
|
|
98
101
|
| **Architecture** | Binding-based. Client attaches to existing DOM | Virtual DOM. Client re-renders and diffs against server HTML | Template-based with comment markers |
|
|
99
102
|
| **Hydration** | `ssrMode = true` — one flag, no diffing | `hydrateRoot()` — must produce identical output or errors | Requires `ssr-client` + hydrate support module loaded before Lit |
|
|
100
|
-
| **Packages** | 1 module (`
|
|
103
|
+
| **Packages** | 1 module (`node/SSR.js`) + `linkedom` peer dep | Next.js framework (full buy-in) | 3 packages: `ssr`, `ssr-client`, `ssr-dom-shim` |
|
|
101
104
|
| **Streaming** | `renderToStream()` async generator | `renderToPipeableStream()` | Iterable `RenderResult` |
|
|
102
105
|
| **Mismatch handling** | Not needed — bindings attach to whatever DOM exists | Hard errors or visual glitches if server/client output differs | N/A |
|
|
103
106
|
| **Component code** | Same code, no changes | Server Components vs Client Components split | Same code, but load-order constraints |
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DICT } from './dictionary.js';
|
|
2
2
|
import { animateOut } from './animateOut.js';
|
|
3
|
+
import { ownElements } from './ownElements.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Optimized itemize template processor.
|
|
@@ -29,7 +30,7 @@ import { animateOut } from './animateOut.js';
|
|
|
29
30
|
* @param {T} fnCtx
|
|
30
31
|
*/
|
|
31
32
|
export function itemizeProcessor(fr, fnCtx) {
|
|
32
|
-
|
|
33
|
+
ownElements(fr, `[${DICT.LIST_ATTR}]`).filter((el) => {
|
|
33
34
|
return !el.matches(`[${DICT.LIST_ATTR}] [${DICT.LIST_ATTR}]`);
|
|
34
35
|
}).forEach((el) => {
|
|
35
36
|
let itemTag = el.getAttribute(DICT.LIST_ITEM_TAG_ATTR);
|
package/core/itemizeProcessor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DICT } from './dictionary.js';
|
|
2
2
|
import { animateOut } from './animateOut.js';
|
|
3
|
+
import { ownElements } from './ownElements.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @template {import('./Symbiote.js').Symbiote} T
|
|
@@ -7,7 +8,7 @@ import { animateOut } from './animateOut.js';
|
|
|
7
8
|
* @param {T} fnCtx
|
|
8
9
|
*/
|
|
9
10
|
export function itemizeProcessor(fr, fnCtx) {
|
|
10
|
-
|
|
11
|
+
ownElements(fr, `[${DICT.LIST_ATTR}]`).filter((el) => {
|
|
11
12
|
return !el.matches(`[${DICT.LIST_ATTR}] [${DICT.LIST_ATTR}]`);
|
|
12
13
|
}).forEach((el) => {
|
|
13
14
|
let itemTag = el.getAttribute(DICT.LIST_ITEM_TAG_ATTR);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* querySelectorAll scoped to own elements only.
|
|
3
|
+
* During ssrMode hydration, `fr` is the component itself (an HTMLElement).
|
|
4
|
+
* querySelectorAll matches ALL descendants, including those inside child components.
|
|
5
|
+
* This helper excludes elements whose closest custom-element ancestor is not `fr`.
|
|
6
|
+
* @param {Element | DocumentFragment} fr
|
|
7
|
+
* @param {string} selector
|
|
8
|
+
* @returns {Element[]}
|
|
9
|
+
*/
|
|
10
|
+
export function ownElements(fr, selector) {
|
|
11
|
+
let all = [...fr.querySelectorAll(selector)];
|
|
12
|
+
if (!(fr instanceof HTMLElement) || !fr.localName?.includes('-')) {
|
|
13
|
+
return all;
|
|
14
|
+
}
|
|
15
|
+
return all.filter((el) => {
|
|
16
|
+
let parent = el.parentElement;
|
|
17
|
+
while (parent && parent !== fr) {
|
|
18
|
+
if (parent.localName?.includes('-')) return false;
|
|
19
|
+
parent = parent.parentElement;
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if a node belongs to `root` and not to a nested custom element.
|
|
27
|
+
* @param {Node} node
|
|
28
|
+
* @param {Element} root
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
*/
|
|
31
|
+
export function isOwnNode(node, root) {
|
|
32
|
+
let parent = node.parentElement;
|
|
33
|
+
while (parent && parent !== root) {
|
|
34
|
+
if (parent.localName?.includes('-')) return false;
|
|
35
|
+
parent = parent.parentElement;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
package/core/tpl-processors.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { DICT } from './dictionary.js';
|
|
2
2
|
import { setNestedProp } from '../utils/setNestedProp.js';
|
|
3
|
+
import { ownElements, isOwnNode } from './ownElements.js';
|
|
3
4
|
|
|
4
5
|
// Should go first among other processors:
|
|
5
6
|
import { itemizeProcessor } from './itemizeProcessor.js';
|
|
6
7
|
|
|
8
|
+
|
|
9
|
+
|
|
7
10
|
/**
|
|
8
11
|
* @template {import('./Symbiote.js').Symbiote} T
|
|
9
12
|
* @param {DocumentFragment} fr
|
|
10
13
|
* @param {T} fnCtx
|
|
11
14
|
*/
|
|
12
15
|
function refProcessor(fr, fnCtx) {
|
|
13
|
-
|
|
16
|
+
ownElements(fr, `[${DICT.EL_REF_ATTR}]`).forEach((/** @type {HTMLElement} */ el) => {
|
|
14
17
|
let refName = el.getAttribute(DICT.EL_REF_ATTR);
|
|
15
18
|
fnCtx.ref[refName] = el;
|
|
16
19
|
if (!globalThis.__SYMBIOTE_SSR) {
|
|
@@ -25,7 +28,7 @@ function refProcessor(fr, fnCtx) {
|
|
|
25
28
|
* @param {T} fnCtx
|
|
26
29
|
*/
|
|
27
30
|
function domBindProcessor(fr, fnCtx) {
|
|
28
|
-
|
|
31
|
+
ownElements(fr, `[${DICT.BIND_ATTR}]`).forEach((el) => {
|
|
29
32
|
let subStr = el.getAttribute(DICT.BIND_ATTR);
|
|
30
33
|
let keyValArr = subStr.split(';');
|
|
31
34
|
keyValArr.forEach((keyValStr) => {
|
|
@@ -104,10 +107,12 @@ function domBindProcessor(fr, fnCtx) {
|
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
function getTextNodesWithTokens(el) {
|
|
110
|
+
let isCustomEl = el instanceof HTMLElement && el.localName?.includes('-');
|
|
107
111
|
let node;
|
|
108
112
|
let result = [];
|
|
109
113
|
let walk = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, {
|
|
110
114
|
acceptNode: (txt) => {
|
|
115
|
+
if (isCustomEl && !isOwnNode(txt, el)) return NodeFilter.FILTER_REJECT;
|
|
111
116
|
return !txt.parentElement?.hasAttribute(DICT.TEXT_NODE_SKIP_ATTR)
|
|
112
117
|
&& txt.textContent.includes(DICT.TEXT_NODE_OPEN_TOKEN)
|
|
113
118
|
&& txt.textContent.includes(DICT.TEXT_NODE_CLOSE_TOKEN)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@symbiotejs/symbiote",
|
|
4
|
-
"version": "3.0.
|
|
4
|
+
"version": "3.0.2",
|
|
5
5
|
"description": "Symbiote.js - zero-dependency close-to-platform frontend library to build super-powered web components",
|
|
6
6
|
"author": "team@rnd-pro.com",
|
|
7
7
|
"license": "MIT",
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
"prepare": "git config core.hooksPath .git-hooks",
|
|
10
10
|
"types": "rm -rf types && tsc -p dts.cfg.json && node scripts/clean-dts.js",
|
|
11
11
|
"prepublishOnly": "npm test",
|
|
12
|
-
"
|
|
12
|
+
"pub": "npm run types && node scripts/update-exports.js && npm publish --tag beta",
|
|
13
13
|
"postinstall": "node scripts/postinstall.js",
|
|
14
|
-
"test": "node --test test/*.test.js && npx playwright test",
|
|
15
|
-
"test:unit": "node --test test/*.test.js",
|
|
14
|
+
"test": "node --test test/node/*.test.js && npx playwright test",
|
|
15
|
+
"test:unit": "node --test test/node/*.test.js",
|
|
16
16
|
"test:browser": "npx playwright test"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
@@ -68,14 +68,14 @@
|
|
|
68
68
|
"types": "./types/core/itemizeProcessor.d.ts",
|
|
69
69
|
"default": "./core/itemizeProcessor.js"
|
|
70
70
|
},
|
|
71
|
+
"./core/ownElements.js": {
|
|
72
|
+
"types": "./types/core/ownElements.d.ts",
|
|
73
|
+
"default": "./core/ownElements.js"
|
|
74
|
+
},
|
|
71
75
|
"./core/slotProcessor.js": {
|
|
72
76
|
"types": "./types/core/slotProcessor.d.ts",
|
|
73
77
|
"default": "./core/slotProcessor.js"
|
|
74
78
|
},
|
|
75
|
-
"./core/ssr.js": {
|
|
76
|
-
"types": "./types/core/ssr.d.ts",
|
|
77
|
-
"default": "./core/ssr.js"
|
|
78
|
-
},
|
|
79
79
|
"./core/tpl-processors.js": {
|
|
80
80
|
"types": "./types/core/tpl-processors.d.ts",
|
|
81
81
|
"default": "./core/tpl-processors.js"
|
|
@@ -122,7 +122,11 @@
|
|
|
122
122
|
},
|
|
123
123
|
"keywords": [
|
|
124
124
|
"web components",
|
|
125
|
+
"SSR",
|
|
126
|
+
"server side rendering for web components",
|
|
125
127
|
"ui library",
|
|
128
|
+
"data flow",
|
|
129
|
+
"design system",
|
|
126
130
|
"symbiote.js",
|
|
127
131
|
"symbiote",
|
|
128
132
|
"widget",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"itemizeProcessor-keyed.d.ts","sourceRoot":"","sources":["../../core/itemizeProcessor-keyed.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"itemizeProcessor-keyed.d.ts","sourceRoot":"","sources":["../../core/itemizeProcessor-keyed.js"],"names":[],"mappings":"AA+BA,iCAJgD,CAAC,SAApC,qCAAkC,MACpC,gBAAgB,SAChB,CAAC,QA6MX"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"itemizeProcessor.d.ts","sourceRoot":"","sources":["../../core/itemizeProcessor.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"itemizeProcessor.d.ts","sourceRoot":"","sources":["../../core/itemizeProcessor.js"],"names":[],"mappings":"AASA,iCAJgD,CAAC,SAApC,qCAAkC,MACpC,gBAAgB,SAChB,CAAC,QAoFX"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ownElements.d.ts","sourceRoot":"","sources":["../../core/ownElements.js"],"names":[],"mappings":"AASA,gCAJW,OAAO,GAAG,gBAAgB,YAC1B,MAAM,GACJ,OAAO,EAAE,CAerB;AAQD,gCAJW,IAAI,QACJ,OAAO,GACL,OAAO,CASnB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tpl-processors.d.ts","sourceRoot":"","sources":["../../core/tpl-processors.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"tpl-processors.d.ts","sourceRoot":"","sources":["../../core/tpl-processors.js"],"names":[],"mappings":"0BAgIgD,CAAC,SAApC,qCAAkC,MACpC,gBAAgB,SAChB,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class SSR {
|
|
2
|
+
static init(): Promise<{
|
|
3
|
+
document: any;
|
|
4
|
+
window: any;
|
|
5
|
+
}>;
|
|
6
|
+
static destroy(): void;
|
|
7
|
+
static processHtml(html: string): Promise<string>;
|
|
8
|
+
static renderToString(tagName: string, attrs?: {
|
|
9
|
+
[x: string]: string;
|
|
10
|
+
}): string;
|
|
11
|
+
static renderToStream(tagName: string, attrs?: {
|
|
12
|
+
[x: string]: string;
|
|
13
|
+
}): AsyncGenerator<string>;
|
|
14
|
+
}
|
|
15
|
+
export default SSR;
|
|
16
|
+
//# sourceMappingURL=SSR.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SSR.d.ts","sourceRoot":"","sources":["../../node/SSR.js"],"names":[],"mappings":"AAuPA;IAEE,8BAAmB;IACnB,8BAAmB;IAMnB;;;OAkEC;IAMD,uBAeC;IAeD,yBATW,MAAM,GACJ,OAAO,CAAC,MAAM,CAAC,CAwB3B;IAUD,+BAJW,MAAM;;QAEJ,MAAM,CAclB;IAUD,+BAJW,MAAM;;QAEJ,cAAc,CAAC,MAAM,CAAC,CAalC;CACF"}
|
package/core/README.md
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
## Folder contents
|
|
2
|
-
|
|
3
|
-
### index.js
|
|
4
|
-
All-in-one exports.
|
|
5
|
-
|
|
6
|
-
### Symbiote.js
|
|
7
|
-
Base component class. Major utility for the web-component creation, template data binding and data management.
|
|
8
|
-
|
|
9
|
-
### html.js
|
|
10
|
-
Template literal tag-function, that transforms interpolated binding descriptions into resulting html.
|
|
11
|
-
|
|
12
|
-
### css.js
|
|
13
|
-
Template literal tag-function, that creates the CSSStyleSheet instance.
|
|
14
|
-
|
|
15
|
-
### PubSub.js
|
|
16
|
-
Implements data layer for the local component context and the top level context both. The state management approach is based on simple well known pub/sub pattern.
|
|
17
|
-
|
|
18
|
-
### AppRouter.js
|
|
19
|
-
SPA routing utility. Based on browser-native History API.
|
|
20
|
-
|
|
21
|
-
### tpl-rpcessors.js
|
|
22
|
-
Template processing functions. Implements basic template processing flow.
|
|
23
|
-
|
|
24
|
-
### itemizeProcessor.js
|
|
25
|
-
Dynamic list items rendering implementation.
|
|
26
|
-
|
|
27
|
-
### dictionary.js
|
|
28
|
-
Dictionary for the set of the basic keys.
|
|
29
|
-
|
|
30
|
-
### slotProcessor.js
|
|
31
|
-
Light DOM support for the template `slot`s. This processor is optional since 2.x. and excluded from default template processing pipeline.
|
package/core/ssr.js
DELETED
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module ssr
|
|
3
|
-
* Server-side rendering for Symbiote.js components.
|
|
4
|
-
* Requires `linkedom` as a peer dependency.
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* ```js
|
|
8
|
-
* import { initSSR, renderToString } from '@symbiotejs/symbiote/core/ssr.js';
|
|
9
|
-
* initSSR();
|
|
10
|
-
* import './my-component.js';
|
|
11
|
-
* let html = renderToString('my-component', { title: 'Hello' });
|
|
12
|
-
* ```
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
let ssrDocument = null;
|
|
16
|
-
let ssrWindow = null;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Initialize the SSR environment using linkedom.
|
|
20
|
-
* Must be called before importing any Symbiote components.
|
|
21
|
-
*/
|
|
22
|
-
export async function initSSR() {
|
|
23
|
-
// @ts-ignore
|
|
24
|
-
let { parseHTML } = /** @type {any} */ (await import('linkedom'));
|
|
25
|
-
let { document, window, HTMLElement, customElements, DocumentFragment, NodeFilter, MutationObserver } = parseHTML('<!DOCTYPE html><html><head></head><body></body></html>');
|
|
26
|
-
|
|
27
|
-
ssrDocument = document;
|
|
28
|
-
ssrWindow = window;
|
|
29
|
-
|
|
30
|
-
// Polyfill CSSStyleSheet for linkedom:
|
|
31
|
-
if (!window.CSSStyleSheet || !('replaceSync' in (window.CSSStyleSheet?.prototype || {}))) {
|
|
32
|
-
class SSRStyleSheet {
|
|
33
|
-
#cssText = '';
|
|
34
|
-
replaceSync(text) {
|
|
35
|
-
this.#cssText = text;
|
|
36
|
-
}
|
|
37
|
-
replace(text) {
|
|
38
|
-
this.#cssText = text;
|
|
39
|
-
return Promise.resolve(this);
|
|
40
|
-
}
|
|
41
|
-
get cssText() {
|
|
42
|
-
return this.#cssText;
|
|
43
|
-
}
|
|
44
|
-
get cssRules() {
|
|
45
|
-
return [];
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
// @ts-ignore — SSR polyfill doesn't need full CSSStyleSheet interface
|
|
49
|
-
window.CSSStyleSheet = SSRStyleSheet;
|
|
50
|
-
// @ts-ignore
|
|
51
|
-
globalThis.CSSStyleSheet = SSRStyleSheet;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Polyfill NodeFilter (linkedom may not expose it):
|
|
55
|
-
let nodeFilter = NodeFilter || {
|
|
56
|
-
SHOW_ALL: 0xFFFFFFFF,
|
|
57
|
-
SHOW_ELEMENT: 0x1,
|
|
58
|
-
SHOW_TEXT: 0x4,
|
|
59
|
-
SHOW_COMMENT: 0x80,
|
|
60
|
-
FILTER_ACCEPT: 1,
|
|
61
|
-
FILTER_REJECT: 2,
|
|
62
|
-
FILTER_SKIP: 3,
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// Polyfill MutationObserver (not needed for SSR one-shot render):
|
|
66
|
-
let mutationObserver = MutationObserver || class {
|
|
67
|
-
observe() {}
|
|
68
|
-
disconnect() {}
|
|
69
|
-
takeRecords() { return []; }
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
// Patch globals:
|
|
73
|
-
globalThis.__SYMBIOTE_SSR = true;
|
|
74
|
-
globalThis.document = document;
|
|
75
|
-
globalThis.window = window;
|
|
76
|
-
globalThis.HTMLElement = HTMLElement;
|
|
77
|
-
globalThis.customElements = customElements;
|
|
78
|
-
globalThis.DocumentFragment = DocumentFragment;
|
|
79
|
-
globalThis.NodeFilter = nodeFilter;
|
|
80
|
-
globalThis.MutationObserver = mutationObserver;
|
|
81
|
-
|
|
82
|
-
return { document, window };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Extract CSS text from a stylesheet (works with both CSSStyleSheet and SSR polyfill).
|
|
87
|
-
* @param {CSSStyleSheet | {cssText: string}} sheet
|
|
88
|
-
* @returns {string}
|
|
89
|
-
*/
|
|
90
|
-
function extractCSS(sheet) {
|
|
91
|
-
if ('cssText' in sheet && typeof sheet.cssText === 'string') {
|
|
92
|
-
return sheet.cssText;
|
|
93
|
-
}
|
|
94
|
-
// Standard CSSStyleSheet — extract from cssRules:
|
|
95
|
-
let text = '';
|
|
96
|
-
try {
|
|
97
|
-
// @ts-ignore — cssRules may not exist on SSR polyfill
|
|
98
|
-
for (let rule of sheet.cssRules) {
|
|
99
|
-
text += rule.cssText + '\n';
|
|
100
|
-
}
|
|
101
|
-
} catch {
|
|
102
|
-
// Security restrictions on some stylesheets
|
|
103
|
-
}
|
|
104
|
-
return text;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Render a Symbiote component to an HTML string.
|
|
109
|
-
* @param {string} tagName - Custom element tag name
|
|
110
|
-
* @param {Object<string, string>} [attrs] - Attributes to set on the element
|
|
111
|
-
* @returns {string} HTML string with Declarative Shadow DOM if applicable
|
|
112
|
-
*/
|
|
113
|
-
export function renderToString(tagName, attrs = {}) {
|
|
114
|
-
if (!ssrDocument) {
|
|
115
|
-
throw new Error('[Symbiote SSR] Call initSSR() before renderToString()');
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
let el = ssrDocument.createElement(tagName);
|
|
119
|
-
|
|
120
|
-
for (let [key, val] of Object.entries(attrs)) {
|
|
121
|
-
el.setAttribute(key, String(val));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Trigger component lifecycle:
|
|
125
|
-
ssrDocument.body.appendChild(el);
|
|
126
|
-
|
|
127
|
-
// Build output HTML:
|
|
128
|
-
let html = serializeElement(el);
|
|
129
|
-
|
|
130
|
-
// Cleanup:
|
|
131
|
-
el.remove();
|
|
132
|
-
|
|
133
|
-
return html;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Serialize a custom element to HTML with DSD support.
|
|
138
|
-
* @param {HTMLElement} el
|
|
139
|
-
* @returns {string}
|
|
140
|
-
*/
|
|
141
|
-
function serializeElement(el) {
|
|
142
|
-
let tagName = el.localName;
|
|
143
|
-
let attrsStr = '';
|
|
144
|
-
if (el.attributes) {
|
|
145
|
-
for (let attr of el.attributes) {
|
|
146
|
-
attrsStr += ` ${attr.name}="${attr.value}"`;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
let innerContent = '';
|
|
151
|
-
|
|
152
|
-
// Declarative Shadow DOM:
|
|
153
|
-
if (el.shadowRoot) {
|
|
154
|
-
let shadowHTML = '';
|
|
155
|
-
|
|
156
|
-
// Inline shadow styles:
|
|
157
|
-
let ctor = /** @type {any} */ (el).constructor;
|
|
158
|
-
if (ctor.shadowStyleSheets) {
|
|
159
|
-
for (let sheet of ctor.shadowStyleSheets) {
|
|
160
|
-
let cssText = extractCSS(sheet);
|
|
161
|
-
if (cssText) {
|
|
162
|
-
shadowHTML += `<style>${cssText}</style>`;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
shadowHTML += el.shadowRoot.innerHTML;
|
|
168
|
-
innerContent += `<template shadowrootmode="open">${shadowHTML}</template>`;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Light DOM content:
|
|
172
|
-
if (!el.shadowRoot) {
|
|
173
|
-
innerContent += el.innerHTML;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return `<${tagName}${attrsStr}>${innerContent}</${tagName}>`;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Render a Symbiote component to a stream of HTML chunks.
|
|
181
|
-
* @param {string} tagName - Custom element tag name
|
|
182
|
-
* @param {Object<string, string>} [attrs] - Attributes to set on the element
|
|
183
|
-
* @returns {AsyncGenerator<string>}
|
|
184
|
-
*/
|
|
185
|
-
export async function* renderToStream(tagName, attrs = {}) {
|
|
186
|
-
if (!ssrDocument) {
|
|
187
|
-
throw new Error('[Symbiote SSR] Call initSSR() before renderToStream()');
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
let el = ssrDocument.createElement(tagName);
|
|
191
|
-
|
|
192
|
-
for (let [key, val] of Object.entries(attrs)) {
|
|
193
|
-
el.setAttribute(key, String(val));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Trigger component lifecycle:
|
|
197
|
-
ssrDocument.body.appendChild(el);
|
|
198
|
-
|
|
199
|
-
// Stream the element:
|
|
200
|
-
yield* streamElement(el);
|
|
201
|
-
|
|
202
|
-
// Cleanup:
|
|
203
|
-
el.remove();
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Stream-serialize an element, yielding chunks.
|
|
208
|
-
* Custom element children are recursed into for granular streaming.
|
|
209
|
-
* @param {HTMLElement} el
|
|
210
|
-
* @returns {AsyncGenerator<string>}
|
|
211
|
-
*/
|
|
212
|
-
async function* streamElement(el) {
|
|
213
|
-
let tagName = el.localName;
|
|
214
|
-
let attrsStr = '';
|
|
215
|
-
if (el.attributes) {
|
|
216
|
-
for (let attr of el.attributes) {
|
|
217
|
-
attrsStr += ` ${attr.name}="${attr.value}"`;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
yield `<${tagName}${attrsStr}>`;
|
|
222
|
-
|
|
223
|
-
// Declarative Shadow DOM:
|
|
224
|
-
if (el.shadowRoot) {
|
|
225
|
-
yield '<template shadowrootmode="open">';
|
|
226
|
-
|
|
227
|
-
let ctor = /** @type {any} */ (el).constructor;
|
|
228
|
-
if (ctor.shadowStyleSheets) {
|
|
229
|
-
for (let sheet of ctor.shadowStyleSheets) {
|
|
230
|
-
let cssText = extractCSS(sheet);
|
|
231
|
-
if (cssText) {
|
|
232
|
-
yield `<style>${cssText}</style>`;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Stream shadow DOM children:
|
|
238
|
-
for (let child of el.shadowRoot.childNodes) {
|
|
239
|
-
yield* streamNode(child);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
yield '</template>';
|
|
243
|
-
} else {
|
|
244
|
-
// Stream light DOM children:
|
|
245
|
-
for (let child of el.childNodes) {
|
|
246
|
-
yield* streamNode(child);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
yield `</${tagName}>`;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Stream-serialize a DOM node.
|
|
255
|
-
* @param {Node} node
|
|
256
|
-
* @returns {AsyncGenerator<string>}
|
|
257
|
-
*/
|
|
258
|
-
async function* streamNode(node) {
|
|
259
|
-
// Custom element — recurse for granular streaming:
|
|
260
|
-
if (node.nodeType === 1 && /** @type {Element} */ (node).localName?.includes('-')) {
|
|
261
|
-
yield* streamElement(/** @type {HTMLElement} */ (node));
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
// Regular element with children:
|
|
265
|
-
if (node.nodeType === 1) {
|
|
266
|
-
let el = /** @type {HTMLElement} */ (node);
|
|
267
|
-
let attrsStr = '';
|
|
268
|
-
if (el.attributes) {
|
|
269
|
-
for (let attr of el.attributes) {
|
|
270
|
-
attrsStr += ` ${attr.name}="${attr.value}"`;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
if (el.childNodes.length) {
|
|
274
|
-
yield `<${el.localName}${attrsStr}>`;
|
|
275
|
-
for (let child of el.childNodes) {
|
|
276
|
-
yield* streamNode(child);
|
|
277
|
-
}
|
|
278
|
-
yield `</${el.localName}>`;
|
|
279
|
-
} else {
|
|
280
|
-
yield `<${el.localName}${attrsStr}>${el.innerHTML}</${el.localName}>`;
|
|
281
|
-
}
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
// Text node:
|
|
285
|
-
if (node.nodeType === 3) {
|
|
286
|
-
yield node.textContent || '';
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
// Comment node:
|
|
290
|
-
if (node.nodeType === 8) {
|
|
291
|
-
yield `<!--${node.textContent}-->`;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Clean up the SSR environment.
|
|
297
|
-
*/
|
|
298
|
-
export function destroySSR() {
|
|
299
|
-
if (ssrDocument) {
|
|
300
|
-
ssrDocument.body.innerHTML = '';
|
|
301
|
-
}
|
|
302
|
-
delete globalThis.__SYMBIOTE_SSR;
|
|
303
|
-
delete globalThis.document;
|
|
304
|
-
delete globalThis.window;
|
|
305
|
-
delete globalThis.HTMLElement;
|
|
306
|
-
delete globalThis.customElements;
|
|
307
|
-
delete globalThis.DocumentFragment;
|
|
308
|
-
delete globalThis.NodeFilter;
|
|
309
|
-
delete globalThis.MutationObserver;
|
|
310
|
-
delete globalThis.CSSStyleSheet;
|
|
311
|
-
ssrDocument = null;
|
|
312
|
-
ssrWindow = null;
|
|
313
|
-
}
|
package/types/core/ssr.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export function initSSR(): Promise<{
|
|
2
|
-
document: any;
|
|
3
|
-
window: any;
|
|
4
|
-
}>;
|
|
5
|
-
export function renderToString(tagName: string, attrs?: {
|
|
6
|
-
[x: string]: string;
|
|
7
|
-
}): string;
|
|
8
|
-
export function renderToStream(tagName: string, attrs?: {
|
|
9
|
-
[x: string]: string;
|
|
10
|
-
}): AsyncGenerator<string>;
|
|
11
|
-
export function destroySSR(): void;
|
|
12
|
-
//# sourceMappingURL=ssr.d.ts.map
|
package/types/core/ssr.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ssr.d.ts","sourceRoot":"","sources":["../../core/ssr.js"],"names":[],"mappings":"AAqBA;;;GA6DC;AA8BD,wCAJW,MAAM;;IAEJ,MAAM,CAuBlB;AAmDD,wCAJW,MAAM;;IAEJ,cAAc,CAAC,MAAM,CAAC,CAqBlC;AA8FD,mCAeC"}
|