@firsthandjs/server 0.9.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/README.md +73 -0
- package/dist/chunk-2FO6Y3CD.js +1 -0
- package/dist/component.d.ts +38 -0
- package/dist/component.d.ts.map +1 -0
- package/dist/html.d.ts +60 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/internal.d.ts +14 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +1 -0
- package/dist/markup.d.ts +35 -0
- package/dist/markup.d.ts.map +1 -0
- package/dist/render.d.ts +87 -0
- package/dist/render.d.ts.map +1 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Firsthand contributors
|
|
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/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @firsthandjs/server
|
|
2
|
+
|
|
3
|
+
Server rendering for Firsthand: the same components, rendered to markup.
|
|
4
|
+
|
|
5
|
+
**Documentation:** [guide](https://github.com/firsthandjs/firsthand/blob/main/docs/guide/17-server-rendering.md) · [API reference](https://github.com/firsthandjs/firsthand/blob/main/docs/reference/server.md) · [ADR-0027](https://github.com/firsthandjs/firsthand/blob/main/docs/adr/0027-server-rendering-and-hydration.md)
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm install @firsthandjs/server
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
1.17 kB gzip, on a server, so it is not part of any browser's download. It
|
|
12
|
+
depends on `@firsthandjs/core` and never touches a `document`.
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { renderToString } from '@firsthandjs/server';
|
|
16
|
+
|
|
17
|
+
const html = renderToString(() => <App />);
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The application is the application. Nothing in it is written for a server:
|
|
21
|
+
`App` is the same file the browser renders, compiled a second time against a
|
|
22
|
+
runtime that builds a string instead of a tree. Vite already knows which build
|
|
23
|
+
it is running, so the project configures nothing beyond saying that it
|
|
24
|
+
hydrates:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
firsthand({ hydratable: true });
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## With data
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
const storage = createMemoryStorage();
|
|
34
|
+
const data = createData({ storage });
|
|
35
|
+
|
|
36
|
+
const html = await renderToStringAsync(() => <App data={data} />, {
|
|
37
|
+
settle: () => data.settle(),
|
|
38
|
+
timeout: 2_000,
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`storage.dump()` goes into the page, and a browser that starts from it finds
|
|
43
|
+
every named resource already answered — during its first run, not a microtask
|
|
44
|
+
later, which is what makes the first paint the markup rather than a spinner
|
|
45
|
+
replacing it.
|
|
46
|
+
|
|
47
|
+
## In the browser
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { hydrate } from '@firsthandjs/dom/hydrate';
|
|
51
|
+
|
|
52
|
+
hydrate(() => <App data={data} />, document.getElementById('app')!);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Every element the server sent is adopted rather than built. The only writes are
|
|
56
|
+
the listeners and the properties markup cannot express — which
|
|
57
|
+
`tests/browser/ssr.spec.ts` asserts in Chromium, Firefox and WebKit with a
|
|
58
|
+
`MutationObserver` installed before any script the page carries.
|
|
59
|
+
|
|
60
|
+
## Measured
|
|
61
|
+
|
|
62
|
+
1 000 rows, production builds on every side, output compared before anything is
|
|
63
|
+
timed (`node benchmarks/ssr/run.mjs`):
|
|
64
|
+
|
|
65
|
+
| | Firsthand | Solid | Vue | React |
|
|
66
|
+
| ---------------- | ------------ | --------- | --------- | --------- |
|
|
67
|
+
| render to markup | **0.171 ms** | 0.205 ms | 15.02 ms | 199.67 ms |
|
|
68
|
+
| markup size | 222 802 B | 238 694 B | 222 802 B | 222 802 B |
|
|
69
|
+
| hydrate | **4.12 ms** | 4.62 ms | 10.48 ms | — |
|
|
70
|
+
|
|
71
|
+
A complete example — application, two entry points, and a server in eighty
|
|
72
|
+
lines of `node:http` — is in
|
|
73
|
+
[`examples/ssr`](https://github.com/firsthandjs/firsthand/tree/main/examples/ssr).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var u=/[&<>]/,l=/[&"]/;function f(n){if(!u.test(n))return n;let t="",r=0;for(let e=0;e<n.length;e++){let s=n.charCodeAt(e),i;if(s===38)i="&";else if(s===60)i="<";else if(s===62)i=">";else continue;t+=n.slice(r,e)+i,r=e+1}return t+n.slice(r)}function p(n){if(!l.test(n))return n;let t="",r=0;for(let e=0;e<n.length;e++){let s=n.charCodeAt(e),i;if(s===38)i="&";else if(s===34)i=""";else continue;t+=n.slice(r,e)+i,r=e+1}return t+n.slice(r)}function c(n,t){return t==null||t===!1?"":t===!0?` ${n}=""`:` ${n}="${p(String(t))}"`}function T(n,t){return t===!0||t!=null&&t!==!1&&t?` ${d(n)}=""`:""}function d(n){return n==="readOnly"?"readonly":n.toLowerCase()}var a={value:"value",textContent:"",innerHTML:"",innerText:"",scrollTop:"",scrollLeft:"",volume:"",currentTime:"",playbackRate:"",srcObject:"",className:"class",class:"class",htmlFor:"for"};function x(n,t){let r=a[n];return r===void 0||r===""?"":c(r,t)}function k(n){return c("class",y(n))}function y(n){if(n==null)return null;if(typeof n=="string")return n;if(Array.isArray(n))return n.filter(Boolean).join(" ");if(typeof n=="object"){let t=[];for(let r in n)n[r]&&t.push(r);return t.length===0?null:t.join(" ")}return String(n)}function b(n){if(n==null)return"";if(typeof n!="object")return c("style",String(n));let t=[];for(let r in n){let e=n[r];e!=null&&t.push(`${w(r)}: ${String(e)};`)}return t.length===0?"":c("style",t.join(" "))}var h=/[A-Z]/g;function w(n){return n.startsWith("--")?n:n.replace(h,t=>`-${t.toLowerCase()}`)}var o=class{constructor(t){this.html=t}html};function E(n,...t){if(t.length===0)return new o(n[0]);let r=n[0];for(let e=0;e<t.length;e++)r+=t[e]??"",r+=n[e+1];return new o(r)}function g(n){let t=typeof n;if(t==="object"){if(n===null)return"";if(n instanceof o)return n.html}else{if(t==="string")return f(n);if(t==="number"||t==="bigint")return String(n);if(n===void 0||t==="boolean")return""}if(Array.isArray(n)){let r="";for(let e=0;e<n.length;e++)r+=g(n[e]);return r}return t==="function"?g(n()):f(String(n))}export{f as a,p as b,c,T as d,x as e,k as f,b as g,o as h,E as i,g as j};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instantiating a component where there is no DOM.
|
|
3
|
+
*
|
|
4
|
+
* The twin of `createComponent` in `@firsthandjs/dom`, and deliberately the
|
|
5
|
+
* same shape: props are frozen and handed over untouched, the setup runs once
|
|
6
|
+
* under its own owner, and what it returns is the view. The only difference is
|
|
7
|
+
* what a view *is* — markup rather than nodes.
|
|
8
|
+
*
|
|
9
|
+
* The symbols are `Symbol.for`, so this recognises a component and a view
|
|
10
|
+
* function without importing the DOM package at all. A server that never
|
|
11
|
+
* touches `@firsthandjs/dom` cannot accidentally reach for a `document`.
|
|
12
|
+
*/
|
|
13
|
+
declare const COMPONENT: unique symbol;
|
|
14
|
+
declare const VIEW: unique symbol;
|
|
15
|
+
type Declared = {
|
|
16
|
+
readonly [COMPONENT]?: true;
|
|
17
|
+
readonly [VIEW]?: true;
|
|
18
|
+
readonly setup?: (props: unknown) => unknown;
|
|
19
|
+
readonly name?: string;
|
|
20
|
+
tag?: string | undefined;
|
|
21
|
+
readonly options?: {
|
|
22
|
+
readonly shadow?: boolean;
|
|
23
|
+
} | undefined;
|
|
24
|
+
};
|
|
25
|
+
/** Emitted by the compiler for a module-level function it compiled markup into. */
|
|
26
|
+
export declare function view<T>(target: T): T;
|
|
27
|
+
export declare function createComponent(target: Declared, props: unknown): unknown;
|
|
28
|
+
/**
|
|
29
|
+
* `{...props}` as markup.
|
|
30
|
+
*
|
|
31
|
+
* `spread` in the browser applies whatever it is given by the same rules the
|
|
32
|
+
* named attributes follow, so this has to as well — including leaving out the
|
|
33
|
+
* things a server cannot write: handlers, refs and properties with no
|
|
34
|
+
* attribute behind them.
|
|
35
|
+
*/
|
|
36
|
+
export declare function spread(values: Record<string, unknown>): string;
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=component.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,QAAA,MAAM,SAAS,EAAE,OAAO,MAAmD,CAAC;AAC5E,QAAA,MAAM,IAAI,EAAE,OAAO,MAA8C,CAAC;AAElE,KAAK,QAAQ,GAAG;IACd,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC;IAC5B,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IAC7C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAC;CAC9D,CAAC;AAEF,mFAAmF;AACnF,wBAAgB,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAGpC;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAuCzE;AAkBD;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAiB9D"}
|
package/dist/html.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning values into markup, with exactly the meaning the DOM layer gives
|
|
3
|
+
* them.
|
|
4
|
+
*
|
|
5
|
+
* Every function here has a twin in `@firsthandjs/dom`'s `attributes.ts`, and
|
|
6
|
+
* the pairs have to agree: what the server writes is what the client would
|
|
7
|
+
* have built, or hydration adopts a tree that does not match and the
|
|
8
|
+
* difference shows up as a repaint nobody asked for.
|
|
9
|
+
*
|
|
10
|
+
* The agreement is not asserted by reading the two files. It is asserted by
|
|
11
|
+
* rendering the same component both ways and comparing the markup, which is
|
|
12
|
+
* what `packages/server/test/parity.test.tsx` does for every shape the
|
|
13
|
+
* compiler can emit.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Text, escaped for a child position.
|
|
17
|
+
*
|
|
18
|
+
* One pass over the characters, and no allocation at all when there is
|
|
19
|
+
* nothing to escape — which is almost every string a page contains. A regular
|
|
20
|
+
* expression test followed by three `replace` calls reads better and measured
|
|
21
|
+
* slower on the server benchmark, where escaping happens twice per row.
|
|
22
|
+
*
|
|
23
|
+
* `&` is handled by the same pass as the others, or the escapes would escape
|
|
24
|
+
* each other. `>` is not strictly required in text, but a stray one is a
|
|
25
|
+
* smell worth not emitting.
|
|
26
|
+
*/
|
|
27
|
+
export declare function escapeText(value: string): string;
|
|
28
|
+
/** Text, escaped for a double-quoted attribute value. The same pass. */
|
|
29
|
+
export declare function escapeAttribute(value: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* One attribute, or nothing at all.
|
|
32
|
+
*
|
|
33
|
+
* `setAttribute` in the DOM layer removes the attribute for `null`,
|
|
34
|
+
* `undefined` and `false`, and writes an empty value for `true`. The absent
|
|
35
|
+
* cases have to produce no attribute here rather than an empty one, because
|
|
36
|
+
* `title=""` and no `title` are different trees.
|
|
37
|
+
*/
|
|
38
|
+
export declare function attribute(name: string, value: unknown): string;
|
|
39
|
+
/**
|
|
40
|
+
* A boolean DOM property, as the attribute that stands for it.
|
|
41
|
+
*
|
|
42
|
+
* The client writes `node.checked = true`, which is a property and leaves no
|
|
43
|
+
* attribute behind. The server has only attributes, so it writes the one the
|
|
44
|
+
* parser turns back into that property.
|
|
45
|
+
*/
|
|
46
|
+
export declare function booleanAttribute(name: string, value: unknown): string;
|
|
47
|
+
/**
|
|
48
|
+
* A DOM property, as markup.
|
|
49
|
+
*
|
|
50
|
+
* Most properties have no attribute that reproduces them — a `scrollTop` is
|
|
51
|
+
* not something HTML can say — and those produce nothing: the client sets them
|
|
52
|
+
* during hydration, which is the only time they can be set at all. `value` is
|
|
53
|
+
* the one that matters in practice, and the parser does seed it.
|
|
54
|
+
*/
|
|
55
|
+
export declare function property(name: string, value: unknown): string;
|
|
56
|
+
/** `class` from a string, an object or an array, as the DOM layer reads it. */
|
|
57
|
+
export declare function classValue(value: unknown): string;
|
|
58
|
+
/** `style` from a string or an object. */
|
|
59
|
+
export declare function styleValue(value: unknown): string;
|
|
60
|
+
//# sourceMappingURL=html.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CA4BhD;AAED,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAsBrD;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAQ9D;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAIrE;AAwBD;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAM7D;AAED,+EAA+E;AAC/E,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEjD;AA6BD,0CAA0C;AAC1C,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAejD"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side rendering for Firsthand.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import { renderToString } from '@firsthandjs/server';
|
|
6
|
+
*
|
|
7
|
+
* const html = renderToString(() => <App />);
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* The application is the application: the same components, the same signals,
|
|
11
|
+
* the same context. What is different is only what cannot exist without a
|
|
12
|
+
* browser — effects do not run, and refs and handlers are attached later, by
|
|
13
|
+
* `hydrate` in `@firsthandjs/dom`.
|
|
14
|
+
*/
|
|
15
|
+
export { renderToString, renderToStringAsync, type AsyncRenderOptions, type RenderOptions, } from './render.js';
|
|
16
|
+
export { escapeAttribute, escapeText } from './html.js';
|
|
17
|
+
export { Markup } from './markup.js';
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,KAAK,kBAAkB,EACvB,KAAK,aAAa,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as l,b as O,h as w,j as u}from"./chunk-2FO6Y3CD.js";import{createOwner as c,disposeOwner as f,setOwner as d,setRendering as p,untrack as g}from"@firsthandjs/core";function R(n,e={}){let r=c(null);try{return m(i(n,r),e)}finally{f(r)}}async function T(n,e={}){let r=c(null);try{let t=i(n,r),o=e.settle;if(o!==void 0){let a=e.passes??5;for(let s=1;s<a;s++){let y=t;if(await x(o(),e.timeout),t=i(n,r),t===y)break}}return m(t,e)}finally{f(r)}}async function x(n,e){if(e===void 0)return n;let r;try{await Promise.race([n,new Promise(t=>{r=setTimeout(t,e)})])}finally{clearTimeout(r)}}function i(n,e){let r=d(e),t=p(!0);try{return u(g(n))}finally{p(t),d(r)}}function m(n,e){return e.document===void 0?n:e.document(n)}export{w as Markup,O as escapeAttribute,l as escapeText,R as renderToString,T as renderToStringAsync};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The compiler/runtime protocol for a server render.
|
|
3
|
+
*
|
|
4
|
+
* The twin of `@firsthandjs/dom/internal`: the compiler emits calls against
|
|
5
|
+
* exactly this surface when it is asked for server output, and against the
|
|
6
|
+
* DOM one otherwise. Two surfaces, one set of semantics — which is a promise
|
|
7
|
+
* that has to be kept by measurement, not by care, and is: the parity suite
|
|
8
|
+
* renders every shape both ways and compares the markup.
|
|
9
|
+
*/
|
|
10
|
+
export declare const PROTOCOL_VERSION = 1;
|
|
11
|
+
export { ssr, child, Markup } from './markup.js';
|
|
12
|
+
export { createComponent, spread, view } from './component.js';
|
|
13
|
+
export { attribute, attribute as setAttribute, booleanAttribute as setBoolean, classValue as setClass, escapeAttribute, escapeText, property as setProperty, styleValue as setStyle, } from './html.js';
|
|
14
|
+
//# sourceMappingURL=internal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,EACL,SAAS,EACT,SAAS,IAAI,YAAY,EACzB,gBAAgB,IAAI,UAAU,EAC9B,UAAU,IAAI,QAAQ,EACtB,eAAe,EACf,UAAU,EACV,QAAQ,IAAI,WAAW,EACvB,UAAU,IAAI,QAAQ,GACvB,MAAM,WAAW,CAAC"}
|
package/dist/internal.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as l,b as p,c as t,d,e as f,f as m,g as w,h as i,i as y,j as a}from"./chunk-2FO6Y3CD.js";import{deferOwner as h,getOwner as b,handleError as k,restoreOwner as u}from"@firsthandjs/core";var c=Symbol.for("firsthand.view");function g(n){return n[c]=!0,n}function O(n,r){if(n.setup===void 0){if(c in n)return n(r);let o=typeof n=="function"&&n.name!==""?n.name:"The value";throw new Error(`${String(o)} is not a Firsthand component, and a server render has no adapter to hand it to. Declare it with component(), or render it in the browser only.`)}let e=h();try{let o=n.setup(r);return u(e),n.tag===void 0?o:x(n,o)}catch(o){let s=b();return u(e),k(o,s),null}}function x(n,r){let e=n.tag,o=a(r),s=n.options?.shadow===!0?`<template shadowrootmode="open">${o}</template>`:o;return new i(`<${e}>${s}</${e}>`)}function T(n){let r="";for(let e in n){if(e==="ref"||e==="children"||E(e))continue;let o=n[e];if(e==="class"||e==="className"){r+=t("class",typeof o=="string"?o:A(o));continue}e==="style"&&typeof o=="object"||(r+=t(e,o))}return r}function E(n){return n.startsWith("on")&&n.length>2&&/[A-Z:]/.test(n[2])}function A(n){if(n==null)return null;if(Array.isArray(n))return n.filter(Boolean).join(" ");if(typeof n=="object"){let r=[];for(let e in n)n[e]===!0&&r.push(e);return r.join(" ")}return String(n)}var M=1;export{i as Markup,M as PROTOCOL_VERSION,t as attribute,a as child,O as createComponent,p as escapeAttribute,l as escapeText,t as setAttribute,d as setBoolean,m as setClass,f as setProperty,w as setStyle,T as spread,y as ssr,g as view};
|
package/dist/markup.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the compiler's server output is made of.
|
|
3
|
+
*
|
|
4
|
+
* A view is a **string** on the server, and that creates exactly one problem:
|
|
5
|
+
* once markup and text are both strings, nothing can tell them apart, and a
|
|
6
|
+
* component that returns `<b>hi</b>` would be escaped into visible angle
|
|
7
|
+
* brackets while a user string containing `<script>` would not be.
|
|
8
|
+
*
|
|
9
|
+
* So markup is carried in a one-field object and text is not. `ssr()` produces
|
|
10
|
+
* markup; `child()` escapes everything that is not already markup. The object
|
|
11
|
+
* costs one allocation per element, which is a fraction of the DOM node the
|
|
12
|
+
* client would have built for the same thing.
|
|
13
|
+
*/
|
|
14
|
+
export declare class Markup {
|
|
15
|
+
readonly html: string;
|
|
16
|
+
constructor(html: string);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Interleaves a template's static parts with the values between them.
|
|
20
|
+
*
|
|
21
|
+
* `parts` is one longer than `values`, always: the compiler splits the markup
|
|
22
|
+
* at every hole, so a template with no holes is a single part and a template
|
|
23
|
+
* ending in a hole still has an empty part after it.
|
|
24
|
+
*/
|
|
25
|
+
export declare function ssr(parts: readonly string[], ...values: readonly unknown[]): Markup;
|
|
26
|
+
/**
|
|
27
|
+
* A child position: markup passes through, everything else is escaped.
|
|
28
|
+
*
|
|
29
|
+
* The shapes are the ones `applyChild` accepts in the browser, so that a view
|
|
30
|
+
* that renders on the client renders the same thing here: nothing for `null`,
|
|
31
|
+
* `undefined` and booleans, the text for a string or a number, and the
|
|
32
|
+
* concatenation for an array.
|
|
33
|
+
*/
|
|
34
|
+
export declare function child(value: unknown): string;
|
|
35
|
+
//# sourceMappingURL=markup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markup.d.ts","sourceRoot":"","sources":["../src/markup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,qBAAa,MAAM;IACL,QAAQ,CAAC,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;CAClC;AAED;;;;;;GAMG;AACH,wBAAgB,GAAG,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,MAAM,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,CAYnF;AAED;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CA+B5C"}
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a view into markup.
|
|
3
|
+
*
|
|
4
|
+
* The same components, the same signals, the same context — compiled to build
|
|
5
|
+
* a string instead of a tree. What differs from the browser is only what
|
|
6
|
+
* cannot exist without one:
|
|
7
|
+
*
|
|
8
|
+
* - **Effects do not run.** An effect is a side effect over time, and a render
|
|
9
|
+
* that produces one string has none. Anything that must happen before the
|
|
10
|
+
* markup exists is data, and data has `useResource`.
|
|
11
|
+
* - **Refs and event handlers are not emitted.** A server has no node to hand
|
|
12
|
+
* to a ref and no one to click. Both are attached during hydration, where
|
|
13
|
+
* they are the only things left to do.
|
|
14
|
+
*/
|
|
15
|
+
export type RenderOptions = {
|
|
16
|
+
/**
|
|
17
|
+
* Wraps the markup, so the document a server sends is written in one place.
|
|
18
|
+
*
|
|
19
|
+
* It receives the rendered body and returns the whole response; leaving it
|
|
20
|
+
* out returns the body alone, which is what a fragment or a test wants.
|
|
21
|
+
*/
|
|
22
|
+
document?: (body: string) => string;
|
|
23
|
+
};
|
|
24
|
+
export type AsyncRenderOptions = RenderOptions & {
|
|
25
|
+
/**
|
|
26
|
+
* Waits for whatever the render started.
|
|
27
|
+
*
|
|
28
|
+
* A data store's `settle()`, a cache client's, or both — the server package
|
|
29
|
+
* knows nothing about either, so the application says what waiting means:
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* const html = await renderToStringAsync(() => <App />, {
|
|
33
|
+
* settle: () => data.settle(),
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
settle?: () => Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* How many times to render before giving up on stillness.
|
|
40
|
+
*
|
|
41
|
+
* Each pass renders, waits, and renders again if the wait produced more
|
|
42
|
+
* work. Default 5, which is four chances for one load to reveal the next;
|
|
43
|
+
* past that the last markup is sent as it stands rather than never.
|
|
44
|
+
*/
|
|
45
|
+
passes?: number;
|
|
46
|
+
/**
|
|
47
|
+
* How long to wait, in ms, before rendering with what is there.
|
|
48
|
+
*
|
|
49
|
+
* A loader that never answers would otherwise hold the response open for as
|
|
50
|
+
* long as the client is willing to wait, which is a page that never arrives
|
|
51
|
+
* rather than one that arrives incomplete. Left out, the wait is however
|
|
52
|
+
* long the application's own loaders take — which is the right default when
|
|
53
|
+
* they have timeouts of their own, and the wrong one when they do not.
|
|
54
|
+
*/
|
|
55
|
+
timeout?: number;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Renders a view to markup.
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* const html = renderToString(() => <App />);
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* Everything the view creates belongs to a root that is disposed before this
|
|
65
|
+
* returns, so a server that renders a thousand requests holds nothing from any
|
|
66
|
+
* of them.
|
|
67
|
+
*/
|
|
68
|
+
export declare function renderToString(view: () => unknown, options?: RenderOptions): string;
|
|
69
|
+
/**
|
|
70
|
+
* Renders a view that loads something first.
|
|
71
|
+
*
|
|
72
|
+
* ```ts
|
|
73
|
+
* const html = await renderToStringAsync(() => <App />, {
|
|
74
|
+
* settle: () => data.settle(),
|
|
75
|
+
* });
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* The view is rendered, the wait is awaited, and the view is rendered again —
|
|
79
|
+
* over the same owner, so a component's setup runs once and its resources are
|
|
80
|
+
* not started twice. What the second pass reads is what the first pass asked
|
|
81
|
+
* for, now answered.
|
|
82
|
+
*
|
|
83
|
+
* Without `settle` this is `renderToString` with a promise around it, which is
|
|
84
|
+
* what a view with nothing to wait for should cost.
|
|
85
|
+
*/
|
|
86
|
+
export declare function renderToStringAsync(view: () => unknown, options?: AsyncRenderOptions): Promise<string>;
|
|
87
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAYH,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,aAAa,GAAG;IAC/C;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,OAAO,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,CAOvF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,OAAO,EACnB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,MAAM,CAAC,CAoBjB"}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@firsthandjs/server",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Server-side rendering for Firsthand: the same components, rendered to markup.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/firsthandjs/firsthand.git",
|
|
10
|
+
"directory": "packages/server"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/firsthandjs/firsthand#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/firsthandjs/firsthand/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"reactive",
|
|
18
|
+
"signals",
|
|
19
|
+
"ssr",
|
|
20
|
+
"server-side-rendering",
|
|
21
|
+
"hydration",
|
|
22
|
+
"jsx",
|
|
23
|
+
"no-virtual-dom"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20.11.0"
|
|
27
|
+
},
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./internal": {
|
|
35
|
+
"types": "./dist/internal.d.ts",
|
|
36
|
+
"default": "./dist/internal.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@firsthandjs/core": "0.9.0"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public",
|
|
49
|
+
"provenance": true
|
|
50
|
+
}
|
|
51
|
+
}
|