what-core 0.8.3 → 0.10.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/dist/chunk-AW3BAPIK.js +1685 -0
- package/dist/chunk-AW3BAPIK.js.map +7 -0
- package/dist/chunk-AZP2EOGX.js +188 -0
- package/dist/chunk-AZP2EOGX.js.map +7 -0
- package/dist/chunk-F2HUXI22.js +1675 -0
- package/dist/chunk-F2HUXI22.js.map +7 -0
- package/dist/chunk-KBM6CWG4.min.js +2 -0
- package/dist/chunk-KBM6CWG4.min.js.map +7 -0
- package/dist/chunk-KL7TNUIU.min.js +2 -0
- package/dist/chunk-KL7TNUIU.min.js.map +7 -0
- package/dist/chunk-L6XOF7P4.min.js +2 -0
- package/dist/chunk-L6XOF7P4.min.js.map +7 -0
- package/dist/chunk-M7UEET5O.js +1323 -0
- package/dist/chunk-M7UEET5O.js.map +7 -0
- package/dist/chunk-O3SKPRTY.min.js +2 -0
- package/dist/chunk-O3SKPRTY.min.js.map +7 -0
- package/dist/chunk-RN6QIBWL.min.js +2 -0
- package/dist/chunk-RN6QIBWL.min.js.map +7 -0
- package/dist/chunk-VMTTYB4L.min.js +2 -0
- package/dist/chunk-VMTTYB4L.min.js.map +7 -0
- package/dist/chunk-VP4WLF5A.js +1323 -0
- package/dist/chunk-VP4WLF5A.js.map +7 -0
- package/dist/chunk-YA3W4XKH.js +1323 -0
- package/dist/chunk-YA3W4XKH.js.map +7 -0
- package/dist/index.js +212 -2785
- package/dist/index.js.map +4 -4
- package/dist/index.min.js +6 -6
- package/dist/index.min.js.map +4 -4
- package/dist/jsx-dev-runtime.js +4 -53
- package/dist/jsx-dev-runtime.js.map +3 -3
- package/dist/jsx-dev-runtime.min.js +1 -1
- package/dist/jsx-dev-runtime.min.js.map +4 -4
- package/dist/jsx-runtime.js +4 -53
- package/dist/jsx-runtime.js.map +3 -3
- package/dist/jsx-runtime.min.js +1 -1
- package/dist/jsx-runtime.min.js.map +4 -4
- package/dist/render.js +22 -2044
- package/dist/render.js.map +4 -4
- package/dist/render.min.js +1 -1
- package/dist/render.min.js.map +4 -4
- package/dist/testing.js +13 -1079
- package/dist/testing.js.map +4 -4
- package/dist/testing.min.js +1 -1
- package/dist/testing.min.js.map +4 -4
- package/package.json +2 -2
- package/src/dom.js +54 -6
- package/src/h.js +15 -3
- package/src/head.js +72 -2
- package/src/hooks.js +65 -4
- package/src/hydration-data.js +34 -0
- package/src/index.js +9 -2
- package/src/reactive.js +78 -1
- package/src/render.js +450 -105
- package/src/server-context.js +48 -0
- package/src/store.js +6 -2
package/src/head.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
// What Framework - Head Management
|
|
2
2
|
// Declarative <head> updates from any component.
|
|
3
|
-
// Supports title, meta, link
|
|
3
|
+
// Supports title, meta, link tags. Auto-deduplicates by key.
|
|
4
|
+
//
|
|
5
|
+
// Isomorphic: on the client it mutates document.head directly (last-one-wins);
|
|
6
|
+
// on the server it writes into the active render context's head sink (see
|
|
7
|
+
// server-context.js), which renderToStringWithHead serializes into <head> HTML.
|
|
8
|
+
// The server and client use IDENTICAL dedup keys so the rendered head matches
|
|
9
|
+
// what the client would produce — no hydration mismatch.
|
|
10
|
+
|
|
11
|
+
import { getServerContext } from './server-context.js';
|
|
4
12
|
|
|
5
13
|
const headState = {
|
|
6
14
|
title: null,
|
|
@@ -12,7 +20,13 @@ const headState = {
|
|
|
12
20
|
// Use in any component to set head tags. Last one wins for title/meta.
|
|
13
21
|
|
|
14
22
|
export function Head({ title, meta, link, children }) {
|
|
15
|
-
if (typeof document === 'undefined')
|
|
23
|
+
if (typeof document === 'undefined') {
|
|
24
|
+
// Server: collect into the active render context's sink (if a render is
|
|
25
|
+
// collecting head). No active context => no-op (renderToString body-only).
|
|
26
|
+
const ctx = getServerContext();
|
|
27
|
+
if (ctx && ctx.head) writeToSink(ctx.head, { title, meta, link });
|
|
28
|
+
return children ?? null;
|
|
29
|
+
}
|
|
16
30
|
|
|
17
31
|
if (title) {
|
|
18
32
|
document.title = title;
|
|
@@ -36,6 +50,62 @@ export function Head({ title, meta, link, children }) {
|
|
|
36
50
|
return children || null;
|
|
37
51
|
}
|
|
38
52
|
|
|
53
|
+
// --- Server-side head collection ---
|
|
54
|
+
|
|
55
|
+
function metaKey(attrs) {
|
|
56
|
+
return attrs.name || attrs.property || attrs.httpEquiv || JSON.stringify(attrs);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function writeToSink(sink, { title, meta, link }) {
|
|
60
|
+
if (title != null) sink.title = title;
|
|
61
|
+
if (meta) {
|
|
62
|
+
for (const attrs of (Array.isArray(meta) ? meta : [meta])) {
|
|
63
|
+
sink.metas.set(metaKey(attrs), attrs);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (link) {
|
|
67
|
+
for (const attrs of (Array.isArray(link) ? link : [link])) {
|
|
68
|
+
sink.links.set(attrs.rel + (attrs.href || ''), attrs);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Create a fresh head sink for one render. */
|
|
74
|
+
export function beginHeadCollection() {
|
|
75
|
+
return { title: null, metas: new Map(), links: new Map() };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Serialize a head sink into escaped <head> HTML (title + meta + link). */
|
|
79
|
+
export function endHeadCollection(sink) {
|
|
80
|
+
if (!sink) return '';
|
|
81
|
+
let out = '';
|
|
82
|
+
if (sink.title != null) out += `<title>${escapeHtml(String(sink.title))}</title>`;
|
|
83
|
+
for (const attrs of sink.metas.values()) out += renderHeadTag('meta', attrs);
|
|
84
|
+
for (const attrs of sink.links.values()) out += renderHeadTag('link', attrs);
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderHeadTag(tag, attrs) {
|
|
89
|
+
let s = `<${tag}`;
|
|
90
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
91
|
+
if (v == null || v === false) continue;
|
|
92
|
+
const name = k === 'httpEquiv' ? 'http-equiv' : k;
|
|
93
|
+
s += ` ${name}="${escapeHtml(String(v))}"`;
|
|
94
|
+
}
|
|
95
|
+
return s + '>';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function escapeHtml(str) {
|
|
99
|
+
return str
|
|
100
|
+
.replace(/&/g, '&')
|
|
101
|
+
.replace(/</g, '<')
|
|
102
|
+
.replace(/>/g, '>')
|
|
103
|
+
.replace(/"/g, '"')
|
|
104
|
+
.replace(/'/g, ''');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- Client DOM helpers ---
|
|
108
|
+
|
|
39
109
|
function setHeadTag(tag, key, attrs) {
|
|
40
110
|
const existing = document.head.querySelector(`[data-what-head="${key}"]`);
|
|
41
111
|
if (existing) {
|
package/src/hooks.js
CHANGED
|
@@ -5,6 +5,22 @@
|
|
|
5
5
|
|
|
6
6
|
import { signal, computed, effect, batch, untrack, createRoot, __DEV__ } from './reactive.js';
|
|
7
7
|
import { getCurrentComponent } from './dom.js';
|
|
8
|
+
import { getServerContext } from './server-context.js';
|
|
9
|
+
import { getLoaderData as _getLoaderData, getResource as _getResource } from './hydration-data.js';
|
|
10
|
+
|
|
11
|
+
// --- useLoaderData ---
|
|
12
|
+
// Returns the current page's server loader data. Works during SSR (reads the
|
|
13
|
+
// active render context) and during/after hydration on the client (reads the
|
|
14
|
+
// consolidated #__what_data payload). Intentionally NOT a component-scoped hook
|
|
15
|
+
// (no hook slot) so it is safe to call anywhere — components, effects, helpers.
|
|
16
|
+
export function useLoaderData() {
|
|
17
|
+
if (typeof document === 'undefined') {
|
|
18
|
+
const ctx = getServerContext();
|
|
19
|
+
return ctx ? ctx.loaderData : undefined;
|
|
20
|
+
}
|
|
21
|
+
// Client: read from the consolidated hydration payload (cached, single parse).
|
|
22
|
+
return _getLoaderData();
|
|
23
|
+
}
|
|
8
24
|
|
|
9
25
|
function getCtx(hookName) {
|
|
10
26
|
const ctx = getCurrentComponent();
|
|
@@ -315,8 +331,53 @@ export function onCleanup(fn) {
|
|
|
315
331
|
// Returns [data, { loading, error, refetch, mutate }]
|
|
316
332
|
|
|
317
333
|
export function createResource(fetcher, options = {}) {
|
|
318
|
-
|
|
319
|
-
|
|
334
|
+
// --- Server branch: run the fetcher, cache by key on the render context, and
|
|
335
|
+
// suspend (throw the promise) so the nearest Suspense shows its fallback until
|
|
336
|
+
// the data resolves. On re-render the cached value is returned synchronously.
|
|
337
|
+
if (typeof document === 'undefined') {
|
|
338
|
+
const ctx = getServerContext();
|
|
339
|
+
if (ctx) {
|
|
340
|
+
const key = options.key != null ? options.key : `__r${ctx.resourceCounter++}`;
|
|
341
|
+
const cached = ctx.resources.get(key);
|
|
342
|
+
if (cached && cached.status === 'ready') {
|
|
343
|
+
const accessor = () => cached.value;
|
|
344
|
+
return [accessor, { loading: () => false, error: () => null, refetch: () => {}, mutate: () => {} }];
|
|
345
|
+
}
|
|
346
|
+
if (cached && cached.status === 'error') {
|
|
347
|
+
const accessor = () => undefined;
|
|
348
|
+
return [accessor, { loading: () => false, error: () => cached.error, refetch: () => {}, mutate: () => {} }];
|
|
349
|
+
}
|
|
350
|
+
if (!cached) {
|
|
351
|
+
const promise = Promise.resolve()
|
|
352
|
+
.then(() => fetcher(options.source ?? true, {}))
|
|
353
|
+
.then((v) => { ctx.resources.set(key, { status: 'ready', value: v }); })
|
|
354
|
+
.catch((e) => { ctx.resources.set(key, { status: 'error', error: e }); });
|
|
355
|
+
ctx.resources.set(key, { status: 'pending', promise });
|
|
356
|
+
throw promise; // caught by the nearest Suspense boundary
|
|
357
|
+
}
|
|
358
|
+
// pending (shouldn't normally re-reach here within one pass)
|
|
359
|
+
throw cached.promise;
|
|
360
|
+
}
|
|
361
|
+
// No active render context (bare synchronous renderToString): suspend so the
|
|
362
|
+
// nearest Suspense boundary shows its fallback. There's no cache to resolve
|
|
363
|
+
// into here — use renderToStringAsync / renderToStream for resolved data.
|
|
364
|
+
if (options.initialValue != null) {
|
|
365
|
+
const accessor = () => options.initialValue;
|
|
366
|
+
return [accessor, { loading: () => false, error: () => null, refetch: () => {}, mutate: () => {} }];
|
|
367
|
+
}
|
|
368
|
+
throw Promise.resolve().then(() => fetcher(options.source ?? true, {}));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// --- Client branch: seed from the hydration payload so SSR'd resources don't
|
|
372
|
+
// refetch on hydrate.
|
|
373
|
+
let seeded = options.initialValue;
|
|
374
|
+
if (seeded == null && options.key != null) {
|
|
375
|
+
const fromPayload = _getResource(options.key);
|
|
376
|
+
if (fromPayload !== undefined) seeded = fromPayload;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const data = signal(seeded ?? null);
|
|
380
|
+
const loading = signal(seeded == null);
|
|
320
381
|
const error = signal(null);
|
|
321
382
|
|
|
322
383
|
let controller = null;
|
|
@@ -363,8 +424,8 @@ export function createResource(fetcher, options = {}) {
|
|
|
363
424
|
});
|
|
364
425
|
}
|
|
365
426
|
|
|
366
|
-
// Initial fetch if no initial
|
|
367
|
-
if (
|
|
427
|
+
// Initial fetch only if we have no value (initial or hydrated from payload)
|
|
428
|
+
if (seeded == null) {
|
|
368
429
|
refetch(options.source);
|
|
369
430
|
}
|
|
370
431
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Client hydration payload reader. The server emits a single
|
|
2
|
+
// <script id="__what_data" type="application/json">{loaderData,resources,islandStores}</script>
|
|
3
|
+
// (via serializeState — XSS-safe, valid JSON). This module is the single source
|
|
4
|
+
// of truth the client uses for useLoaderData() and createResource() seeding.
|
|
5
|
+
|
|
6
|
+
let _cache;
|
|
7
|
+
|
|
8
|
+
export function __readHydrationData() {
|
|
9
|
+
if (_cache !== undefined) return _cache;
|
|
10
|
+
if (typeof document === 'undefined') return (_cache = null);
|
|
11
|
+
const el = document.getElementById('__what_data');
|
|
12
|
+
if (!el) return (_cache = null);
|
|
13
|
+
try {
|
|
14
|
+
_cache = JSON.parse(el.textContent);
|
|
15
|
+
} catch {
|
|
16
|
+
_cache = null;
|
|
17
|
+
}
|
|
18
|
+
return _cache;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Test/HMR hook: drop the cached payload so the next read re-parses. */
|
|
22
|
+
export function __resetHydrationData() {
|
|
23
|
+
_cache = undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getLoaderData() {
|
|
27
|
+
const data = __readHydrationData();
|
|
28
|
+
return data ? data.loaderData : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getResource(key) {
|
|
32
|
+
const data = __readHydrationData();
|
|
33
|
+
return data && data.resources ? data.resources[key] : undefined;
|
|
34
|
+
}
|
package/src/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// The closest framework to vanilla JS.
|
|
3
3
|
|
|
4
4
|
// Reactive primitives
|
|
5
|
-
export { signal, computed, effect, memo as signalMemo, batch, untrack, flushSync, createRoot, getOwner, runWithOwner, onCleanup as onRootCleanup, __setDevToolsHooks } from './reactive.js';
|
|
5
|
+
export { signal, computed, effect, memo as signalMemo, batch, untrack, flushSync, createRoot, getOwner, runWithOwner, onCleanup as onRootCleanup, __setDevToolsHooks, __drainPreinstallBuffer } from './reactive.js';
|
|
6
6
|
|
|
7
7
|
// Fine-grained rendering primitives
|
|
8
8
|
export { template, _template, _$template, svgTemplate, insert, mapArray, spread, setProp, delegateEvents, on, classList, hydrate, isHydrating, _$createComponent } from './render.js';
|
|
@@ -30,6 +30,7 @@ export {
|
|
|
30
30
|
onMount,
|
|
31
31
|
onCleanup,
|
|
32
32
|
createResource,
|
|
33
|
+
useLoaderData,
|
|
33
34
|
} from './hooks.js';
|
|
34
35
|
|
|
35
36
|
// Component helpers
|
|
@@ -39,7 +40,13 @@ export { memo, lazy, Suspense, ErrorBoundary, Show, For, Switch, Match, Island }
|
|
|
39
40
|
export { createStore, derived, storeComputed, atom } from './store.js';
|
|
40
41
|
|
|
41
42
|
// Head management
|
|
42
|
-
export { Head, clearHead } from './head.js';
|
|
43
|
+
export { Head, clearHead, beginHeadCollection, endHeadCollection } from './head.js';
|
|
44
|
+
|
|
45
|
+
// SSR render-scoped context (keystone for head collection, loaders, resources)
|
|
46
|
+
export { getServerContext, setServerContext, runWithServerContext } from './server-context.js';
|
|
47
|
+
|
|
48
|
+
// Client hydration payload reader (loader data + resources)
|
|
49
|
+
export { __readHydrationData, __resetHydrationData, getLoaderData, getResource } from './hydration-data.js';
|
|
43
50
|
|
|
44
51
|
// Utilities
|
|
45
52
|
export {
|
package/src/reactive.js
CHANGED
|
@@ -571,7 +571,21 @@ function flush() {
|
|
|
571
571
|
e._pending = false;
|
|
572
572
|
if (!e.disposed && !e._onNotify) {
|
|
573
573
|
const prevDepsLen = e.deps.length;
|
|
574
|
-
|
|
574
|
+
// Isolate per-effect errors: one throwing effect must NOT abort the
|
|
575
|
+
// rest of the batch (which would drop queued effects and leave the
|
|
576
|
+
// graph half-updated). NEEDS_UPSTREAM is the iterative-eval sentinel
|
|
577
|
+
// and must still propagate. (AUDIT-2026-06-06 H8)
|
|
578
|
+
try {
|
|
579
|
+
_runEffect(e);
|
|
580
|
+
} catch (err) {
|
|
581
|
+
if (err === NEEDS_UPSTREAM) throw err;
|
|
582
|
+
if (__DEV__ && __devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });
|
|
583
|
+
// Surface in production too — an uncaught reactive-update error is a
|
|
584
|
+
// real bug; staying silent (as the old throw-out-of-flush did once it
|
|
585
|
+
// escaped) hides it. console.error never aborts the batch.
|
|
586
|
+
try { console.error('[what] Uncaught error in effect during update:', err); } catch { /* no console */ }
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
575
589
|
// Update level only if deps changed (graph structure change)
|
|
576
590
|
if (!e._computed && e.deps.length !== prevDepsLen) {
|
|
577
591
|
_updateLevel(e);
|
|
@@ -843,3 +857,66 @@ export function onCleanup(fn) {
|
|
|
843
857
|
currentRoot.disposals.push(fn);
|
|
844
858
|
}
|
|
845
859
|
}
|
|
860
|
+
|
|
861
|
+
// devtools: registry-iterator export for P1-9 —
|
|
862
|
+
// Late-install replay buffer. When installDevTools() runs AFTER signals or
|
|
863
|
+
// effects have already been created (the canonical example: a module-scope
|
|
864
|
+
// `export const todos = signal([], 'todos')` in store.js, imported before
|
|
865
|
+
// the devtools entry point), those signals were invisible to what_signals.
|
|
866
|
+
//
|
|
867
|
+
// We install a placeholder __devtools that ONLY buffers creations into weak
|
|
868
|
+
// refs. When the real devtools install via __setDevToolsHooks, they call
|
|
869
|
+
// __drainPreinstallBuffer() to register every buffered primitive.
|
|
870
|
+
//
|
|
871
|
+
// Cost in production: __DEV__ is false → __devtools stays null, no buffering.
|
|
872
|
+
// Cost in dev: one WeakRef per signal/effect created before install.
|
|
873
|
+
if (__DEV__ && typeof WeakRef !== 'undefined') {
|
|
874
|
+
// Cap each buffer so an app that never installs devtools doesn't accumulate
|
|
875
|
+
// refs unbounded. 2k is far more than any realistic component/signal count
|
|
876
|
+
// needed before the devtools entry point runs; once devtools install, the
|
|
877
|
+
// buffer is drained and subsequent creations flow through the real hooks.
|
|
878
|
+
const PREINSTALL_CAP = 2000;
|
|
879
|
+
const buffer = { signals: new Set(), effects: new Set(), components: [] };
|
|
880
|
+
__devtools = {
|
|
881
|
+
__isPreinstallBuffer: true,
|
|
882
|
+
onSignalCreate(sig) {
|
|
883
|
+
if (buffer.signals.size < PREINSTALL_CAP) buffer.signals.add(new WeakRef(sig));
|
|
884
|
+
},
|
|
885
|
+
onSignalUpdate() {},
|
|
886
|
+
onEffectCreate(e) {
|
|
887
|
+
if (buffer.effects.size < PREINSTALL_CAP) buffer.effects.add(new WeakRef(e));
|
|
888
|
+
},
|
|
889
|
+
onEffectDispose() {},
|
|
890
|
+
onEffectRun() {},
|
|
891
|
+
onError() {},
|
|
892
|
+
onComponentMount(ctx) {
|
|
893
|
+
if (buffer.components.length < PREINSTALL_CAP) buffer.components.push(ctx);
|
|
894
|
+
},
|
|
895
|
+
onComponentUnmount() {},
|
|
896
|
+
__buffer: buffer,
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Drain the pre-install buffer. Called by the real devtools hooks after
|
|
902
|
+
* __setDevToolsHooks replaces the placeholder. Returns arrays of live refs.
|
|
903
|
+
*/
|
|
904
|
+
export function __drainPreinstallBuffer() {
|
|
905
|
+
if (!__DEV__) return { signals: [], effects: [], components: [] };
|
|
906
|
+
// If the current __devtools is the real one (no __isPreinstallBuffer), the
|
|
907
|
+
// caller installed late and there is nothing to drain from this side.
|
|
908
|
+
const out = { signals: [], effects: [], components: [] };
|
|
909
|
+
const buf = (typeof __preinstallSnapshot !== 'undefined') ? __preinstallSnapshot : null;
|
|
910
|
+
if (!buf) return out;
|
|
911
|
+
for (const ref of buf.signals) { const v = ref.deref?.(); if (v) out.signals.push(v); }
|
|
912
|
+
for (const ref of buf.effects) { const v = ref.deref?.(); if (v) out.effects.push(v); }
|
|
913
|
+
for (const ctx of buf.components) out.components.push(ctx);
|
|
914
|
+
return out;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Capture the placeholder buffer at module load so __drainPreinstallBuffer
|
|
918
|
+
// can return it AFTER __setDevToolsHooks has replaced __devtools.
|
|
919
|
+
let __preinstallSnapshot = null;
|
|
920
|
+
if (__DEV__ && __devtools?.__isPreinstallBuffer) {
|
|
921
|
+
__preinstallSnapshot = __devtools.__buffer;
|
|
922
|
+
}
|