hadars 0.1.40 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -70
- package/cli-lib.ts +89 -12
- package/dist/chunk-HWOLYLPF.js +332 -0
- package/dist/{chunk-2ENP7IAW.js → chunk-LY5MTHFV.js} +360 -203
- package/dist/cli.js +506 -274
- package/dist/cloudflare.cjs +1394 -0
- package/dist/cloudflare.d.cts +64 -0
- package/dist/cloudflare.d.ts +64 -0
- package/dist/cloudflare.js +68 -0
- package/dist/{hadars-Bh-V5YXg.d.cts → hadars-DEBSYAQl.d.cts} +1 -36
- package/dist/{hadars-Bh-V5YXg.d.ts → hadars-DEBSYAQl.d.ts} +1 -36
- package/dist/index.cjs +129 -156
- package/dist/index.d.cts +5 -11
- package/dist/index.d.ts +5 -11
- package/dist/index.js +129 -155
- package/dist/lambda.cjs +391 -229
- package/dist/lambda.d.cts +1 -2
- package/dist/lambda.d.ts +1 -2
- package/dist/lambda.js +18 -307
- package/dist/slim-react/index.cjs +361 -203
- package/dist/slim-react/index.d.cts +24 -8
- package/dist/slim-react/index.d.ts +24 -8
- package/dist/slim-react/index.js +3 -1
- package/dist/ssr-render-worker.js +352 -221
- package/dist/utils/Head.tsx +132 -187
- package/package.json +7 -2
- package/src/build.ts +7 -6
- package/src/cloudflare.ts +139 -0
- package/src/index.tsx +0 -3
- package/src/lambda.ts +6 -2
- package/src/slim-react/context.ts +2 -1
- package/src/slim-react/index.ts +21 -18
- package/src/slim-react/render.ts +379 -240
- package/src/slim-react/renderContext.ts +105 -45
- package/src/ssr-render-worker.ts +14 -44
- package/src/types/hadars.ts +0 -1
- package/src/utils/Head.tsx +132 -187
- package/src/utils/cookies.ts +1 -1
- package/src/utils/response.tsx +68 -33
- package/src/utils/serve.ts +29 -27
- package/src/utils/ssrHandler.ts +54 -25
- package/src/utils/staticFile.ts +2 -7
|
@@ -36,6 +36,23 @@ export function captureMap(): Map<object, unknown> | null {
|
|
|
36
36
|
return _g[MAP_KEY];
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
const UNSUSPEND_KEY = "__hadarsUnsuspend";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Capture the current __hadarsUnsuspend slot alongside captureMap() before
|
|
43
|
+
* an async boundary. Because useServerData reads this global, concurrent
|
|
44
|
+
* renders would corrupt each other's cache if it weren't restored after every
|
|
45
|
+
* await continuation — exactly like the context map itself.
|
|
46
|
+
*/
|
|
47
|
+
export function captureUnsuspend(): unknown {
|
|
48
|
+
return _g[UNSUSPEND_KEY];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Restore a previously captured __hadarsUnsuspend slot after an await. */
|
|
52
|
+
export function restoreUnsuspend(u: unknown): void {
|
|
53
|
+
_g[UNSUSPEND_KEY] = u;
|
|
54
|
+
}
|
|
55
|
+
|
|
39
56
|
/** Read the current value for a context within the active render. */
|
|
40
57
|
export function getContextValue<T>(context: object): T {
|
|
41
58
|
const map: Map<object, unknown> | null = _g[MAP_KEY];
|
|
@@ -49,12 +66,18 @@ export function getContextValue<T>(context: object): T {
|
|
|
49
66
|
* Returns the previous value so the caller can restore it on exit.
|
|
50
67
|
*/
|
|
51
68
|
export function pushContextValue(context: object, value: unknown): unknown {
|
|
52
|
-
|
|
69
|
+
let map: Map<object, unknown> | null = _g[MAP_KEY];
|
|
70
|
+
// Lazily create the Map on the first Provider encountered — renders without
|
|
71
|
+
// any Context.Provider never allocate a Map at all.
|
|
72
|
+
if (map === null) {
|
|
73
|
+
map = new Map();
|
|
74
|
+
_g[MAP_KEY] = map;
|
|
75
|
+
}
|
|
53
76
|
const c = context as any;
|
|
54
|
-
const prev = map
|
|
77
|
+
const prev = map.has(context)
|
|
55
78
|
? map.get(context)
|
|
56
79
|
: ("_defaultValue" in c ? c._defaultValue : c._currentValue);
|
|
57
|
-
map
|
|
80
|
+
map.set(context, value);
|
|
58
81
|
return prev;
|
|
59
82
|
}
|
|
60
83
|
|
|
@@ -84,19 +107,46 @@ const GLOBAL_KEY = "__slimReactRenderState";
|
|
|
84
107
|
// React 19's initial context is { id: 1, overflow: "" } — sentinel bit only.
|
|
85
108
|
const EMPTY: TreeContext = { id: 1, overflow: "" };
|
|
86
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Module-level cache for the shared RenderState singleton.
|
|
112
|
+
* Avoids a `globalThis[key]` property lookup on every push/pop/reset call
|
|
113
|
+
* (which happens multiple times per component during rendering).
|
|
114
|
+
* Both slim-react instances (direct import + SSR bundle copy) initialise the
|
|
115
|
+
* same `globalThis[GLOBAL_KEY]` object and then each cache that same reference,
|
|
116
|
+
* so correctness is preserved.
|
|
117
|
+
*/
|
|
118
|
+
let _stateCache: RenderState | null = null;
|
|
119
|
+
|
|
87
120
|
function s(): RenderState {
|
|
88
|
-
|
|
89
|
-
if (!
|
|
90
|
-
|
|
121
|
+
if (_stateCache !== null) return _stateCache;
|
|
122
|
+
if (!_g[GLOBAL_KEY]) {
|
|
123
|
+
_g[GLOBAL_KEY] = { currentTreeContext: { ...EMPTY }, localIdCounter: 0, idPrefix: "" };
|
|
91
124
|
}
|
|
92
|
-
|
|
125
|
+
_stateCache = _g[GLOBAL_KEY] as RenderState;
|
|
126
|
+
return _stateCache;
|
|
93
127
|
}
|
|
94
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Flat primitive stacks for pushTreeContext / popTreeContext.
|
|
131
|
+
*
|
|
132
|
+
* Instead of allocating a new `{ id, overflow }` object on every array child
|
|
133
|
+
* (which is the hot path — called once per child in every array render), we
|
|
134
|
+
* save the two scalar fields into parallel pre-allocated arrays and return a
|
|
135
|
+
* numeric depth index. No heap objects are allocated in the push. Both arrays
|
|
136
|
+
* grow lazily and are never shrunk, so after a few renders they stop growing.
|
|
137
|
+
*/
|
|
138
|
+
const _treeIdStack: number[] = [];
|
|
139
|
+
const _treeOvStack: string[] = [];
|
|
140
|
+
let _treeDepth = 0;
|
|
141
|
+
|
|
95
142
|
export function resetRenderState(idPrefix = "") {
|
|
96
143
|
const st = s();
|
|
97
|
-
|
|
144
|
+
// Mutate in place — avoids allocating a new TreeContext object each render.
|
|
145
|
+
st.currentTreeContext.id = EMPTY.id;
|
|
146
|
+
st.currentTreeContext.overflow = EMPTY.overflow;
|
|
98
147
|
st.localIdCounter = 0;
|
|
99
|
-
st.idPrefix
|
|
148
|
+
st.idPrefix = idPrefix;
|
|
149
|
+
_treeDepth = 0;
|
|
100
150
|
}
|
|
101
151
|
|
|
102
152
|
export function setIdPrefix(prefix: string) {
|
|
@@ -110,44 +160,48 @@ export function setIdPrefix(prefix: string) {
|
|
|
110
160
|
* - on overflow, the LOWEST bits of the old data move to the overflow string
|
|
111
161
|
* (rounded to a multiple of 5 so base-32 digits align on byte boundaries)
|
|
112
162
|
*/
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Push a new tree-context level. Returns a numeric depth token (not an
|
|
165
|
+
* object) so the caller can pop with zero heap allocation in the common case.
|
|
166
|
+
*/
|
|
167
|
+
export function pushTreeContext(totalChildren: number, index: number): number {
|
|
168
|
+
const st = s();
|
|
169
|
+
const ctx = st.currentTreeContext;
|
|
170
|
+
const depth = _treeDepth++;
|
|
171
|
+
|
|
172
|
+
// Save current scalars into the flat stacks — no object allocation.
|
|
173
|
+
_treeIdStack[depth] = ctx.id;
|
|
174
|
+
_treeOvStack[depth] = ctx.overflow;
|
|
116
175
|
|
|
117
|
-
const baseIdWithLeadingBit =
|
|
118
|
-
const baseOverflow
|
|
119
|
-
// Number of data bits currently stored (excludes the sentinel bit).
|
|
176
|
+
const baseIdWithLeadingBit = ctx.id;
|
|
177
|
+
const baseOverflow = ctx.overflow;
|
|
120
178
|
const baseLength = 31 - Math.clz32(baseIdWithLeadingBit);
|
|
121
|
-
|
|
122
|
-
let baseId = baseIdWithLeadingBit & ~(1 << baseLength);
|
|
179
|
+
let baseId = baseIdWithLeadingBit & ~(1 << baseLength);
|
|
123
180
|
|
|
124
|
-
const slot
|
|
125
|
-
const newBits = 32 - Math.clz32(totalChildren);
|
|
126
|
-
const length
|
|
181
|
+
const slot = index + 1;
|
|
182
|
+
const newBits = 32 - Math.clz32(totalChildren);
|
|
183
|
+
const length = newBits + baseLength;
|
|
127
184
|
|
|
185
|
+
// Mutate currentTreeContext in place — avoids allocating a new object.
|
|
128
186
|
if (30 < length) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const newBaseLength = baseLength - numberOfOverflowBits;
|
|
136
|
-
st.currentTreeContext = {
|
|
137
|
-
id: (1 << (newBits + newBaseLength)) | (slot << newBaseLength) | baseId,
|
|
138
|
-
overflow: overflowStr + baseOverflow,
|
|
139
|
-
};
|
|
187
|
+
const overflowBits = baseLength - (baseLength % 5);
|
|
188
|
+
const overflowStr = (baseId & ((1 << overflowBits) - 1)).toString(32);
|
|
189
|
+
baseId >>= overflowBits;
|
|
190
|
+
const newBaseLength = baseLength - overflowBits;
|
|
191
|
+
ctx.id = (1 << (newBits + newBaseLength)) | (slot << newBaseLength) | baseId;
|
|
192
|
+
ctx.overflow = overflowStr + baseOverflow;
|
|
140
193
|
} else {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
overflow: baseOverflow,
|
|
144
|
-
};
|
|
194
|
+
ctx.id = (1 << length) | (slot << baseLength) | baseId;
|
|
195
|
+
ctx.overflow = baseOverflow;
|
|
145
196
|
}
|
|
146
|
-
return
|
|
197
|
+
return depth;
|
|
147
198
|
}
|
|
148
199
|
|
|
149
|
-
export function popTreeContext(
|
|
150
|
-
s().currentTreeContext
|
|
200
|
+
export function popTreeContext(depth: number): void {
|
|
201
|
+
const ctx = s().currentTreeContext;
|
|
202
|
+
ctx.id = _treeIdStack[depth]!;
|
|
203
|
+
ctx.overflow = _treeOvStack[depth]!;
|
|
204
|
+
_treeDepth = depth;
|
|
151
205
|
}
|
|
152
206
|
|
|
153
207
|
export function pushComponentScope(): number {
|
|
@@ -166,15 +220,21 @@ export function componentCalledUseId(): boolean {
|
|
|
166
220
|
return s().localIdCounter > 0;
|
|
167
221
|
}
|
|
168
222
|
|
|
169
|
-
export function snapshotContext(): { tree: TreeContext; localId: number } {
|
|
170
|
-
const st
|
|
171
|
-
|
|
223
|
+
export function snapshotContext(): { tree: TreeContext; localId: number; treeDepth: number } {
|
|
224
|
+
const st = s();
|
|
225
|
+
const ctx = st.currentTreeContext;
|
|
226
|
+
// Copy scalars directly — avoids a spread allocation.
|
|
227
|
+
return { tree: { id: ctx.id, overflow: ctx.overflow }, localId: st.localIdCounter, treeDepth: _treeDepth };
|
|
172
228
|
}
|
|
173
229
|
|
|
174
|
-
export function restoreContext(snap: { tree: TreeContext; localId: number }) {
|
|
175
|
-
const st
|
|
176
|
-
|
|
177
|
-
|
|
230
|
+
export function restoreContext(snap: { tree: TreeContext; localId: number; treeDepth: number }): void {
|
|
231
|
+
const st = s();
|
|
232
|
+
const ctx = st.currentTreeContext;
|
|
233
|
+
// Mutate in place — avoids allocating a new TreeContext object.
|
|
234
|
+
ctx.id = snap.tree.id;
|
|
235
|
+
ctx.overflow = snap.tree.overflow;
|
|
236
|
+
st.localIdCounter = snap.localId;
|
|
237
|
+
_treeDepth = snap.treeDepth;
|
|
178
238
|
}
|
|
179
239
|
|
|
180
240
|
/**
|
package/src/ssr-render-worker.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { workerData, parentPort } from 'node:worker_threads';
|
|
14
14
|
import { pathToFileURL } from 'node:url';
|
|
15
15
|
import { renderToString, createElement } from './slim-react/index';
|
|
16
|
+
import { buildHeadHtml } from './utils/response';
|
|
16
17
|
|
|
17
18
|
const { ssrBundlePath } = workerData as { ssrBundlePath: string };
|
|
18
19
|
|
|
@@ -42,43 +43,6 @@ function deserializeRequest(s: SerializableRequest): any {
|
|
|
42
43
|
return req;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
// ── Head HTML serialisation ────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
const ESC: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"' };
|
|
48
|
-
const escAttr = (s: string) => s.replace(/[&<>"]/g, c => ESC[c] ?? c);
|
|
49
|
-
const escText = (s: string) => s.replace(/[&<>]/g, c => ESC[c] ?? c);
|
|
50
|
-
|
|
51
|
-
const HEAD_ATTR: Record<string, string> = {
|
|
52
|
-
className: 'class', htmlFor: 'for', httpEquiv: 'http-equiv',
|
|
53
|
-
charSet: 'charset', crossOrigin: 'crossorigin',
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
function renderHeadTag(tag: string, id: string, opts: Record<string, unknown>, selfClose = false): string {
|
|
57
|
-
let a = ` id="${escAttr(id)}"`;
|
|
58
|
-
let inner = '';
|
|
59
|
-
for (const [k, v] of Object.entries(opts)) {
|
|
60
|
-
if (k === 'key' || k === 'children') continue;
|
|
61
|
-
if (k === 'dangerouslySetInnerHTML') { inner = (v as any).__html ?? ''; continue; }
|
|
62
|
-
const attr = HEAD_ATTR[k] ?? k;
|
|
63
|
-
if (v === true) a += ` ${attr}`;
|
|
64
|
-
else if (v !== false && v != null) a += ` ${attr}="${escAttr(String(v))}"`;
|
|
65
|
-
}
|
|
66
|
-
return selfClose ? `<${tag}${a}>` : `<${tag}${a}>${inner}</${tag}>`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function buildHeadHtml(head: any): string {
|
|
70
|
-
let html = `<title>${escText(head.title ?? '')}</title>`;
|
|
71
|
-
for (const [id, opts] of Object.entries(head.meta ?? {}))
|
|
72
|
-
html += renderHeadTag('meta', id, opts as Record<string, unknown>, true);
|
|
73
|
-
for (const [id, opts] of Object.entries(head.link ?? {}))
|
|
74
|
-
html += renderHeadTag('link', id, opts as Record<string, unknown>, true);
|
|
75
|
-
for (const [id, opts] of Object.entries(head.style ?? {}))
|
|
76
|
-
html += renderHeadTag('style', id, opts as Record<string, unknown>);
|
|
77
|
-
for (const [id, opts] of Object.entries(head.script ?? {}))
|
|
78
|
-
html += renderHeadTag('script', id, opts as Record<string, unknown>);
|
|
79
|
-
return html;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
46
|
// ── Full lifecycle ─────────────────────────────────────────────────────────
|
|
83
47
|
|
|
84
48
|
async function runFullLifecycle(serialReq: SerializableRequest) {
|
|
@@ -94,39 +58,45 @@ async function runFullLifecycle(serialReq: SerializableRequest) {
|
|
|
94
58
|
let props: any = {
|
|
95
59
|
...(getInitProps ? await getInitProps(parsedReq) : {}),
|
|
96
60
|
location: serialReq.location,
|
|
97
|
-
context,
|
|
98
61
|
};
|
|
99
62
|
|
|
100
63
|
// Create per-request cache for useServerData, active for all renders.
|
|
101
64
|
const unsuspend = { cache: new Map<string, any>() };
|
|
102
65
|
(globalThis as any).__hadarsUnsuspend = unsuspend;
|
|
66
|
+
// Expose the head context so HadarsHead can write into it without needing
|
|
67
|
+
// the user to manually wrap their App with HadarsContext.
|
|
68
|
+
(globalThis as any).__hadarsContext = context;
|
|
103
69
|
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
// render pass needed.
|
|
70
|
+
// Single pass — component-level self-retry resolves all useServerData inline.
|
|
71
|
+
// context.head is fully populated by the time renderToString returns.
|
|
107
72
|
let appHtml: string;
|
|
108
73
|
try {
|
|
109
74
|
appHtml = await renderToString(createElement(Component, props));
|
|
110
75
|
} finally {
|
|
111
76
|
(globalThis as any).__hadarsUnsuspend = null;
|
|
77
|
+
(globalThis as any).__hadarsContext = null;
|
|
112
78
|
}
|
|
79
|
+
// Head is captured after the render — all components have run.
|
|
80
|
+
const headHtml = buildHeadHtml(context.head);
|
|
81
|
+
const status = context.head.status ?? 200;
|
|
113
82
|
const { context: _ctx, ...restProps } = getFinalProps ? await getFinalProps(props) : props;
|
|
114
83
|
|
|
115
84
|
// Collect fulfilled useServerData values for client-side hydration.
|
|
116
85
|
const serverData: Record<string, unknown> = {};
|
|
86
|
+
let hasServerData = false;
|
|
117
87
|
for (const [key, entry] of unsuspend.cache) {
|
|
118
|
-
if (entry.status === 'fulfilled') serverData[key] = entry.value;
|
|
88
|
+
if (entry.status === 'fulfilled') { serverData[key] = entry.value; hasServerData = true; }
|
|
119
89
|
}
|
|
120
90
|
const clientProps = {
|
|
121
91
|
...restProps,
|
|
122
92
|
location: serialReq.location,
|
|
123
|
-
...(
|
|
93
|
+
...(hasServerData ? { __serverData: serverData } : {}),
|
|
124
94
|
};
|
|
125
95
|
|
|
126
96
|
const scriptContent = JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, '\\u003c');
|
|
127
97
|
const html = `<div id="app">${appHtml}</div><script id="hadars" type="application/json">${scriptContent}</script>`;
|
|
128
98
|
|
|
129
|
-
return { html, headHtml
|
|
99
|
+
return { html, headHtml, status };
|
|
130
100
|
}
|
|
131
101
|
|
|
132
102
|
// ── Message handler ────────────────────────────────────────────────────────
|