@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 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
@@ -0,0 +1,4 @@
1
+ export { ssrWindow } from "./ssr.js";
2
+ export { wait } from "./wait.js";
3
+ export type { Done, Wait } from "./wait.js";
4
+ //# sourceMappingURL=mod.d.ts.map
@@ -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
@@ -0,0 +1,2 @@
1
+ export { ssrWindow } from "./ssr.js";
2
+ export { wait } from "./wait.js";
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
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
@@ -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 "&amp;";
46
+ case "<":
47
+ return "&lt;";
48
+ }
49
+ return "&gt;";
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
+ }
@@ -0,0 +1,4 @@
1
+ export { ssrWindow } from "./ssr.js";
2
+ export { wait } from "./wait.js";
3
+ export type { Done, Wait } from "./wait.js";
4
+ //# sourceMappingURL=mod.d.ts.map
@@ -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; } });
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -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 "&amp;";
49
+ case "<":
50
+ return "&lt;";
51
+ }
52
+ return "&gt;";
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
+ };
@@ -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;