what-core 0.11.7 → 0.11.8

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/index.d.ts CHANGED
@@ -24,7 +24,7 @@ export interface Computed<T> {
24
24
  _signal: true;
25
25
  }
26
26
 
27
- export function signal<T>(initial: T): Signal<T>;
27
+ export function signal<T>(initial: T, debugName?: string): Signal<T>;
28
28
  export function computed<T>(fn: () => T): Computed<T>;
29
29
  export function effect(fn: () => void | (() => void), opts?: { stable?: boolean }): () => void;
30
30
  export function signalMemo<T>(fn: () => T): Computed<T>;
@@ -33,12 +33,36 @@ export function untrack<T>(fn: () => T): T;
33
33
  export function flushSync(): void;
34
34
  export function createRoot<T>(fn: (dispose: () => void) => T): T;
35
35
 
36
+ /** Opaque ownership scope handle. Pair with runWithOwner() for async work. */
37
+ export interface Owner {
38
+ disposals: Array<() => void>;
39
+ }
40
+
41
+ export function getOwner(): Owner | null;
42
+ export function runWithOwner<T>(owner: Owner | null, fn: () => T): T;
43
+ /** Register a cleanup with the current owner/root (root-level onCleanup). */
44
+ export function onRootCleanup(fn: () => void): void;
45
+
36
46
  // --- Virtual DOM ---
37
47
 
38
48
  export type PrimitiveChild = string | number | boolean | null | undefined;
39
49
  export type VNodeChild = PrimitiveChild | VNode | (() => VNodeChild) | VNodeChild[];
40
50
 
41
- export type Component<P = {}> = (props: P & { children?: VNodeChild }) => VNodeChild;
51
+ /** A component may legitimately render nothing, so `null` is part of the contract. */
52
+ export type Component<P = {}> = ((props: P & { children?: VNodeChild }) => VNode | null) & {
53
+ /**
54
+ * Opt out of realizing compiled children before the component runs.
55
+ *
56
+ * A component that establishes a scope its children depend on (a context
57
+ * provider, an error or suspense boundary) receives `props.children` as a
58
+ * zero-argument factory instead of built nodes, and the runtime realizes it
59
+ * once that scope exists. `ErrorBoundary`, `Suspense` and `Context.Provider`
60
+ * set this themselves, and the compiler keeps a component that only forwards
61
+ * its children lazy, so this is only needed by a component that both
62
+ * inspects its children and forwards them into a boundary or a provider.
63
+ */
64
+ _deferChildren?: boolean;
65
+ };
42
66
 
43
67
  export interface VNode<P = Record<string, any>> {
44
68
  tag: string | Component<P>;
@@ -54,15 +78,20 @@ export function h<P extends Record<string, any>>(
54
78
  ...children: VNodeChild[]
55
79
  ): VNode<P>;
56
80
 
57
- export function Fragment(props: { children?: VNodeChild }): VNodeChild;
81
+ export function Fragment(props: { children?: VNodeChild }): VNode;
58
82
  export function html(strings: TemplateStringsArray, ...values: any[]): VNode | VNode[];
59
83
 
60
84
  // --- DOM ---
61
85
 
62
86
  export function mount(vnode: VNodeChild, container: string | Element): () => void;
63
87
 
88
+ /** Attach reactive bindings to server-rendered DOM instead of creating it. */
89
+ export function hydrate(vnode: VNodeChild, container: Element): Node | null;
90
+ export function isHydrating(): boolean;
91
+
64
92
  // Fine-grained rendering primitives
65
93
  export function template(html: string): () => Element;
94
+ export function svgTemplate(html: string): () => Element;
66
95
  export function insert(parent: Node, child: any, marker?: Node | null): any;
67
96
  export function mapArray<T>(
68
97
  source: () => T[],
@@ -132,18 +161,18 @@ export function Show(props: {
132
161
  when: boolean | (() => boolean);
133
162
  fallback?: VNodeChild;
134
163
  children?: VNodeChild;
135
- }): VNodeChild;
164
+ }): VNode;
136
165
 
137
166
  export function For<T>(props: {
138
167
  each: T[] | (() => T[]);
139
168
  fallback?: VNodeChild;
140
169
  children: ((item: T, index: number) => VNodeChild) | VNodeChild;
141
- }): VNodeChild;
170
+ }): VNode;
142
171
 
143
172
  export function Switch(props: {
144
173
  fallback?: VNodeChild;
145
174
  children?: VNodeChild;
146
- }): VNodeChild;
175
+ }): VNode;
147
176
 
148
177
  export function Match(props: {
149
178
  when: boolean | (() => boolean);
@@ -198,6 +227,37 @@ export function Head(props: {
198
227
  }): null;
199
228
  export function clearHead(): void;
200
229
 
230
+ /** Per-render head accumulator used by the SSR renderer. */
231
+ export interface HeadSink {
232
+ title: string | null;
233
+ metas: Map<string, Record<string, string>>;
234
+ links: Map<string, Record<string, string>>;
235
+ }
236
+
237
+ export function beginHeadCollection(): HeadSink;
238
+ export function endHeadCollection(sink: HeadSink | null): string;
239
+
240
+ // --- Loader Data ---
241
+
242
+ export function useLoaderData<T = any>(): T | undefined;
243
+ export function getLoaderData<T = any>(): T | undefined;
244
+ export function getResource<T = any>(key: string): T | undefined;
245
+
246
+ // --- Server Context ---
247
+
248
+ export interface ServerContext {
249
+ loaderData?: any;
250
+ head?: HeadSink;
251
+ resources?: Record<string, any>;
252
+ [key: string]: any;
253
+ }
254
+
255
+ /** The active render context, or null on the client / outside a render. */
256
+ export function getServerContext(): ServerContext | null;
257
+ /** Set the active context and return the previous one so callers can restore it. */
258
+ export function setServerContext(ctx: ServerContext | null): ServerContext | null;
259
+ export function runWithServerContext<T>(ctx: ServerContext, fn: () => T): T;
260
+
201
261
  // --- Scheduler ---
202
262
 
203
263
  export function scheduleRead(fn: () => void): () => void;
@@ -334,17 +394,17 @@ export function onKeys(keys: string[], handler: (e: KeyboardEvent) => void): (e:
334
394
 
335
395
  // --- Skeleton ---
336
396
 
337
- export function Skeleton(props?: Record<string, any>): VNodeChild;
397
+ export function Skeleton(props?: Record<string, any>): VNode;
338
398
  export function SkeletonText(props?: Record<string, any>): VNode;
339
- export function SkeletonAvatar(props?: Record<string, any>): VNodeChild;
399
+ export function SkeletonAvatar(props?: Record<string, any>): VNode;
340
400
  export function SkeletonCard(props?: Record<string, any>): VNode;
341
401
  export function SkeletonTable(props?: Record<string, any>): VNode;
342
- export function IslandSkeleton(props?: Record<string, any>): VNodeChild;
402
+ export function IslandSkeleton(props?: Record<string, any>): VNode;
343
403
  export function useSkeleton<T>(asyncFn: () => Promise<T> | T, deps?: unknown[]): {
344
404
  isLoading: () => boolean;
345
405
  data: () => T | null;
346
406
  error: () => any;
347
- Skeleton: (props?: Record<string, any>) => VNodeChild;
407
+ Skeleton: (props?: Record<string, any>) => VNode;
348
408
  };
349
409
  export function Placeholder(props?: Record<string, any>): VNode;
350
410
  export function LoadingDots(props?: Record<string, any>): VNode;
@@ -504,4 +564,85 @@ export function ErrorMessage(props: {
504
564
  formState?: FormState;
505
565
  errors?: Record<string, FieldError> | (() => Record<string, FieldError>);
506
566
  render?: (args: { message?: string; type?: string }) => VNodeChild;
507
- }): VNodeChild;
567
+ }): VNode;
568
+
569
+ // --- Structured Errors ---
570
+
571
+ export interface ErrorCodeDefinition {
572
+ code: string;
573
+ severity: 'error' | 'warning';
574
+ template: string;
575
+ suggestion: string;
576
+ codeExample?: string;
577
+ }
578
+
579
+ export const ERROR_CODES: Record<string, ErrorCodeDefinition>;
580
+
581
+ export interface WhatErrorJSON {
582
+ code: string;
583
+ message: string;
584
+ suggestion?: string;
585
+ file?: string;
586
+ line?: number;
587
+ component?: string;
588
+ signal?: string;
589
+ effect?: string;
590
+ }
591
+
592
+ export class WhatError extends Error {
593
+ constructor(init: {
594
+ code: string;
595
+ message: string;
596
+ suggestion?: string;
597
+ file?: string;
598
+ line?: number;
599
+ component?: string;
600
+ signal?: string;
601
+ effect?: string;
602
+ });
603
+ code: string;
604
+ suggestion?: string;
605
+ file?: string;
606
+ line?: number;
607
+ component?: string;
608
+ signal?: string;
609
+ effect?: string;
610
+ toJSON(): WhatErrorJSON;
611
+ }
612
+
613
+ export function createWhatError(
614
+ errorCode: string | ErrorCodeDefinition,
615
+ context?: Record<string, any>,
616
+ ): WhatError;
617
+ export function classifyError(err: unknown, context?: Record<string, any>): WhatError;
618
+ export function collectError(error: WhatError): void;
619
+ export function getCollectedErrors(since?: number): Array<WhatErrorJSON & { timestamp: number }>;
620
+ export function clearCollectedErrors(): void;
621
+
622
+ // --- Guardrails ---
623
+
624
+ export interface GuardrailConfig {
625
+ signalReadDetection: boolean;
626
+ componentNaming: boolean;
627
+ importValidation: boolean;
628
+ }
629
+
630
+ export function configureGuardrails(overrides: Partial<GuardrailConfig>): void;
631
+ export function getGuardrailConfig(): GuardrailConfig;
632
+ export function installSignalReadGuardrail<T>(signalFn: T, debugName?: string): T;
633
+
634
+ // --- Agent Context ---
635
+
636
+ export interface HealthReport {
637
+ effectCycleRisk: boolean;
638
+ orphanEffects: number;
639
+ signalLeaks: number;
640
+ memoryPressure: 'low' | 'medium' | 'high';
641
+ recentErrorCount: number;
642
+ totalSignals: number;
643
+ totalComponents: number;
644
+ }
645
+
646
+ export function getHealth(): HealthReport;
647
+ /** Expose globalThis.__WHAT_AGENT__ for agent tooling (dev mode only). */
648
+ export function installAgentContext(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-core",
3
- "version": "0.11.7",
3
+ "version": "0.11.8",
4
4
  "description": "What Framework - Signal-based UI framework built for AI agents",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -8,7 +8,7 @@ import { getCollectedErrors } from './errors.js';
8
8
  // --- Version ---
9
9
  // Keep in sync with packages/core/package.json (checked by
10
10
  // core/test/guardrails.test.js so it can't silently go stale again).
11
- const VERSION = '0.11.7';
11
+ const VERSION = '0.11.8';
12
12
 
13
13
  // --- Component Registry ---
14
14
  // Tracks mounted components for agent inspection.
package/src/components.js CHANGED
@@ -81,19 +81,31 @@ export function lazy(loader) {
81
81
  export function Suspense({ fallback, children }) {
82
82
  const loading = signal(false);
83
83
  const pendingPromises = new Set();
84
+ let failed = false;
84
85
 
85
86
  // Suspense boundary marker
86
87
  const boundary = {
87
88
  _suspense: true,
88
89
  onSuspend(promise) {
90
+ if (failed) return;
89
91
  loading.set(true);
90
92
  pendingPromises.add(promise);
91
- promise.finally(() => {
92
- pendingPromises.delete(promise);
93
- if (pendingPromises.size === 0) {
94
- loading.set(false);
95
- }
96
- });
93
+ // Rejection is handled separately from fulfilment. Clearing the fallback
94
+ // on a rejection re-renders the child, which suspends on the same
95
+ // rejected thenable again, forever. Latch the failure and stay put.
96
+ promise.then(
97
+ () => {
98
+ pendingPromises.delete(promise);
99
+ if (pendingPromises.size === 0) {
100
+ loading.set(false);
101
+ }
102
+ },
103
+ (err) => {
104
+ failed = true;
105
+ pendingPromises.delete(promise);
106
+ console.error('[what] Suspense: a suspended child rejected:', err);
107
+ },
108
+ );
97
109
  },
98
110
  };
99
111
 
@@ -105,6 +117,10 @@ export function Suspense({ fallback, children }) {
105
117
  };
106
118
  }
107
119
 
120
+ // The boundary context only exists once createSuspenseBoundary runs, so
121
+ // compiled children must not be built during this call. See createComponent.
122
+ Suspense._deferChildren = true;
123
+
108
124
  // --- ErrorBoundary ---
109
125
  // Catch errors in children and show fallback.
110
126
  // Uses a signal to track error state so it works with reactive rendering.
@@ -135,6 +151,10 @@ export function ErrorBoundary({ fallback, children, onError }) {
135
151
  };
136
152
  }
137
153
 
154
+ // The boundary context only exists once createErrorBoundary runs, so compiled
155
+ // children must not be built during this call. See createComponent.
156
+ ErrorBoundary._deferChildren = true;
157
+
138
158
  // Helper to report error to nearest boundary
139
159
  // Walks the component context tree (not a runtime stack) so async errors are caught
140
160
  export function reportError(error, startCtx) {
@@ -216,33 +236,38 @@ export function For({ each, fallback = null, children }) {
216
236
  // Multi-condition rendering (like switch statement).
217
237
 
218
238
  export function Switch({ fallback = null, children }) {
219
- // The Match children (marker vnodes) are static resolve them once. The
220
- // match loop, which reads each Match's reactive `when`, must run inside a
221
- // reactive thunk (see Show/For above): components run once, so evaluating
222
- // `when()` in the body here would snapshot the active arm a single time and
223
- // `<Switch>`/`<Match when={() => sig()}>` would render once and never update.
224
- // Switch/Match are NOT lowered by the fine-grained compiler, so this runtime
225
- // path is the ONLY path the thunk is what makes them reactive at all.
239
+ // The Match children are static, so resolve them once. The match loop, which
240
+ // reads each Match's reactive `when`, must run inside a reactive thunk (see
241
+ // Show/For above): components run once, so evaluating `when()` in the body
242
+ // here would snapshot the active arm a single time and `<Switch>`/`<Match
243
+ // when={() => sig()}>` would render once and never update.
244
+ // This is the runtime path, for h() and the automatic JSX runtime, where a
245
+ // <Match> arrives as an unexecuted marker vnode. The fine-grained compiler
246
+ // lowers <Switch> to the same conditional thunk and never reaches here; a
247
+ // <Switch> it cannot lower is a build error rather than a call into this.
226
248
  const kids = Array.isArray(children) ? children : [children];
227
249
 
228
250
  return () => {
229
251
  for (const child of kids) {
230
252
  if (child && child.tag === Match) {
231
- const condition = typeof child.props.when === 'function'
232
- ? child.props.when()
233
- : child.props.when;
234
- if (condition) {
235
- return child.children;
236
- }
253
+ const when = child.props.when;
254
+ const condition = typeof when === 'function' ? when() : when;
255
+ if (condition) return child.children;
237
256
  }
238
257
  }
239
258
  return fallback;
240
259
  };
241
260
  }
242
261
 
243
- export function Match({ when, children }) {
244
- // Match is just a marker component, Switch handles the logic
245
- return { tag: Match, props: { when }, children, _vnode: true };
262
+ export function Match(props) {
263
+ // Executed rather than left as a marker, which is what a lone compiled
264
+ // <Match> does. Returning a `{ tag: Match }` vnode here would send createDOM
265
+ // straight back into Match forever, so return a reactive thunk that renders
266
+ // the arm when it matches.
267
+ return () => {
268
+ const condition = typeof props.when === 'function' ? props.when() : props.when;
269
+ return condition ? props.children : null;
270
+ };
246
271
  }
247
272
 
248
273
  // --- Island ---