@magnet-js/ssr 0.1.0
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/LICENSE +21 -0
- package/esm/mod.d.ts +4 -0
- package/esm/mod.d.ts.map +1 -0
- package/esm/mod.js +2 -0
- package/esm/package.json +3 -0
- package/esm/ssr.d.ts +93 -0
- package/esm/ssr.d.ts.map +1 -0
- package/esm/ssr.js +300 -0
- package/esm/wait.d.ts +67 -0
- package/esm/wait.d.ts.map +1 -0
- package/esm/wait.js +59 -0
- package/package.json +27 -0
- package/script/mod.d.ts +4 -0
- package/script/mod.d.ts.map +1 -0
- package/script/mod.js +7 -0
- package/script/package.json +3 -0
- package/script/ssr.d.ts +93 -0
- package/script/ssr.d.ts.map +1 -0
- package/script/ssr.js +310 -0
- package/script/wait.d.ts +67 -0
- package/script/wait.d.ts.map +1 -0
- package/script/wait.js +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fernando G. Vilar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/esm/mod.d.ts
ADDED
package/esm/mod.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC"}
|
package/esm/mod.js
ADDED
package/esm/package.json
ADDED
package/esm/ssr.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { MagnetWindow } from "@magnet-js/ui";
|
|
2
|
+
/**
|
|
3
|
+
* Base class for server-side elements. Tracks attributes and children with
|
|
4
|
+
* just enough surface area for Magnet to operate during SSR.
|
|
5
|
+
*
|
|
6
|
+
* Event listener methods are no-ops because listeners are never invoked on
|
|
7
|
+
* the server. `attachShadow` creates a declarative shadow root rendered as a
|
|
8
|
+
* `<template shadowrootmode="...">` child.
|
|
9
|
+
*/
|
|
10
|
+
declare class SSRElement {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
/** Plain attribute map. Namespaced attributes are stored by their full key. */
|
|
13
|
+
readonly attributes: any;
|
|
14
|
+
protected children: Set<object>;
|
|
15
|
+
isConnected: boolean;
|
|
16
|
+
readonly nodeType = 1;
|
|
17
|
+
private shadow;
|
|
18
|
+
constructor(name: string);
|
|
19
|
+
/** Sets a regular attribute. */
|
|
20
|
+
setAttribute(name: string, value: string): void;
|
|
21
|
+
/** Sets a namespaced attribute, storing it under the supplied key. */
|
|
22
|
+
setAttributeNS(_namespace: string, name: string, value: string): void;
|
|
23
|
+
/** Removes an attribute by key. */
|
|
24
|
+
removeAttribute(name: string): void;
|
|
25
|
+
/** Removes a namespaced attribute by key. */
|
|
26
|
+
removeAttributeNS(_namespace: string, name: string): void;
|
|
27
|
+
/**
|
|
28
|
+
* Creates a declarative shadow root for SSR.
|
|
29
|
+
*
|
|
30
|
+
* Renders as a `<template shadowrootmode="...">` child element. Throws if
|
|
31
|
+
* a shadow root has already been attached.
|
|
32
|
+
*/
|
|
33
|
+
attachShadow({ clonable, customElementRegistry, delegatesFocus, mode, serializable, slotAssignment, }: ShadowRootInit): SSRParentHTMLElement;
|
|
34
|
+
/** Returns the declarative shadow root, or `null` if none exists. */
|
|
35
|
+
get shadowRoot(): SSRElement | null;
|
|
36
|
+
/** Replaces all children with a single escaped text node. */
|
|
37
|
+
set textContent(value: string);
|
|
38
|
+
/** Concatenates the rendered text of all children. */
|
|
39
|
+
get textContent(): string;
|
|
40
|
+
/** No-op: event listeners are not invoked on the server. */
|
|
41
|
+
addEventListener(_type: string, _callback: EventListenerOrEventListenerObject | null, _options?: AddEventListenerOptions | boolean): void;
|
|
42
|
+
/** No-op: event listeners are not invoked on the server. */
|
|
43
|
+
removeEventListener(_type: string, _callback: EventListenerOrEventListenerObject | null, _options?: EventListenerOptions | boolean): void;
|
|
44
|
+
/** Appends a child, replacing it if already present. */
|
|
45
|
+
appendChild(child: object): object;
|
|
46
|
+
/** Removes a child from this element. */
|
|
47
|
+
removeChild(child: object): object;
|
|
48
|
+
/** Inserts a child before a reference child, or appends if the ref is missing. */
|
|
49
|
+
insertBefore(child: object, ref: object): object;
|
|
50
|
+
}
|
|
51
|
+
/** HTML element with the XHTML namespace and an uppercase tag name. */
|
|
52
|
+
export declare class SSRHTMLElement extends SSRElement {
|
|
53
|
+
get namespaceURI(): any;
|
|
54
|
+
get tagName(): string;
|
|
55
|
+
}
|
|
56
|
+
/** Void HTML element that renders as a self-closing tag. */
|
|
57
|
+
export declare class SSRVoidHTMLElement extends SSRHTMLElement {
|
|
58
|
+
toString(): string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Serializes an element opening tag into `parts`.
|
|
62
|
+
*
|
|
63
|
+
* Truthy attribute values are emitted as `key="value"`; empty strings are
|
|
64
|
+
* emitted as boolean attributes (`key`).
|
|
65
|
+
*/
|
|
66
|
+
export declare const voidToString: (parts: string[], name: string, attributes: Record<string, string>, end: string) => void;
|
|
67
|
+
/** HTML element with children, rendered as `<tag>...</tag>`. */
|
|
68
|
+
export declare class SSRParentHTMLElement extends SSRHTMLElement {
|
|
69
|
+
toString(): string;
|
|
70
|
+
}
|
|
71
|
+
/** Base class for MathML and SVG elements, rendered as self-closing or parent tags. */
|
|
72
|
+
export declare class SSRNSElement extends SSRElement {
|
|
73
|
+
toString(): string;
|
|
74
|
+
}
|
|
75
|
+
/** MathML element with the MathML namespace. */
|
|
76
|
+
export declare class SSRMathMLElement extends SSRNSElement {
|
|
77
|
+
get namespaceURI(): any;
|
|
78
|
+
get tagName(): string;
|
|
79
|
+
}
|
|
80
|
+
/** SVG element with the SVG namespace. */
|
|
81
|
+
export declare class SSRSVGElement extends SSRNSElement {
|
|
82
|
+
get namespaceURI(): any;
|
|
83
|
+
get tagName(): string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Minimal `MagnetWindow` implementation for server-side rendering.
|
|
87
|
+
*
|
|
88
|
+
* Use this as the `window` when constructing a Magnet context to render
|
|
89
|
+
* components to HTML strings on the server.
|
|
90
|
+
*/
|
|
91
|
+
export declare const ssrWindow: MagnetWindow;
|
|
92
|
+
export {};
|
|
93
|
+
//# sourceMappingURL=ssr.d.ts.map
|
package/esm/ssr.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssr.d.ts","sourceRoot":"","sources":["../src/ssr.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AA+ClD;;;;;;;GAOG;AACH,cAAM,UAAU;IAQF,QAAQ,CAAC,IAAI,EAAE,MAAM;IAPjC,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,MAAyC;IAC5D,SAAS,CAAC,QAAQ,cAAqB;IACvC,WAAW,UAAS;IACpB,QAAQ,CAAC,QAAQ,KAAK;IACtB,OAAO,CAAC,MAAM,CAA2B;gBAEpB,IAAI,EAAE,MAAM;IAEjC,gCAAgC;IAChC,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAIxC,sEAAsE;IACtE,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAI9D,mCAAmC;IACnC,eAAe,CAAC,IAAI,EAAE,MAAM;IAI5B,6CAA6C;IAC7C,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;IAIlD;;;;;OAKG;IACH,YAAY,CACV,EACE,QAAQ,EACR,qBAAqB,EACrB,cAAc,EACd,IAAI,EACJ,YAAY,EACZ,cAAc,GACf,EAAE,cAAc;IAoBnB,qEAAqE;IACrE,IAAI,UAAU,sBAEb;IAED,6DAA6D;IAC7D,IAAI,WAAW,CAAC,KAAK,EAAE,MAAM,EAG5B;IAED,sDAAsD;IACtD,IAAI,WAAW,IAAI,MAAM,CAKxB;IAED,4DAA4D;IAC5D,gBAAgB,CACd,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,kCAAkC,GAAG,IAAI,EACpD,QAAQ,CAAC,EAAE,uBAAuB,GAAG,OAAO,GAC3C,IAAI;IAEP,4DAA4D;IAC5D,mBAAmB,CACjB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,kCAAkC,GAAG,IAAI,EACpD,QAAQ,CAAC,EAAE,oBAAoB,GAAG,OAAO,GACxC,IAAI;IAEP,wDAAwD;IACxD,WAAW,CAAC,KAAK,EAAE,MAAM;IAQzB,yCAAyC;IACzC,WAAW,CAAC,KAAK,EAAE,MAAM;IAKzB,kFAAkF;IAClF,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM;CAgBxC;AAED,uEAAuE;AACvE,qBAAa,cAAe,SAAQ,UAAU;IAC5C,IAAI,YAAY,QAEf;IACD,IAAI,OAAO,WAEV;CACF;AAED,4DAA4D;AAC5D,qBAAa,kBAAmB,SAAQ,cAAc;IAC3C,QAAQ;CAKlB;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,UAChB,MAAM,EAAE,QACT,MAAM,cACA,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,OAC7B,MAAM,SAaZ,CAAC;AAEF,gEAAgE;AAChE,qBAAa,oBAAqB,SAAQ,cAAc;IAC7C,QAAQ;CAOlB;AAED,uFAAuF;AACvF,qBAAa,YAAa,SAAQ,UAAU;IACjC,QAAQ;CAUlB;AAED,gDAAgD;AAChD,qBAAa,gBAAiB,SAAQ,YAAY;IAChD,IAAI,YAAY,QAEf;IACD,IAAI,OAAO,WAEV;CACF;AAED,0CAA0C;AAC1C,qBAAa,aAAc,SAAQ,YAAY;IAC7C,IAAI,YAAY,QAEf;IACD,IAAI,OAAO,WAEV;CACF;AAED;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,YAcvB,CAAC"}
|
package/esm/ssr.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { createEmpty, isVoid, MATHML_NAMESPACE, SVG_NAMESPACE, XHTML_NAMESPACE, } from "@magnet-js/common";
|
|
2
|
+
/** Minimal server-side comment node. Renders as `<!--data-->`. */
|
|
3
|
+
class SSRComment {
|
|
4
|
+
constructor(data) {
|
|
5
|
+
Object.defineProperty(this, "data", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
configurable: true,
|
|
8
|
+
writable: true,
|
|
9
|
+
value: data
|
|
10
|
+
});
|
|
11
|
+
Object.defineProperty(this, "nodeType", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true,
|
|
15
|
+
value: 8
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/** Length of the rendered comment string. */
|
|
19
|
+
get length() {
|
|
20
|
+
return this.data.length + 7;
|
|
21
|
+
}
|
|
22
|
+
/** Returns the HTML comment representation. */
|
|
23
|
+
toString() {
|
|
24
|
+
return `<!--${this.data}-->`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Minimal server-side text node. Escapes `&`, `<`, and `>`. */
|
|
28
|
+
class SSRText {
|
|
29
|
+
constructor(text) {
|
|
30
|
+
Object.defineProperty(this, "nodeType", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
configurable: true,
|
|
33
|
+
writable: true,
|
|
34
|
+
value: 3
|
|
35
|
+
});
|
|
36
|
+
Object.defineProperty(this, "data", {
|
|
37
|
+
enumerable: true,
|
|
38
|
+
configurable: true,
|
|
39
|
+
writable: true,
|
|
40
|
+
value: void 0
|
|
41
|
+
});
|
|
42
|
+
this.data = text.replace(/&|<|>/g, (value) => {
|
|
43
|
+
switch (value) {
|
|
44
|
+
case "&":
|
|
45
|
+
return "&";
|
|
46
|
+
case "<":
|
|
47
|
+
return "<";
|
|
48
|
+
}
|
|
49
|
+
return ">";
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Length of the escaped text content. */
|
|
53
|
+
get length() {
|
|
54
|
+
return this.data.length;
|
|
55
|
+
}
|
|
56
|
+
/** Returns the escaped text content. */
|
|
57
|
+
toString() {
|
|
58
|
+
return this.data;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Base class for server-side elements. Tracks attributes and children with
|
|
63
|
+
* just enough surface area for Magnet to operate during SSR.
|
|
64
|
+
*
|
|
65
|
+
* Event listener methods are no-ops because listeners are never invoked on
|
|
66
|
+
* the server. `attachShadow` creates a declarative shadow root rendered as a
|
|
67
|
+
* `<template shadowrootmode="...">` child.
|
|
68
|
+
*/
|
|
69
|
+
class SSRElement {
|
|
70
|
+
constructor(name) {
|
|
71
|
+
Object.defineProperty(this, "name", {
|
|
72
|
+
enumerable: true,
|
|
73
|
+
configurable: true,
|
|
74
|
+
writable: true,
|
|
75
|
+
value: name
|
|
76
|
+
});
|
|
77
|
+
/** Plain attribute map. Namespaced attributes are stored by their full key. */
|
|
78
|
+
Object.defineProperty(this, "attributes", {
|
|
79
|
+
enumerable: true,
|
|
80
|
+
configurable: true,
|
|
81
|
+
writable: true,
|
|
82
|
+
value: createEmpty()
|
|
83
|
+
});
|
|
84
|
+
Object.defineProperty(this, "children", {
|
|
85
|
+
enumerable: true,
|
|
86
|
+
configurable: true,
|
|
87
|
+
writable: true,
|
|
88
|
+
value: new Set()
|
|
89
|
+
});
|
|
90
|
+
Object.defineProperty(this, "isConnected", {
|
|
91
|
+
enumerable: true,
|
|
92
|
+
configurable: true,
|
|
93
|
+
writable: true,
|
|
94
|
+
value: false
|
|
95
|
+
});
|
|
96
|
+
Object.defineProperty(this, "nodeType", {
|
|
97
|
+
enumerable: true,
|
|
98
|
+
configurable: true,
|
|
99
|
+
writable: true,
|
|
100
|
+
value: 1
|
|
101
|
+
});
|
|
102
|
+
Object.defineProperty(this, "shadow", {
|
|
103
|
+
enumerable: true,
|
|
104
|
+
configurable: true,
|
|
105
|
+
writable: true,
|
|
106
|
+
value: null
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** Sets a regular attribute. */
|
|
110
|
+
setAttribute(name, value) {
|
|
111
|
+
this.attributes[name] = value;
|
|
112
|
+
}
|
|
113
|
+
/** Sets a namespaced attribute, storing it under the supplied key. */
|
|
114
|
+
setAttributeNS(_namespace, name, value) {
|
|
115
|
+
this.attributes[name] = value;
|
|
116
|
+
}
|
|
117
|
+
/** Removes an attribute by key. */
|
|
118
|
+
removeAttribute(name) {
|
|
119
|
+
delete this.attributes[name];
|
|
120
|
+
}
|
|
121
|
+
/** Removes a namespaced attribute by key. */
|
|
122
|
+
removeAttributeNS(_namespace, name) {
|
|
123
|
+
delete this.attributes[name];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Creates a declarative shadow root for SSR.
|
|
127
|
+
*
|
|
128
|
+
* Renders as a `<template shadowrootmode="...">` child element. Throws if
|
|
129
|
+
* a shadow root has already been attached.
|
|
130
|
+
*/
|
|
131
|
+
attachShadow({ clonable, customElementRegistry, delegatesFocus, mode, serializable, slotAssignment, }) {
|
|
132
|
+
if (this.shadow)
|
|
133
|
+
throw new Error("Shadow root already exists");
|
|
134
|
+
const shadow = new SSRParentHTMLElement("template");
|
|
135
|
+
shadow.attributes.shadowrootmode = mode;
|
|
136
|
+
if (clonable)
|
|
137
|
+
shadow.attributes.shadowrootclonable = "";
|
|
138
|
+
if (customElementRegistry) {
|
|
139
|
+
shadow.attributes.shadowrootcustomelementregistry = "";
|
|
140
|
+
}
|
|
141
|
+
if (delegatesFocus)
|
|
142
|
+
shadow.attributes.shadowrootdelegatesfocus = "";
|
|
143
|
+
if (serializable)
|
|
144
|
+
shadow.attributes.shadowrootserializable = "";
|
|
145
|
+
if (slotAssignment) {
|
|
146
|
+
shadow.attributes.shadowrootslotassignment = slotAssignment;
|
|
147
|
+
}
|
|
148
|
+
this.appendChild(shadow);
|
|
149
|
+
this.shadow = shadow;
|
|
150
|
+
return shadow;
|
|
151
|
+
}
|
|
152
|
+
/** Returns the declarative shadow root, or `null` if none exists. */
|
|
153
|
+
get shadowRoot() {
|
|
154
|
+
return this.shadow;
|
|
155
|
+
}
|
|
156
|
+
/** Replaces all children with a single escaped text node. */
|
|
157
|
+
set textContent(value) {
|
|
158
|
+
this.children.clear();
|
|
159
|
+
this.appendChild(new SSRText(value));
|
|
160
|
+
}
|
|
161
|
+
/** Concatenates the rendered text of all children. */
|
|
162
|
+
get textContent() {
|
|
163
|
+
const out = new Array(this.children.size);
|
|
164
|
+
let i = 0;
|
|
165
|
+
for (const child of this.children)
|
|
166
|
+
out[i++] = child.toString();
|
|
167
|
+
return out.join("");
|
|
168
|
+
}
|
|
169
|
+
/** No-op: event listeners are not invoked on the server. */
|
|
170
|
+
addEventListener(_type, _callback, _options) { }
|
|
171
|
+
/** No-op: event listeners are not invoked on the server. */
|
|
172
|
+
removeEventListener(_type, _callback, _options) { }
|
|
173
|
+
/** Appends a child, replacing it if already present. */
|
|
174
|
+
appendChild(child) {
|
|
175
|
+
if (child) {
|
|
176
|
+
this.children.delete(child);
|
|
177
|
+
this.children.add(child);
|
|
178
|
+
}
|
|
179
|
+
return child;
|
|
180
|
+
}
|
|
181
|
+
/** Removes a child from this element. */
|
|
182
|
+
removeChild(child) {
|
|
183
|
+
if (child)
|
|
184
|
+
this.children.delete(child);
|
|
185
|
+
return child;
|
|
186
|
+
}
|
|
187
|
+
/** Inserts a child before a reference child, or appends if the ref is missing. */
|
|
188
|
+
insertBefore(child, ref) {
|
|
189
|
+
if (child) {
|
|
190
|
+
if (ref && this.children.has(ref)) {
|
|
191
|
+
if (ref !== child) {
|
|
192
|
+
const nextChildren = new Set();
|
|
193
|
+
this.children.delete(child);
|
|
194
|
+
for (const prev of this.children) {
|
|
195
|
+
if (prev === ref)
|
|
196
|
+
nextChildren.add(child);
|
|
197
|
+
nextChildren.add(prev);
|
|
198
|
+
}
|
|
199
|
+
this.children = nextChildren;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else
|
|
203
|
+
this.appendChild(child);
|
|
204
|
+
}
|
|
205
|
+
return child;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** HTML element with the XHTML namespace and an uppercase tag name. */
|
|
209
|
+
export class SSRHTMLElement extends SSRElement {
|
|
210
|
+
get namespaceURI() {
|
|
211
|
+
return XHTML_NAMESPACE;
|
|
212
|
+
}
|
|
213
|
+
get tagName() {
|
|
214
|
+
return this.name.toUpperCase();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** Void HTML element that renders as a self-closing tag. */
|
|
218
|
+
export class SSRVoidHTMLElement extends SSRHTMLElement {
|
|
219
|
+
toString() {
|
|
220
|
+
const parts = [];
|
|
221
|
+
voidToString(parts, this.name, this.attributes, ">");
|
|
222
|
+
return parts.join("");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Serializes an element opening tag into `parts`.
|
|
227
|
+
*
|
|
228
|
+
* Truthy attribute values are emitted as `key="value"`; empty strings are
|
|
229
|
+
* emitted as boolean attributes (`key`).
|
|
230
|
+
*/
|
|
231
|
+
export const voidToString = (parts, name, attributes, end) => {
|
|
232
|
+
parts.push("<" + name);
|
|
233
|
+
const keys = Object.keys(attributes);
|
|
234
|
+
let i = keys.length;
|
|
235
|
+
let key;
|
|
236
|
+
let value;
|
|
237
|
+
while (i--) {
|
|
238
|
+
key = keys[i];
|
|
239
|
+
value = attributes[key];
|
|
240
|
+
parts.push(value ? ` ${key}="${value}"` : ` ${key}`);
|
|
241
|
+
}
|
|
242
|
+
parts.push(end);
|
|
243
|
+
};
|
|
244
|
+
/** HTML element with children, rendered as `<tag>...</tag>`. */
|
|
245
|
+
export class SSRParentHTMLElement extends SSRHTMLElement {
|
|
246
|
+
toString() {
|
|
247
|
+
const parts = [];
|
|
248
|
+
voidToString(parts, this.name, this.attributes, ">");
|
|
249
|
+
for (const child of this.children)
|
|
250
|
+
parts.push(child.toString());
|
|
251
|
+
parts.push("</" + this.name + ">");
|
|
252
|
+
return parts.join("");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** Base class for MathML and SVG elements, rendered as self-closing or parent tags. */
|
|
256
|
+
export class SSRNSElement extends SSRElement {
|
|
257
|
+
toString() {
|
|
258
|
+
const parent = this.children.size > 0;
|
|
259
|
+
const parts = [];
|
|
260
|
+
voidToString(parts, this.name, this.attributes, parent ? " >" : "/>");
|
|
261
|
+
if (parent) {
|
|
262
|
+
for (const child of this.children)
|
|
263
|
+
parts.push(child.toString());
|
|
264
|
+
parts.push("</" + this.name + ">");
|
|
265
|
+
}
|
|
266
|
+
return parts.join("");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** MathML element with the MathML namespace. */
|
|
270
|
+
export class SSRMathMLElement extends SSRNSElement {
|
|
271
|
+
get namespaceURI() {
|
|
272
|
+
return MATHML_NAMESPACE;
|
|
273
|
+
}
|
|
274
|
+
get tagName() {
|
|
275
|
+
return this.name;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/** SVG element with the SVG namespace. */
|
|
279
|
+
export class SSRSVGElement extends SSRNSElement {
|
|
280
|
+
get namespaceURI() {
|
|
281
|
+
return SVG_NAMESPACE;
|
|
282
|
+
}
|
|
283
|
+
get tagName() {
|
|
284
|
+
return this.name;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Minimal `MagnetWindow` implementation for server-side rendering.
|
|
289
|
+
*
|
|
290
|
+
* Use this as the `window` when constructing a Magnet context to render
|
|
291
|
+
* components to HTML strings on the server.
|
|
292
|
+
*/
|
|
293
|
+
export const ssrWindow = {
|
|
294
|
+
document: {
|
|
295
|
+
createComment: (data) => new SSRComment(data),
|
|
296
|
+
createElement: (name) => new (isVoid(name) ? SSRVoidHTMLElement : SSRParentHTMLElement)(name),
|
|
297
|
+
createElementNS: (namespace, name) => new (namespace === MATHML_NAMESPACE ? SSRMathMLElement : SSRSVGElement)(name),
|
|
298
|
+
createTextNode: (data) => new SSRText(data),
|
|
299
|
+
},
|
|
300
|
+
};
|
package/esm/wait.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { SignalAPI } from "@magnet-js/common";
|
|
2
|
+
/**
|
|
3
|
+
* Marks a single unit of pending asynchronous work as complete.
|
|
4
|
+
*
|
|
5
|
+
* Returned by {@link Wait}. Each call must balance a prior call to the
|
|
6
|
+
* wait-group factory. Calling the same `Done` more than once is a no-op.
|
|
7
|
+
*/
|
|
8
|
+
export interface Done {
|
|
9
|
+
(): void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Wait-group factory used to coordinate asynchronous work during SSR.
|
|
13
|
+
*
|
|
14
|
+
* Magnet components can start async operations (for example data fetching)
|
|
15
|
+
* and register each operation by calling `wait()`. The returned {@link Done}
|
|
16
|
+
* callback is invoked when the operation finishes. The `done` promise
|
|
17
|
+
* resolves once every registered operation has completed, signaling that
|
|
18
|
+
* the server can safely serialize the rendered tree.
|
|
19
|
+
*/
|
|
20
|
+
export interface Wait {
|
|
21
|
+
/** Registers one pending operation and returns its completion callback. */
|
|
22
|
+
(): Done;
|
|
23
|
+
/**
|
|
24
|
+
* Resolves the first time every registered operation has completed.
|
|
25
|
+
*
|
|
26
|
+
* Once resolved, this promise never returns to a pending state; the wait
|
|
27
|
+
* group must be fully populated before the counter reaches zero.
|
|
28
|
+
*/
|
|
29
|
+
done: Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Creates a wait group for coordinating async work during server-side
|
|
33
|
+
* rendering.
|
|
34
|
+
*
|
|
35
|
+
* Use it when a component triggers asynchronous tasks whose results update
|
|
36
|
+
* signals. The server awaits `wait.done` before stringifying the rendered
|
|
37
|
+
* DOM tree.
|
|
38
|
+
*
|
|
39
|
+
* The returned `done` promise resolves the first time the pending counter
|
|
40
|
+
* reaches zero. After that it stays resolved forever, so every unit of work
|
|
41
|
+
* must be registered before the counter reaches zero.
|
|
42
|
+
*
|
|
43
|
+
* The `signal` argument provides the reactive primitives used to detect
|
|
44
|
+
* completion. A one-shot effect watches an internal settled signal and
|
|
45
|
+
* resolves `done` the first time the signal becomes true, after any signal
|
|
46
|
+
* effects scheduled by the async work have run.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* const ready = wait(tc39);
|
|
51
|
+
*
|
|
52
|
+
* const page = app(() => {
|
|
53
|
+
* const data = state<string | null>(null);
|
|
54
|
+
* const done = ready();
|
|
55
|
+
* fetchData().then((value) => {
|
|
56
|
+
* data.set(value);
|
|
57
|
+
* done();
|
|
58
|
+
* });
|
|
59
|
+
* return div(data);
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* await ready.done;
|
|
63
|
+
* console.log(page.toString());
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare const wait: (signal: Pick<SignalAPI, "state" | "effect">) => Wait;
|
|
67
|
+
//# sourceMappingURL=wait.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wait.d.ts","sourceRoot":"","sources":["../src/wait.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,WAAW,IAAI;IACnB,IAAI,IAAI,CAAC;CACV;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,IAAI;IACnB,2EAA2E;IAC3E,IAAI,IAAI,CAAC;IAET;;;;;OAKG;IACH,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,IAAI,WAAY,IAAI,CAAC,SAAS,EAAE,OAAO,GAAG,QAAQ,CAAC,KAAG,IAyBlE,CAAC"}
|
package/esm/wait.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a wait group for coordinating async work during server-side
|
|
3
|
+
* rendering.
|
|
4
|
+
*
|
|
5
|
+
* Use it when a component triggers asynchronous tasks whose results update
|
|
6
|
+
* signals. The server awaits `wait.done` before stringifying the rendered
|
|
7
|
+
* DOM tree.
|
|
8
|
+
*
|
|
9
|
+
* The returned `done` promise resolves the first time the pending counter
|
|
10
|
+
* reaches zero. After that it stays resolved forever, so every unit of work
|
|
11
|
+
* must be registered before the counter reaches zero.
|
|
12
|
+
*
|
|
13
|
+
* The `signal` argument provides the reactive primitives used to detect
|
|
14
|
+
* completion. A one-shot effect watches an internal settled signal and
|
|
15
|
+
* resolves `done` the first time the signal becomes true, after any signal
|
|
16
|
+
* effects scheduled by the async work have run.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const ready = wait(tc39);
|
|
21
|
+
*
|
|
22
|
+
* const page = app(() => {
|
|
23
|
+
* const data = state<string | null>(null);
|
|
24
|
+
* const done = ready();
|
|
25
|
+
* fetchData().then((value) => {
|
|
26
|
+
* data.set(value);
|
|
27
|
+
* done();
|
|
28
|
+
* });
|
|
29
|
+
* return div(data);
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* await ready.done;
|
|
33
|
+
* console.log(page.toString());
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export const wait = (signal) => {
|
|
37
|
+
let resolve;
|
|
38
|
+
const done = new Promise((r) => {
|
|
39
|
+
resolve = r;
|
|
40
|
+
});
|
|
41
|
+
let counter = 0;
|
|
42
|
+
const settled = signal.state(false);
|
|
43
|
+
const dispose = signal.effect(() => {
|
|
44
|
+
if (settled.get()) {
|
|
45
|
+
dispose();
|
|
46
|
+
resolve();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
const waiter = () => {
|
|
50
|
+
counter++;
|
|
51
|
+
let callback = () => {
|
|
52
|
+
if (!--counter)
|
|
53
|
+
settled.set(true);
|
|
54
|
+
};
|
|
55
|
+
return () => callback = callback?.();
|
|
56
|
+
};
|
|
57
|
+
waiter.done = done;
|
|
58
|
+
return waiter;
|
|
59
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@magnet-js/ssr",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Server-side rendering support for Magnet components.",
|
|
5
|
+
"homepage": "https://gitlab.com/magnetjs/monorepo/-/tree/main/packages/ssr",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+ssh://git@gitlab.com/magnetjs/monorepo.git"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://gitlab.com/magnetjs/monorepo/-/issues"
|
|
13
|
+
},
|
|
14
|
+
"main": "./script/mod.js",
|
|
15
|
+
"module": "./esm/mod.js",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": "./esm/mod.js",
|
|
19
|
+
"require": "./script/mod.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@magnet-js/common": "0.1.0",
|
|
24
|
+
"@magnet-js/ui": "0.1.0"
|
|
25
|
+
},
|
|
26
|
+
"_generatedBy": "dnt@dev"
|
|
27
|
+
}
|
package/script/mod.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC"}
|
package/script/mod.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.wait = exports.ssrWindow = void 0;
|
|
4
|
+
var ssr_js_1 = require("./ssr.js");
|
|
5
|
+
Object.defineProperty(exports, "ssrWindow", { enumerable: true, get: function () { return ssr_js_1.ssrWindow; } });
|
|
6
|
+
var wait_js_1 = require("./wait.js");
|
|
7
|
+
Object.defineProperty(exports, "wait", { enumerable: true, get: function () { return wait_js_1.wait; } });
|
package/script/ssr.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { MagnetWindow } from "@magnet-js/ui";
|
|
2
|
+
/**
|
|
3
|
+
* Base class for server-side elements. Tracks attributes and children with
|
|
4
|
+
* just enough surface area for Magnet to operate during SSR.
|
|
5
|
+
*
|
|
6
|
+
* Event listener methods are no-ops because listeners are never invoked on
|
|
7
|
+
* the server. `attachShadow` creates a declarative shadow root rendered as a
|
|
8
|
+
* `<template shadowrootmode="...">` child.
|
|
9
|
+
*/
|
|
10
|
+
declare class SSRElement {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
/** Plain attribute map. Namespaced attributes are stored by their full key. */
|
|
13
|
+
readonly attributes: any;
|
|
14
|
+
protected children: Set<object>;
|
|
15
|
+
isConnected: boolean;
|
|
16
|
+
readonly nodeType = 1;
|
|
17
|
+
private shadow;
|
|
18
|
+
constructor(name: string);
|
|
19
|
+
/** Sets a regular attribute. */
|
|
20
|
+
setAttribute(name: string, value: string): void;
|
|
21
|
+
/** Sets a namespaced attribute, storing it under the supplied key. */
|
|
22
|
+
setAttributeNS(_namespace: string, name: string, value: string): void;
|
|
23
|
+
/** Removes an attribute by key. */
|
|
24
|
+
removeAttribute(name: string): void;
|
|
25
|
+
/** Removes a namespaced attribute by key. */
|
|
26
|
+
removeAttributeNS(_namespace: string, name: string): void;
|
|
27
|
+
/**
|
|
28
|
+
* Creates a declarative shadow root for SSR.
|
|
29
|
+
*
|
|
30
|
+
* Renders as a `<template shadowrootmode="...">` child element. Throws if
|
|
31
|
+
* a shadow root has already been attached.
|
|
32
|
+
*/
|
|
33
|
+
attachShadow({ clonable, customElementRegistry, delegatesFocus, mode, serializable, slotAssignment, }: ShadowRootInit): SSRParentHTMLElement;
|
|
34
|
+
/** Returns the declarative shadow root, or `null` if none exists. */
|
|
35
|
+
get shadowRoot(): SSRElement | null;
|
|
36
|
+
/** Replaces all children with a single escaped text node. */
|
|
37
|
+
set textContent(value: string);
|
|
38
|
+
/** Concatenates the rendered text of all children. */
|
|
39
|
+
get textContent(): string;
|
|
40
|
+
/** No-op: event listeners are not invoked on the server. */
|
|
41
|
+
addEventListener(_type: string, _callback: EventListenerOrEventListenerObject | null, _options?: AddEventListenerOptions | boolean): void;
|
|
42
|
+
/** No-op: event listeners are not invoked on the server. */
|
|
43
|
+
removeEventListener(_type: string, _callback: EventListenerOrEventListenerObject | null, _options?: EventListenerOptions | boolean): void;
|
|
44
|
+
/** Appends a child, replacing it if already present. */
|
|
45
|
+
appendChild(child: object): object;
|
|
46
|
+
/** Removes a child from this element. */
|
|
47
|
+
removeChild(child: object): object;
|
|
48
|
+
/** Inserts a child before a reference child, or appends if the ref is missing. */
|
|
49
|
+
insertBefore(child: object, ref: object): object;
|
|
50
|
+
}
|
|
51
|
+
/** HTML element with the XHTML namespace and an uppercase tag name. */
|
|
52
|
+
export declare class SSRHTMLElement extends SSRElement {
|
|
53
|
+
get namespaceURI(): any;
|
|
54
|
+
get tagName(): string;
|
|
55
|
+
}
|
|
56
|
+
/** Void HTML element that renders as a self-closing tag. */
|
|
57
|
+
export declare class SSRVoidHTMLElement extends SSRHTMLElement {
|
|
58
|
+
toString(): string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Serializes an element opening tag into `parts`.
|
|
62
|
+
*
|
|
63
|
+
* Truthy attribute values are emitted as `key="value"`; empty strings are
|
|
64
|
+
* emitted as boolean attributes (`key`).
|
|
65
|
+
*/
|
|
66
|
+
export declare const voidToString: (parts: string[], name: string, attributes: Record<string, string>, end: string) => void;
|
|
67
|
+
/** HTML element with children, rendered as `<tag>...</tag>`. */
|
|
68
|
+
export declare class SSRParentHTMLElement extends SSRHTMLElement {
|
|
69
|
+
toString(): string;
|
|
70
|
+
}
|
|
71
|
+
/** Base class for MathML and SVG elements, rendered as self-closing or parent tags. */
|
|
72
|
+
export declare class SSRNSElement extends SSRElement {
|
|
73
|
+
toString(): string;
|
|
74
|
+
}
|
|
75
|
+
/** MathML element with the MathML namespace. */
|
|
76
|
+
export declare class SSRMathMLElement extends SSRNSElement {
|
|
77
|
+
get namespaceURI(): any;
|
|
78
|
+
get tagName(): string;
|
|
79
|
+
}
|
|
80
|
+
/** SVG element with the SVG namespace. */
|
|
81
|
+
export declare class SSRSVGElement extends SSRNSElement {
|
|
82
|
+
get namespaceURI(): any;
|
|
83
|
+
get tagName(): string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Minimal `MagnetWindow` implementation for server-side rendering.
|
|
87
|
+
*
|
|
88
|
+
* Use this as the `window` when constructing a Magnet context to render
|
|
89
|
+
* components to HTML strings on the server.
|
|
90
|
+
*/
|
|
91
|
+
export declare const ssrWindow: MagnetWindow;
|
|
92
|
+
export {};
|
|
93
|
+
//# sourceMappingURL=ssr.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssr.d.ts","sourceRoot":"","sources":["../src/ssr.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AA+ClD;;;;;;;GAOG;AACH,cAAM,UAAU;IAQF,QAAQ,CAAC,IAAI,EAAE,MAAM;IAPjC,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,MAAyC;IAC5D,SAAS,CAAC,QAAQ,cAAqB;IACvC,WAAW,UAAS;IACpB,QAAQ,CAAC,QAAQ,KAAK;IACtB,OAAO,CAAC,MAAM,CAA2B;gBAEpB,IAAI,EAAE,MAAM;IAEjC,gCAAgC;IAChC,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAIxC,sEAAsE;IACtE,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAI9D,mCAAmC;IACnC,eAAe,CAAC,IAAI,EAAE,MAAM;IAI5B,6CAA6C;IAC7C,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;IAIlD;;;;;OAKG;IACH,YAAY,CACV,EACE,QAAQ,EACR,qBAAqB,EACrB,cAAc,EACd,IAAI,EACJ,YAAY,EACZ,cAAc,GACf,EAAE,cAAc;IAoBnB,qEAAqE;IACrE,IAAI,UAAU,sBAEb;IAED,6DAA6D;IAC7D,IAAI,WAAW,CAAC,KAAK,EAAE,MAAM,EAG5B;IAED,sDAAsD;IACtD,IAAI,WAAW,IAAI,MAAM,CAKxB;IAED,4DAA4D;IAC5D,gBAAgB,CACd,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,kCAAkC,GAAG,IAAI,EACpD,QAAQ,CAAC,EAAE,uBAAuB,GAAG,OAAO,GAC3C,IAAI;IAEP,4DAA4D;IAC5D,mBAAmB,CACjB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,kCAAkC,GAAG,IAAI,EACpD,QAAQ,CAAC,EAAE,oBAAoB,GAAG,OAAO,GACxC,IAAI;IAEP,wDAAwD;IACxD,WAAW,CAAC,KAAK,EAAE,MAAM;IAQzB,yCAAyC;IACzC,WAAW,CAAC,KAAK,EAAE,MAAM;IAKzB,kFAAkF;IAClF,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM;CAgBxC;AAED,uEAAuE;AACvE,qBAAa,cAAe,SAAQ,UAAU;IAC5C,IAAI,YAAY,QAEf;IACD,IAAI,OAAO,WAEV;CACF;AAED,4DAA4D;AAC5D,qBAAa,kBAAmB,SAAQ,cAAc;IAC3C,QAAQ;CAKlB;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,UAChB,MAAM,EAAE,QACT,MAAM,cACA,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,OAC7B,MAAM,SAaZ,CAAC;AAEF,gEAAgE;AAChE,qBAAa,oBAAqB,SAAQ,cAAc;IAC7C,QAAQ;CAOlB;AAED,uFAAuF;AACvF,qBAAa,YAAa,SAAQ,UAAU;IACjC,QAAQ;CAUlB;AAED,gDAAgD;AAChD,qBAAa,gBAAiB,SAAQ,YAAY;IAChD,IAAI,YAAY,QAEf;IACD,IAAI,OAAO,WAEV;CACF;AAED,0CAA0C;AAC1C,qBAAa,aAAc,SAAQ,YAAY;IAC7C,IAAI,YAAY,QAEf;IACD,IAAI,OAAO,WAEV;CACF;AAED;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,YAcvB,CAAC"}
|
package/script/ssr.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ssrWindow = exports.SSRSVGElement = exports.SSRMathMLElement = exports.SSRNSElement = exports.SSRParentHTMLElement = exports.voidToString = exports.SSRVoidHTMLElement = exports.SSRHTMLElement = void 0;
|
|
4
|
+
const common_1 = require("@magnet-js/common");
|
|
5
|
+
/** Minimal server-side comment node. Renders as `<!--data-->`. */
|
|
6
|
+
class SSRComment {
|
|
7
|
+
constructor(data) {
|
|
8
|
+
Object.defineProperty(this, "data", {
|
|
9
|
+
enumerable: true,
|
|
10
|
+
configurable: true,
|
|
11
|
+
writable: true,
|
|
12
|
+
value: data
|
|
13
|
+
});
|
|
14
|
+
Object.defineProperty(this, "nodeType", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
value: 8
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/** Length of the rendered comment string. */
|
|
22
|
+
get length() {
|
|
23
|
+
return this.data.length + 7;
|
|
24
|
+
}
|
|
25
|
+
/** Returns the HTML comment representation. */
|
|
26
|
+
toString() {
|
|
27
|
+
return `<!--${this.data}-->`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Minimal server-side text node. Escapes `&`, `<`, and `>`. */
|
|
31
|
+
class SSRText {
|
|
32
|
+
constructor(text) {
|
|
33
|
+
Object.defineProperty(this, "nodeType", {
|
|
34
|
+
enumerable: true,
|
|
35
|
+
configurable: true,
|
|
36
|
+
writable: true,
|
|
37
|
+
value: 3
|
|
38
|
+
});
|
|
39
|
+
Object.defineProperty(this, "data", {
|
|
40
|
+
enumerable: true,
|
|
41
|
+
configurable: true,
|
|
42
|
+
writable: true,
|
|
43
|
+
value: void 0
|
|
44
|
+
});
|
|
45
|
+
this.data = text.replace(/&|<|>/g, (value) => {
|
|
46
|
+
switch (value) {
|
|
47
|
+
case "&":
|
|
48
|
+
return "&";
|
|
49
|
+
case "<":
|
|
50
|
+
return "<";
|
|
51
|
+
}
|
|
52
|
+
return ">";
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/** Length of the escaped text content. */
|
|
56
|
+
get length() {
|
|
57
|
+
return this.data.length;
|
|
58
|
+
}
|
|
59
|
+
/** Returns the escaped text content. */
|
|
60
|
+
toString() {
|
|
61
|
+
return this.data;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Base class for server-side elements. Tracks attributes and children with
|
|
66
|
+
* just enough surface area for Magnet to operate during SSR.
|
|
67
|
+
*
|
|
68
|
+
* Event listener methods are no-ops because listeners are never invoked on
|
|
69
|
+
* the server. `attachShadow` creates a declarative shadow root rendered as a
|
|
70
|
+
* `<template shadowrootmode="...">` child.
|
|
71
|
+
*/
|
|
72
|
+
class SSRElement {
|
|
73
|
+
constructor(name) {
|
|
74
|
+
Object.defineProperty(this, "name", {
|
|
75
|
+
enumerable: true,
|
|
76
|
+
configurable: true,
|
|
77
|
+
writable: true,
|
|
78
|
+
value: name
|
|
79
|
+
});
|
|
80
|
+
/** Plain attribute map. Namespaced attributes are stored by their full key. */
|
|
81
|
+
Object.defineProperty(this, "attributes", {
|
|
82
|
+
enumerable: true,
|
|
83
|
+
configurable: true,
|
|
84
|
+
writable: true,
|
|
85
|
+
value: (0, common_1.createEmpty)()
|
|
86
|
+
});
|
|
87
|
+
Object.defineProperty(this, "children", {
|
|
88
|
+
enumerable: true,
|
|
89
|
+
configurable: true,
|
|
90
|
+
writable: true,
|
|
91
|
+
value: new Set()
|
|
92
|
+
});
|
|
93
|
+
Object.defineProperty(this, "isConnected", {
|
|
94
|
+
enumerable: true,
|
|
95
|
+
configurable: true,
|
|
96
|
+
writable: true,
|
|
97
|
+
value: false
|
|
98
|
+
});
|
|
99
|
+
Object.defineProperty(this, "nodeType", {
|
|
100
|
+
enumerable: true,
|
|
101
|
+
configurable: true,
|
|
102
|
+
writable: true,
|
|
103
|
+
value: 1
|
|
104
|
+
});
|
|
105
|
+
Object.defineProperty(this, "shadow", {
|
|
106
|
+
enumerable: true,
|
|
107
|
+
configurable: true,
|
|
108
|
+
writable: true,
|
|
109
|
+
value: null
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/** Sets a regular attribute. */
|
|
113
|
+
setAttribute(name, value) {
|
|
114
|
+
this.attributes[name] = value;
|
|
115
|
+
}
|
|
116
|
+
/** Sets a namespaced attribute, storing it under the supplied key. */
|
|
117
|
+
setAttributeNS(_namespace, name, value) {
|
|
118
|
+
this.attributes[name] = value;
|
|
119
|
+
}
|
|
120
|
+
/** Removes an attribute by key. */
|
|
121
|
+
removeAttribute(name) {
|
|
122
|
+
delete this.attributes[name];
|
|
123
|
+
}
|
|
124
|
+
/** Removes a namespaced attribute by key. */
|
|
125
|
+
removeAttributeNS(_namespace, name) {
|
|
126
|
+
delete this.attributes[name];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Creates a declarative shadow root for SSR.
|
|
130
|
+
*
|
|
131
|
+
* Renders as a `<template shadowrootmode="...">` child element. Throws if
|
|
132
|
+
* a shadow root has already been attached.
|
|
133
|
+
*/
|
|
134
|
+
attachShadow({ clonable, customElementRegistry, delegatesFocus, mode, serializable, slotAssignment, }) {
|
|
135
|
+
if (this.shadow)
|
|
136
|
+
throw new Error("Shadow root already exists");
|
|
137
|
+
const shadow = new SSRParentHTMLElement("template");
|
|
138
|
+
shadow.attributes.shadowrootmode = mode;
|
|
139
|
+
if (clonable)
|
|
140
|
+
shadow.attributes.shadowrootclonable = "";
|
|
141
|
+
if (customElementRegistry) {
|
|
142
|
+
shadow.attributes.shadowrootcustomelementregistry = "";
|
|
143
|
+
}
|
|
144
|
+
if (delegatesFocus)
|
|
145
|
+
shadow.attributes.shadowrootdelegatesfocus = "";
|
|
146
|
+
if (serializable)
|
|
147
|
+
shadow.attributes.shadowrootserializable = "";
|
|
148
|
+
if (slotAssignment) {
|
|
149
|
+
shadow.attributes.shadowrootslotassignment = slotAssignment;
|
|
150
|
+
}
|
|
151
|
+
this.appendChild(shadow);
|
|
152
|
+
this.shadow = shadow;
|
|
153
|
+
return shadow;
|
|
154
|
+
}
|
|
155
|
+
/** Returns the declarative shadow root, or `null` if none exists. */
|
|
156
|
+
get shadowRoot() {
|
|
157
|
+
return this.shadow;
|
|
158
|
+
}
|
|
159
|
+
/** Replaces all children with a single escaped text node. */
|
|
160
|
+
set textContent(value) {
|
|
161
|
+
this.children.clear();
|
|
162
|
+
this.appendChild(new SSRText(value));
|
|
163
|
+
}
|
|
164
|
+
/** Concatenates the rendered text of all children. */
|
|
165
|
+
get textContent() {
|
|
166
|
+
const out = new Array(this.children.size);
|
|
167
|
+
let i = 0;
|
|
168
|
+
for (const child of this.children)
|
|
169
|
+
out[i++] = child.toString();
|
|
170
|
+
return out.join("");
|
|
171
|
+
}
|
|
172
|
+
/** No-op: event listeners are not invoked on the server. */
|
|
173
|
+
addEventListener(_type, _callback, _options) { }
|
|
174
|
+
/** No-op: event listeners are not invoked on the server. */
|
|
175
|
+
removeEventListener(_type, _callback, _options) { }
|
|
176
|
+
/** Appends a child, replacing it if already present. */
|
|
177
|
+
appendChild(child) {
|
|
178
|
+
if (child) {
|
|
179
|
+
this.children.delete(child);
|
|
180
|
+
this.children.add(child);
|
|
181
|
+
}
|
|
182
|
+
return child;
|
|
183
|
+
}
|
|
184
|
+
/** Removes a child from this element. */
|
|
185
|
+
removeChild(child) {
|
|
186
|
+
if (child)
|
|
187
|
+
this.children.delete(child);
|
|
188
|
+
return child;
|
|
189
|
+
}
|
|
190
|
+
/** Inserts a child before a reference child, or appends if the ref is missing. */
|
|
191
|
+
insertBefore(child, ref) {
|
|
192
|
+
if (child) {
|
|
193
|
+
if (ref && this.children.has(ref)) {
|
|
194
|
+
if (ref !== child) {
|
|
195
|
+
const nextChildren = new Set();
|
|
196
|
+
this.children.delete(child);
|
|
197
|
+
for (const prev of this.children) {
|
|
198
|
+
if (prev === ref)
|
|
199
|
+
nextChildren.add(child);
|
|
200
|
+
nextChildren.add(prev);
|
|
201
|
+
}
|
|
202
|
+
this.children = nextChildren;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else
|
|
206
|
+
this.appendChild(child);
|
|
207
|
+
}
|
|
208
|
+
return child;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** HTML element with the XHTML namespace and an uppercase tag name. */
|
|
212
|
+
class SSRHTMLElement extends SSRElement {
|
|
213
|
+
get namespaceURI() {
|
|
214
|
+
return common_1.XHTML_NAMESPACE;
|
|
215
|
+
}
|
|
216
|
+
get tagName() {
|
|
217
|
+
return this.name.toUpperCase();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
exports.SSRHTMLElement = SSRHTMLElement;
|
|
221
|
+
/** Void HTML element that renders as a self-closing tag. */
|
|
222
|
+
class SSRVoidHTMLElement extends SSRHTMLElement {
|
|
223
|
+
toString() {
|
|
224
|
+
const parts = [];
|
|
225
|
+
(0, exports.voidToString)(parts, this.name, this.attributes, ">");
|
|
226
|
+
return parts.join("");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
exports.SSRVoidHTMLElement = SSRVoidHTMLElement;
|
|
230
|
+
/**
|
|
231
|
+
* Serializes an element opening tag into `parts`.
|
|
232
|
+
*
|
|
233
|
+
* Truthy attribute values are emitted as `key="value"`; empty strings are
|
|
234
|
+
* emitted as boolean attributes (`key`).
|
|
235
|
+
*/
|
|
236
|
+
const voidToString = (parts, name, attributes, end) => {
|
|
237
|
+
parts.push("<" + name);
|
|
238
|
+
const keys = Object.keys(attributes);
|
|
239
|
+
let i = keys.length;
|
|
240
|
+
let key;
|
|
241
|
+
let value;
|
|
242
|
+
while (i--) {
|
|
243
|
+
key = keys[i];
|
|
244
|
+
value = attributes[key];
|
|
245
|
+
parts.push(value ? ` ${key}="${value}"` : ` ${key}`);
|
|
246
|
+
}
|
|
247
|
+
parts.push(end);
|
|
248
|
+
};
|
|
249
|
+
exports.voidToString = voidToString;
|
|
250
|
+
/** HTML element with children, rendered as `<tag>...</tag>`. */
|
|
251
|
+
class SSRParentHTMLElement extends SSRHTMLElement {
|
|
252
|
+
toString() {
|
|
253
|
+
const parts = [];
|
|
254
|
+
(0, exports.voidToString)(parts, this.name, this.attributes, ">");
|
|
255
|
+
for (const child of this.children)
|
|
256
|
+
parts.push(child.toString());
|
|
257
|
+
parts.push("</" + this.name + ">");
|
|
258
|
+
return parts.join("");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
exports.SSRParentHTMLElement = SSRParentHTMLElement;
|
|
262
|
+
/** Base class for MathML and SVG elements, rendered as self-closing or parent tags. */
|
|
263
|
+
class SSRNSElement extends SSRElement {
|
|
264
|
+
toString() {
|
|
265
|
+
const parent = this.children.size > 0;
|
|
266
|
+
const parts = [];
|
|
267
|
+
(0, exports.voidToString)(parts, this.name, this.attributes, parent ? " >" : "/>");
|
|
268
|
+
if (parent) {
|
|
269
|
+
for (const child of this.children)
|
|
270
|
+
parts.push(child.toString());
|
|
271
|
+
parts.push("</" + this.name + ">");
|
|
272
|
+
}
|
|
273
|
+
return parts.join("");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
exports.SSRNSElement = SSRNSElement;
|
|
277
|
+
/** MathML element with the MathML namespace. */
|
|
278
|
+
class SSRMathMLElement extends SSRNSElement {
|
|
279
|
+
get namespaceURI() {
|
|
280
|
+
return common_1.MATHML_NAMESPACE;
|
|
281
|
+
}
|
|
282
|
+
get tagName() {
|
|
283
|
+
return this.name;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
exports.SSRMathMLElement = SSRMathMLElement;
|
|
287
|
+
/** SVG element with the SVG namespace. */
|
|
288
|
+
class SSRSVGElement extends SSRNSElement {
|
|
289
|
+
get namespaceURI() {
|
|
290
|
+
return common_1.SVG_NAMESPACE;
|
|
291
|
+
}
|
|
292
|
+
get tagName() {
|
|
293
|
+
return this.name;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
exports.SSRSVGElement = SSRSVGElement;
|
|
297
|
+
/**
|
|
298
|
+
* Minimal `MagnetWindow` implementation for server-side rendering.
|
|
299
|
+
*
|
|
300
|
+
* Use this as the `window` when constructing a Magnet context to render
|
|
301
|
+
* components to HTML strings on the server.
|
|
302
|
+
*/
|
|
303
|
+
exports.ssrWindow = {
|
|
304
|
+
document: {
|
|
305
|
+
createComment: (data) => new SSRComment(data),
|
|
306
|
+
createElement: (name) => new ((0, common_1.isVoid)(name) ? SSRVoidHTMLElement : SSRParentHTMLElement)(name),
|
|
307
|
+
createElementNS: (namespace, name) => new (namespace === common_1.MATHML_NAMESPACE ? SSRMathMLElement : SSRSVGElement)(name),
|
|
308
|
+
createTextNode: (data) => new SSRText(data),
|
|
309
|
+
},
|
|
310
|
+
};
|
package/script/wait.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { SignalAPI } from "@magnet-js/common";
|
|
2
|
+
/**
|
|
3
|
+
* Marks a single unit of pending asynchronous work as complete.
|
|
4
|
+
*
|
|
5
|
+
* Returned by {@link Wait}. Each call must balance a prior call to the
|
|
6
|
+
* wait-group factory. Calling the same `Done` more than once is a no-op.
|
|
7
|
+
*/
|
|
8
|
+
export interface Done {
|
|
9
|
+
(): void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Wait-group factory used to coordinate asynchronous work during SSR.
|
|
13
|
+
*
|
|
14
|
+
* Magnet components can start async operations (for example data fetching)
|
|
15
|
+
* and register each operation by calling `wait()`. The returned {@link Done}
|
|
16
|
+
* callback is invoked when the operation finishes. The `done` promise
|
|
17
|
+
* resolves once every registered operation has completed, signaling that
|
|
18
|
+
* the server can safely serialize the rendered tree.
|
|
19
|
+
*/
|
|
20
|
+
export interface Wait {
|
|
21
|
+
/** Registers one pending operation and returns its completion callback. */
|
|
22
|
+
(): Done;
|
|
23
|
+
/**
|
|
24
|
+
* Resolves the first time every registered operation has completed.
|
|
25
|
+
*
|
|
26
|
+
* Once resolved, this promise never returns to a pending state; the wait
|
|
27
|
+
* group must be fully populated before the counter reaches zero.
|
|
28
|
+
*/
|
|
29
|
+
done: Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Creates a wait group for coordinating async work during server-side
|
|
33
|
+
* rendering.
|
|
34
|
+
*
|
|
35
|
+
* Use it when a component triggers asynchronous tasks whose results update
|
|
36
|
+
* signals. The server awaits `wait.done` before stringifying the rendered
|
|
37
|
+
* DOM tree.
|
|
38
|
+
*
|
|
39
|
+
* The returned `done` promise resolves the first time the pending counter
|
|
40
|
+
* reaches zero. After that it stays resolved forever, so every unit of work
|
|
41
|
+
* must be registered before the counter reaches zero.
|
|
42
|
+
*
|
|
43
|
+
* The `signal` argument provides the reactive primitives used to detect
|
|
44
|
+
* completion. A one-shot effect watches an internal settled signal and
|
|
45
|
+
* resolves `done` the first time the signal becomes true, after any signal
|
|
46
|
+
* effects scheduled by the async work have run.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* const ready = wait(tc39);
|
|
51
|
+
*
|
|
52
|
+
* const page = app(() => {
|
|
53
|
+
* const data = state<string | null>(null);
|
|
54
|
+
* const done = ready();
|
|
55
|
+
* fetchData().then((value) => {
|
|
56
|
+
* data.set(value);
|
|
57
|
+
* done();
|
|
58
|
+
* });
|
|
59
|
+
* return div(data);
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* await ready.done;
|
|
63
|
+
* console.log(page.toString());
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare const wait: (signal: Pick<SignalAPI, "state" | "effect">) => Wait;
|
|
67
|
+
//# sourceMappingURL=wait.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wait.d.ts","sourceRoot":"","sources":["../src/wait.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,WAAW,IAAI;IACnB,IAAI,IAAI,CAAC;CACV;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,IAAI;IACnB,2EAA2E;IAC3E,IAAI,IAAI,CAAC;IAET;;;;;OAKG;IACH,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,IAAI,WAAY,IAAI,CAAC,SAAS,EAAE,OAAO,GAAG,QAAQ,CAAC,KAAG,IAyBlE,CAAC"}
|
package/script/wait.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.wait = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Creates a wait group for coordinating async work during server-side
|
|
6
|
+
* rendering.
|
|
7
|
+
*
|
|
8
|
+
* Use it when a component triggers asynchronous tasks whose results update
|
|
9
|
+
* signals. The server awaits `wait.done` before stringifying the rendered
|
|
10
|
+
* DOM tree.
|
|
11
|
+
*
|
|
12
|
+
* The returned `done` promise resolves the first time the pending counter
|
|
13
|
+
* reaches zero. After that it stays resolved forever, so every unit of work
|
|
14
|
+
* must be registered before the counter reaches zero.
|
|
15
|
+
*
|
|
16
|
+
* The `signal` argument provides the reactive primitives used to detect
|
|
17
|
+
* completion. A one-shot effect watches an internal settled signal and
|
|
18
|
+
* resolves `done` the first time the signal becomes true, after any signal
|
|
19
|
+
* effects scheduled by the async work have run.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const ready = wait(tc39);
|
|
24
|
+
*
|
|
25
|
+
* const page = app(() => {
|
|
26
|
+
* const data = state<string | null>(null);
|
|
27
|
+
* const done = ready();
|
|
28
|
+
* fetchData().then((value) => {
|
|
29
|
+
* data.set(value);
|
|
30
|
+
* done();
|
|
31
|
+
* });
|
|
32
|
+
* return div(data);
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* await ready.done;
|
|
36
|
+
* console.log(page.toString());
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
const wait = (signal) => {
|
|
40
|
+
let resolve;
|
|
41
|
+
const done = new Promise((r) => {
|
|
42
|
+
resolve = r;
|
|
43
|
+
});
|
|
44
|
+
let counter = 0;
|
|
45
|
+
const settled = signal.state(false);
|
|
46
|
+
const dispose = signal.effect(() => {
|
|
47
|
+
if (settled.get()) {
|
|
48
|
+
dispose();
|
|
49
|
+
resolve();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
const waiter = () => {
|
|
53
|
+
counter++;
|
|
54
|
+
let callback = () => {
|
|
55
|
+
if (!--counter)
|
|
56
|
+
settled.set(true);
|
|
57
|
+
};
|
|
58
|
+
return () => callback = callback?.();
|
|
59
|
+
};
|
|
60
|
+
waiter.done = done;
|
|
61
|
+
return waiter;
|
|
62
|
+
};
|
|
63
|
+
exports.wait = wait;
|