what-core 0.12.4 → 0.13.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-ENARGHSD.min.js +11 -0
- package/dist/chunk-OKM3GKVP.min.js +1 -0
- package/dist/index.min.js +82 -5
- package/dist/render.min.js +1 -1
- package/dist/testing.min.js +1 -1
- package/index.d.ts +40 -3
- package/package.json +1 -1
- package/render.d.ts +7 -0
- package/src/a11y.js +3 -3
- package/src/agent-context.js +1 -1
- package/src/animation.js +5 -6
- package/src/components.js +1 -1
- package/src/data.js +1 -10
- package/src/dom.js +29 -19
- package/src/errors.js +321 -0
- package/src/form.js +2 -1
- package/src/head.js +2 -1
- package/src/hooks.js +10 -17
- package/src/index.js +1 -0
- package/src/reactive.js +31 -14
- package/src/render.js +11 -14
- package/src/scheduler.js +7 -7
- package/src/skeleton.js +16 -1
- package/src/store.js +0 -1
- package/src/testing.js +101 -50
- package/testing.d.ts +17 -1
- package/dist/chunk-M5GDJRVX.min.js +0 -1
- package/dist/chunk-T2SKNKT5.min.js +0 -11
package/src/errors.js
CHANGED
|
@@ -157,8 +157,294 @@ redirect(ALLOWED.has(query.next) ? query.next : '/');`,
|
|
|
157
157
|
try { html = renderToString(<App />); }
|
|
158
158
|
catch (e) { if (e.name === 'RouterRedirect') return Response.redirect(e.to, 302); throw e; }`,
|
|
159
159
|
},
|
|
160
|
+
|
|
161
|
+
// --- Outside core ---
|
|
162
|
+
//
|
|
163
|
+
// Every package in the workspace threw bare `new Error(...)` with good prose
|
|
164
|
+
// and no code, so nothing downstream could branch on the failure and the
|
|
165
|
+
// what_errors MCP tool could only ever enumerate core's own. The entries
|
|
166
|
+
// below are the catalogue for the rest of the framework.
|
|
167
|
+
//
|
|
168
|
+
// These are catalogued here but NOT constructed here. A throw outside core
|
|
169
|
+
// carries only its `code`; the suggestion and the worked example live once,
|
|
170
|
+
// in this file, and are resolved from the code by classifyError() or by
|
|
171
|
+
// reading ERROR_CODES directly.
|
|
172
|
+
//
|
|
173
|
+
// That split is a size decision, and it was measured. Importing
|
|
174
|
+
// createWhatError() into what-server's client-shipped action surface retains
|
|
175
|
+
// this whole catalogue through the bundler: esbuild took that surface from
|
|
176
|
+
// 8.4 KB minified / 3.5 KB gzipped to 25.9 KB / 9.7 KB. Six kilobytes of
|
|
177
|
+
// gzipped prose in every visitor's browser is the wrong trade for making
|
|
178
|
+
// three server-side messages structured.
|
|
179
|
+
//
|
|
180
|
+
// The same rule keeps the packages that genuinely cannot import core honest:
|
|
181
|
+
// what-isr never imports what-server, which is what keeps it usable from any
|
|
182
|
+
// adapter; the compiler runs inside Babel at build time; the MCP server has
|
|
183
|
+
// no framework dependency; and the CLI loads the project's runtime, not its
|
|
184
|
+
// own. `scripts/check-error-codes.mjs` asserts every `ERR_*` literal thrown
|
|
185
|
+
// anywhere under packages/*/src appears here, so nothing drifts.
|
|
186
|
+
|
|
187
|
+
NO_SECURE_RANDOM: {
|
|
188
|
+
code: 'ERR_NO_SECURE_RANDOM',
|
|
189
|
+
severity: 'error',
|
|
190
|
+
template: '[what] No secure random source available for CSRF token generation.',
|
|
191
|
+
suggestion: 'Neither globalThis.crypto.getRandomValues nor node:crypto was reachable. On Node this means a build older than 18 or a bundler that stripped node:crypto; on an edge runtime it means the Web Crypto global was not provided. A CSRF token from Math.random is not a token, so this refuses rather than degrading.',
|
|
192
|
+
codeExample: `// Node 18+ exposes Web Crypto globally; nothing to configure.
|
|
193
|
+
// If a bundler dropped it, restore the global before creating the server:
|
|
194
|
+
import { webcrypto } from 'node:crypto';
|
|
195
|
+
globalThis.crypto ??= webcrypto;`,
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
ACTION_FAILED: {
|
|
199
|
+
code: 'ERR_ACTION_FAILED',
|
|
200
|
+
severity: 'error',
|
|
201
|
+
template: '{{message}}',
|
|
202
|
+
suggestion: 'The server action rejected. The message is the one the action threw, forwarded to the client by the action handler. Throw a typed error from the action and branch on its shape rather than parsing this string.',
|
|
203
|
+
codeExample: `// In the action, fail with something the client can read:
|
|
204
|
+
export const save = action(async (data) => {
|
|
205
|
+
if (!data.email) throw Object.assign(new Error('Email required'), { field: 'email' });
|
|
206
|
+
});`,
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
STATIC_WRITE_ESCAPE: {
|
|
210
|
+
code: 'ERR_STATIC_WRITE_ESCAPE',
|
|
211
|
+
severity: 'error',
|
|
212
|
+
template: '[what-server] Refusing to write outside outDir: {{path}}.',
|
|
213
|
+
suggestion: 'A route path resolved to a location outside the export directory, which a "../" segment in a route or a param can do. Sanitize the route path, or drop the route from the static export.',
|
|
214
|
+
codeExample: `// Bad — a param that can contain a slash escapes outDir:
|
|
215
|
+
{ path: '/docs/:slug*', mode: 'static' }
|
|
216
|
+
|
|
217
|
+
// Good — constrain the param, or precompute the exact paths:
|
|
218
|
+
{ path: '/docs/:slug', mode: 'static', paths: () => slugs.map(slug => ({ slug })) }`,
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
INVALID_SSR_TAG: {
|
|
222
|
+
code: 'ERR_INVALID_SSR_TAG',
|
|
223
|
+
severity: 'error',
|
|
224
|
+
template: '[what-server] Invalid tag name in SSR: {{tag}}.',
|
|
225
|
+
suggestion: 'renderToString reached a vnode whose tag is neither a string nor a component function. This is almost always a component that returned a raw object, or a value interpolated where an element was expected.',
|
|
226
|
+
codeExample: `// Bad — returns a plain object, not a vnode:
|
|
227
|
+
function Row() { return { name: 'a' }; }
|
|
228
|
+
|
|
229
|
+
// Good — return elements, and interpolate values as children:
|
|
230
|
+
function Row({ name }) { return <li>{name}</li>; }`,
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
FORM_ACTION_NOT_REGISTERED: {
|
|
234
|
+
code: 'ERR_FORM_ACTION_NOT_REGISTERED',
|
|
235
|
+
severity: 'error',
|
|
236
|
+
template: '[what] <Form action={fn}>: that function is not a server action.',
|
|
237
|
+
suggestion: 'Wrap it with action() from what-server, or pass the action id as a string. A plain function has no id, so there is nothing for the form post to address.',
|
|
238
|
+
codeExample: `// Bad — a plain function:
|
|
239
|
+
async function save(data) {}
|
|
240
|
+
<Form action={save} />
|
|
241
|
+
|
|
242
|
+
// Good — a registered action:
|
|
243
|
+
export const save = action(async (data) => {});
|
|
244
|
+
<Form action={save} />`,
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
FORM_ACTION_MISSING: {
|
|
248
|
+
code: 'ERR_FORM_ACTION_MISSING',
|
|
249
|
+
severity: 'error',
|
|
250
|
+
template: '[what] <Form> requires an `action` prop: a server action or its id.',
|
|
251
|
+
suggestion: 'Pass the action itself, or the string id it was registered under.',
|
|
252
|
+
codeExample: `// Bad:
|
|
253
|
+
<Form method="post" />
|
|
254
|
+
|
|
255
|
+
// Good:
|
|
256
|
+
<Form action={save} />
|
|
257
|
+
<Form action="save-user" />`,
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
ISLAND_STORE_OUTSIDE_RENDER: {
|
|
261
|
+
code: 'ERR_ISLAND_STORE_OUTSIDE_RENDER',
|
|
262
|
+
severity: 'error',
|
|
263
|
+
template: '[what-server] Island store "{{name}}" was accessed outside an active server render.',
|
|
264
|
+
suggestion: 'A module-scoped island store resolves against the current request, so it can only be read or written from a component rendered by renderDocument/renderPage. Reading one at module scope, or from a background task, has no request to bind to.',
|
|
265
|
+
codeExample: `// Bad — runs at import time, with no request in scope:
|
|
266
|
+
const count = cart.items.length;
|
|
267
|
+
|
|
268
|
+
// Good — read it inside a component the server is rendering:
|
|
269
|
+
function Cart() { return <span>{cart.items.length}</span>; }`,
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
ISR_MISSING_CLIENT: {
|
|
273
|
+
code: 'ERR_ISR_MISSING_CLIENT',
|
|
274
|
+
severity: 'error',
|
|
275
|
+
template: '[what-isr] createRedisStore requires { client }.',
|
|
276
|
+
suggestion: 'what-isr ships no Redis driver on purpose, so the client is injected. Pass an ioredis or node-redis instance (get/set/del/sadd/srem/smembers, optionally expire/scan/keys).',
|
|
277
|
+
codeExample: `import Redis from 'ioredis';
|
|
278
|
+
const store = createRedisStore({ client: new Redis(process.env.REDIS_URL) });`,
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
ISR_VARY_UNRESOLVED: {
|
|
282
|
+
code: 'ERR_ISR_VARY_UNRESOLVED',
|
|
283
|
+
severity: 'error',
|
|
284
|
+
template: '[what-isr] cannot build a cache key: `vary` is declared but could not be resolved against the request.',
|
|
285
|
+
suggestion: 'A declared vary is a list of names that must be resolved against real request headers before it can be part of a key. Either pass the request headers alongside the declaration, or pass an already-resolved name -> value object. Guessing would cache one visitor page under another visitor key.',
|
|
286
|
+
codeExample: `// Bad — a declaration with nothing to resolve it against:
|
|
287
|
+
cacheKey({ path, vary: ['cookie:session'] });
|
|
288
|
+
|
|
289
|
+
// Good — supply the headers:
|
|
290
|
+
cacheKey({ path, vary: ['cookie:session'], headers: request.headers });
|
|
291
|
+
|
|
292
|
+
// Good — or resolve it yourself:
|
|
293
|
+
cacheKey({ path, vary: { 'cookie:session': sessionId } });`,
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
ISR_VARY_NO_HEADERS: {
|
|
297
|
+
code: 'ERR_ISR_VARY_NO_HEADERS',
|
|
298
|
+
severity: 'error',
|
|
299
|
+
template: '[what-isr] route declares `vary` but the adapter supplied no request headers; refusing to cache.',
|
|
300
|
+
suggestion: 'The route varies its output per header, and the adapter called the engine without them. Caching anyway would serve one variant to every request. Forward the request headers from the adapter into the engine call.',
|
|
301
|
+
codeExample: `// In the adapter:
|
|
302
|
+
await engine.handle(routeMatch, { headers: request.headers });`,
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
DUPLICATE_ACTION_ID: {
|
|
306
|
+
code: 'ERR_DUPLICATE_ACTION_ID',
|
|
307
|
+
severity: 'error',
|
|
308
|
+
template: 'Duplicate server action ID "{{id}}".',
|
|
309
|
+
suggestion: 'Action ids are the wire address of a server action, so two actions cannot share one. Ids derive from the file path and export name, so this usually means the same action is being registered twice, or an explicit id was reused.',
|
|
310
|
+
codeExample: `// Bad — two actions pinned to the same id:
|
|
311
|
+
export const save = action(fn, { id: 'save' });
|
|
312
|
+
export const store = action(fn, { id: 'save' });
|
|
313
|
+
|
|
314
|
+
// Good — let ids derive, or make them distinct:
|
|
315
|
+
export const save = action(fn);
|
|
316
|
+
export const store = action(fn, { id: 'store-user' });`,
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
PAGE_NO_DEFAULT_EXPORT: {
|
|
320
|
+
code: 'ERR_PAGE_NO_DEFAULT_EXPORT',
|
|
321
|
+
severity: 'error',
|
|
322
|
+
template: 'Page module has no default-exported component.',
|
|
323
|
+
suggestion: 'A page file must default-export the component to render. A named export cannot be found by the file-router.',
|
|
324
|
+
codeExample: `// Bad:
|
|
325
|
+
export function Home() { return <h1>Hi</h1>; }
|
|
326
|
+
|
|
327
|
+
// Good:
|
|
328
|
+
export default function Home() { return <h1>Hi</h1>; }`,
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
HOOK_OUTSIDE_RENDER: {
|
|
332
|
+
code: 'ERR_HOOK_OUTSIDE_RENDER',
|
|
333
|
+
severity: 'error',
|
|
334
|
+
template: '[what-react] {{hookName}}() called outside of a component render.',
|
|
335
|
+
suggestion: 'Hooks can only be called while a what-react component is rendering. When this happens inside a React library, the usual cause is two module instances: make sure every `react` and `react-dom` import is aliased to what-react by the reactCompat() vite plugin.',
|
|
336
|
+
codeExample: `// vite.config.js
|
|
337
|
+
import { reactCompat } from 'what-react/vite';
|
|
338
|
+
export default { plugins: [reactCompat()] };`,
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
CHILDREN_ONLY: {
|
|
342
|
+
code: 'ERR_CHILDREN_ONLY',
|
|
343
|
+
severity: 'error',
|
|
344
|
+
template: 'React.Children.only expected to receive a single React element child.',
|
|
345
|
+
suggestion: 'Children.only asserts exactly one element. Pass one child, or use Children.toArray/Children.map when the count can vary.',
|
|
346
|
+
codeExample: `// Bad:
|
|
347
|
+
<Tooltip><span>a</span><span>b</span></Tooltip>
|
|
348
|
+
|
|
349
|
+
// Good:
|
|
350
|
+
<Tooltip><span>a</span></Tooltip>`,
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
USE_INVALID_ARG: {
|
|
354
|
+
code: 'ERR_USE_INVALID_ARG',
|
|
355
|
+
severity: 'error',
|
|
356
|
+
template: '[what-react] use() expects a promise or a context.',
|
|
357
|
+
suggestion: 'use() reads either a thenable or a context object. Anything else has nothing to suspend on or subscribe to.',
|
|
358
|
+
codeExample: `// Good:
|
|
359
|
+
const value = use(ThemeContext);
|
|
360
|
+
const data = use(fetchUser(id));`,
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
PRETEXT_NOT_INSTALLED: {
|
|
364
|
+
code: 'ERR_PRETEXT_NOT_INSTALLED',
|
|
365
|
+
severity: 'error',
|
|
366
|
+
template: '[what-text] Failed to load @chenglou/pretext: {{message}}.',
|
|
367
|
+
suggestion: 'what-text declares pretext as an optional peer so the package installs without it. Install it to use the text engine: npm install @chenglou/pretext',
|
|
368
|
+
codeExample: `npm install @chenglou/pretext`,
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
DESTRUCTURED_PROPS: {
|
|
372
|
+
code: 'ERR_DESTRUCTURED_PROPS',
|
|
373
|
+
severity: 'warning',
|
|
374
|
+
template: "Destructuring '{{binding}}' in the component body snapshots props and loses reactivity.",
|
|
375
|
+
suggestion: "What components run ONCE, so the body is not re-run when a prop changes. Reading `props.foo` goes through the reactive proxy and tracks; `const { foo } = props` reads the value once and detaches it. Read through the proxy inside JSX and effects, or wrap each field in an accessor.",
|
|
376
|
+
codeExample: `// Bad - snapshots at first run, never updates:
|
|
377
|
+
function Row(props) {
|
|
378
|
+
const { label } = props;
|
|
379
|
+
return <span>{label}</span>;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Good - reads through the proxy each time:
|
|
383
|
+
function Row(props) {
|
|
384
|
+
return <span>{props.label}</span>;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Good - an accessor keeps the destructured name:
|
|
388
|
+
function Row(props) {
|
|
389
|
+
const label = () => props.label;
|
|
390
|
+
return <span>{label()}</span>;
|
|
391
|
+
}`,
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
// --- Fallbacks ---
|
|
395
|
+
// classifyError() returns one of these when a raw Error does not match any
|
|
396
|
+
// known pattern. They are catalogued so what_errors never reports a code an
|
|
397
|
+
// agent cannot look up.
|
|
398
|
+
|
|
399
|
+
RUNTIME: {
|
|
400
|
+
code: 'ERR_RUNTIME',
|
|
401
|
+
severity: 'error',
|
|
402
|
+
template: '{{message}}',
|
|
403
|
+
suggestion: 'An error surfaced that the framework could not classify, so the message is the original one verbatim. The stack, file and line on the error narrow it; if the same shape shows up repeatedly it is worth its own code here.',
|
|
404
|
+
codeExample: `// Inspect the structured form rather than the string:
|
|
405
|
+
try { render(); } catch (e) { console.log(classifyError(e).toJSON()); }`,
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
UNKNOWN: {
|
|
409
|
+
code: 'ERR_UNKNOWN',
|
|
410
|
+
severity: 'error',
|
|
411
|
+
template: 'Unknown error: {{errorCode}}.',
|
|
412
|
+
suggestion: 'createWhatError() was called with a code that is not in this catalogue. Check the spelling against ERROR_CODES, or add the entry.',
|
|
413
|
+
codeExample: `// Bad - not a catalogue key:
|
|
414
|
+
createWhatError('MISSING_KEYS');
|
|
415
|
+
|
|
416
|
+
// Good:
|
|
417
|
+
createWhatError('MISSING_KEY', { component: 'TodoList' });`,
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
UNKNOWN_TOOL: {
|
|
421
|
+
code: 'ERR_UNKNOWN_TOOL',
|
|
422
|
+
severity: 'error',
|
|
423
|
+
template: 'Unknown tool: {{name}}.',
|
|
424
|
+
suggestion: 'The MCP client called a tool this server does not expose. Call tools/list to enumerate what is available; a stale client cache is the usual cause.',
|
|
425
|
+
codeExample: `// List what the server actually exposes:
|
|
426
|
+
{ "method": "tools/list" }`,
|
|
427
|
+
},
|
|
160
428
|
};
|
|
161
429
|
|
|
430
|
+
// Reverse index: an error carries `code: 'ERR_X'`, and the catalogue is keyed
|
|
431
|
+
// by its short name.
|
|
432
|
+
//
|
|
433
|
+
// Built on first use, NOT at module load. A top-level `new Map(...)` over
|
|
434
|
+
// ERROR_CODES is a side effect that references the catalogue, which pins it
|
|
435
|
+
// into every bundle that imports what-core: it took the counter app from
|
|
436
|
+
// 6.4 KB gzipped to 12.1 KB and tripped check:size. Inside a function, a
|
|
437
|
+
// bundler that drops getErrorDefinition drops the catalogue with it.
|
|
438
|
+
let _codeIndex = null;
|
|
439
|
+
|
|
440
|
+
/** Look up a catalogue entry by its `ERR_*` code. Returns undefined if unknown. */
|
|
441
|
+
export function getErrorDefinition(code) {
|
|
442
|
+
if (_codeIndex === null) {
|
|
443
|
+
_codeIndex = new Map(Object.values(ERROR_CODES).map((def) => [def.code, def]));
|
|
444
|
+
}
|
|
445
|
+
return _codeIndex.get(code);
|
|
446
|
+
}
|
|
447
|
+
|
|
162
448
|
// --- WhatError ---
|
|
163
449
|
// Structured error class with full context for agent consumption.
|
|
164
450
|
|
|
@@ -169,6 +455,18 @@ export class WhatError extends Error {
|
|
|
169
455
|
// there and `suggestion` had to carry the whole fix in prose. It matters more
|
|
170
456
|
// here than in a framework aimed at humans: the audience reading toJSON() is
|
|
171
457
|
// usually an agent, and a diff-shaped example is the part it can copy.
|
|
458
|
+
/**
|
|
459
|
+
* @param {object} init
|
|
460
|
+
* @param {string} init.code
|
|
461
|
+
* @param {string} [init.message]
|
|
462
|
+
* @param {string} [init.suggestion]
|
|
463
|
+
* @param {{ bad?: string, good?: string } | string} [init.codeExample]
|
|
464
|
+
* @param {string} [init.file]
|
|
465
|
+
* @param {number} [init.line]
|
|
466
|
+
* @param {string} [init.component]
|
|
467
|
+
* @param {string} [init.signal]
|
|
468
|
+
* @param {string} [init.effect]
|
|
469
|
+
*/
|
|
172
470
|
constructor({ code, message, suggestion, codeExample, file, line, component, signal, effect }) {
|
|
173
471
|
super(message);
|
|
174
472
|
this.name = 'WhatError';
|
|
@@ -265,6 +563,29 @@ export function clearCollectedErrors() {
|
|
|
265
563
|
export function classifyError(err, context = {}) {
|
|
266
564
|
const msg = err?.message || String(err);
|
|
267
565
|
|
|
566
|
+
// An error the framework threw already says what it is. Every throw outside
|
|
567
|
+
// core carries a `code` and nothing else — the suggestion and the worked
|
|
568
|
+
// example live once, in ERROR_CODES — so resolving the code here is what
|
|
569
|
+
// makes them reachable at all. Message sniffing below is only for errors
|
|
570
|
+
// that predate the code, or that come from user code.
|
|
571
|
+
if (err && typeof err.code === 'string') {
|
|
572
|
+
const def = getErrorDefinition(err.code);
|
|
573
|
+
if (def) {
|
|
574
|
+
return new WhatError({
|
|
575
|
+
code: def.code,
|
|
576
|
+
// The thrown message is the specific one; the template is generic.
|
|
577
|
+
message: msg,
|
|
578
|
+
suggestion: def.suggestion,
|
|
579
|
+
codeExample: def.codeExample,
|
|
580
|
+
file: context.file,
|
|
581
|
+
line: context.line,
|
|
582
|
+
component: context.component,
|
|
583
|
+
signal: context.signal || context.signalName,
|
|
584
|
+
effect: context.effect || context.effectName,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
268
589
|
// Infinite effect loop
|
|
269
590
|
if (msg.includes('infinite effect loop') || msg.includes('25 iterations')) {
|
|
270
591
|
return createWhatError('INFINITE_EFFECT', context);
|
package/src/form.js
CHANGED
|
@@ -21,6 +21,7 @@ function _applyRef(ref, el) {
|
|
|
21
21
|
function _composeHandlers(registered, callerHandler) {
|
|
22
22
|
if (typeof registered !== 'function') return callerHandler;
|
|
23
23
|
if (typeof callerHandler !== 'function') return registered;
|
|
24
|
+
/** @this {any} */
|
|
24
25
|
return function composedHandler(...args) {
|
|
25
26
|
registered.apply(this, args);
|
|
26
27
|
return callerHandler.apply(this, args);
|
|
@@ -340,7 +341,7 @@ function createFormController(options = {}) {
|
|
|
340
341
|
if (!el._propEffects) el._propEffects = {};
|
|
341
342
|
const key = 'checked:property';
|
|
342
343
|
if (el._propEffects[key]) {
|
|
343
|
-
try { el._propEffects[key](); } catch
|
|
344
|
+
try { el._propEffects[key](); } catch { /* already disposed */ }
|
|
344
345
|
}
|
|
345
346
|
// radioValueOf(el) is re-read on every run rather than captured, so a
|
|
346
347
|
// registration without a declared value still binds correctly once the
|
package/src/head.js
CHANGED
|
@@ -130,7 +130,8 @@ function escapeHtml(str) {
|
|
|
130
130
|
function escapeSelectorValue(key) {
|
|
131
131
|
const s = String(key);
|
|
132
132
|
if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') return CSS.escape(s);
|
|
133
|
-
|
|
133
|
+
// `c` is a single matched code unit, so codePointAt(0) is always defined.
|
|
134
|
+
return s.replace(/[^a-zA-Z0-9_-]/g, (c) => `\\${/** @type {number} */ (c.codePointAt(0)).toString(16)} `);
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
function setHeadTag(tag, key, attrs) {
|
package/src/hooks.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Components run ONCE. Hooks return signal accessors (functions) so the
|
|
4
4
|
// fine-grained runtime handles reactive updates automatically via effects.
|
|
5
5
|
|
|
6
|
-
import { signal, computed, effect, batch, untrack,
|
|
6
|
+
import { signal, computed, effect, batch, untrack, __DEV__ } from './reactive.js';
|
|
7
7
|
import { getCurrentComponent } from './dom.js';
|
|
8
8
|
import { getServerContext, isServerRender } from './server-context.js';
|
|
9
9
|
import { getLoaderData as _getLoaderData, getResource as _getResource } from './hydration-data.js';
|
|
@@ -138,7 +138,7 @@ export function useEffect(fn, deps) {
|
|
|
138
138
|
if (ctx.disposed) return;
|
|
139
139
|
hook.dispose = effect(() => {
|
|
140
140
|
if (hook.cleanup) {
|
|
141
|
-
try { hook.cleanup(); } catch
|
|
141
|
+
try { hook.cleanup(); } catch { /* cleanup error */ }
|
|
142
142
|
hook.cleanup = null;
|
|
143
143
|
}
|
|
144
144
|
const result = fn();
|
|
@@ -174,7 +174,7 @@ export function useEffect(fn, deps) {
|
|
|
174
174
|
|
|
175
175
|
// Run cleanup from previous execution
|
|
176
176
|
if (hook.cleanup) {
|
|
177
|
-
try { hook.cleanup(); } catch
|
|
177
|
+
try { hook.cleanup(); } catch { /* cleanup error */ }
|
|
178
178
|
hook.cleanup = null;
|
|
179
179
|
}
|
|
180
180
|
|
|
@@ -195,7 +195,7 @@ export function useEffect(fn, deps) {
|
|
|
195
195
|
// computed() auto-tracks signal dependencies.
|
|
196
196
|
// Returns a computed signal function (call it to read the value).
|
|
197
197
|
|
|
198
|
-
export function useMemo(fn,
|
|
198
|
+
export function useMemo(fn, _deps) {
|
|
199
199
|
const ctx = getCtx('useMemo');
|
|
200
200
|
const { index, exists } = getHook(ctx);
|
|
201
201
|
|
|
@@ -211,7 +211,7 @@ export function useMemo(fn, deps) {
|
|
|
211
211
|
// executes once, so the callback reference is inherently stable.
|
|
212
212
|
// Simply store and return the function on first call.
|
|
213
213
|
|
|
214
|
-
export function useCallback(fn,
|
|
214
|
+
export function useCallback(fn, _deps) {
|
|
215
215
|
const ctx = getCtx('useCallback');
|
|
216
216
|
const { index, exists } = getHook(ctx);
|
|
217
217
|
|
|
@@ -295,7 +295,7 @@ export function createContext(defaultValue) {
|
|
|
295
295
|
};
|
|
296
296
|
// The context value is only published once the provider body runs, so
|
|
297
297
|
// compiled children must not be built during this call. See createComponent.
|
|
298
|
-
context.Provider._deferChildren = true;
|
|
298
|
+
/** @type {any} */ (context.Provider)._deferChildren = true;
|
|
299
299
|
return context;
|
|
300
300
|
}
|
|
301
301
|
|
|
@@ -452,14 +452,7 @@ export function createResource(fetcher, options = {}) {
|
|
|
452
452
|
return [data, { loading, error, refetch, mutate }];
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
-
//
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if (!oldDeps || !newDeps) return true;
|
|
460
|
-
if (oldDeps.length !== newDeps.length) return true;
|
|
461
|
-
for (let i = 0; i < oldDeps.length; i++) {
|
|
462
|
-
if (!Object.is(oldDeps[i], newDeps[i])) return true;
|
|
463
|
-
}
|
|
464
|
-
return false;
|
|
465
|
-
}
|
|
455
|
+
// A local `depsChanged` lived here, commented "kept for potential external
|
|
456
|
+
// use". It was never exported, so there was no external use to keep it for,
|
|
457
|
+
// and nothing in this package called it. react-compat has its own copy, which
|
|
458
|
+
// is the one that is actually used.
|
package/src/index.js
CHANGED
package/src/reactive.js
CHANGED
|
@@ -30,6 +30,7 @@ export const __DEV__ =
|
|
|
30
30
|
|
|
31
31
|
// DevTools hooks — set by what-devtools when installed.
|
|
32
32
|
// These are no-ops in production (dead-code eliminated with __DEV__).
|
|
33
|
+
/** @type {WhatDevToolsHooks | null} */
|
|
33
34
|
export let __devtools = null;
|
|
34
35
|
|
|
35
36
|
/** @internal Install devtools hooks. Called by what-devtools. */
|
|
@@ -96,7 +97,7 @@ export function signal(initial, debugName) {
|
|
|
96
97
|
// Invalidate lastTracked since value changed — any effect that reads
|
|
97
98
|
// this signal during re-run needs to re-track.
|
|
98
99
|
lastTracked = null;
|
|
99
|
-
if (__DEV__ && __devtools) __devtools.onSignalUpdate(sig);
|
|
100
|
+
if (__DEV__ && __devtools) __devtools.onSignalUpdate?.(sig);
|
|
100
101
|
if (subs.size > 0) notify(subs);
|
|
101
102
|
}
|
|
102
103
|
|
|
@@ -134,11 +135,21 @@ export function signal(initial, debugName) {
|
|
|
134
135
|
sig._signal = true;
|
|
135
136
|
if (__DEV__) {
|
|
136
137
|
sig._subs = subs;
|
|
138
|
+
// Back-reference from the subscriber Set to its signal, for trackSignals()
|
|
139
|
+
// in testing.js. An effect records dependencies as the `subs` Sets it
|
|
140
|
+
// joined (see the read path above), which is one-directional: given an
|
|
141
|
+
// effect you can count its deps but not name them.
|
|
142
|
+
//
|
|
143
|
+
// Deliberately NOT `_owner`. That property is load-bearing for topological
|
|
144
|
+
// level computation, and a signal's Set is documented above as having
|
|
145
|
+
// `_owner === undefined` precisely because signals are level 0. A separate
|
|
146
|
+
// dev-only name keeps that invariant intact.
|
|
147
|
+
subs._signalOwner = sig;
|
|
137
148
|
if (debugName) sig._debugName = debugName;
|
|
138
149
|
}
|
|
139
150
|
|
|
140
151
|
// Notify devtools of signal creation
|
|
141
|
-
if (__DEV__ && __devtools) __devtools.onSignalCreate(sig);
|
|
152
|
+
if (__DEV__ && __devtools) __devtools.onSignalCreate?.(sig);
|
|
142
153
|
|
|
143
154
|
return sig;
|
|
144
155
|
}
|
|
@@ -337,7 +348,7 @@ export function effect(fn, opts) {
|
|
|
337
348
|
// effect could never re-fire anyway, so releasing is safe.
|
|
338
349
|
if (e.deps.length === 0 && e._cleanup === null) {
|
|
339
350
|
e.disposed = true;
|
|
340
|
-
if (__DEV__ && __devtools) __devtools.onEffectDispose(e);
|
|
351
|
+
if (__DEV__ && __devtools) __devtools.onEffectDispose?.(e);
|
|
341
352
|
return _noopDispose;
|
|
342
353
|
}
|
|
343
354
|
|
|
@@ -369,6 +380,7 @@ function _createEffect(fn, lazy) {
|
|
|
369
380
|
// IMPORTANT: V8 optimizes objects with a consistent "hidden class" (shape).
|
|
370
381
|
// All properties must be declared upfront even if null — adding properties
|
|
371
382
|
// later causes shape transitions which deoptimize property access globally.
|
|
383
|
+
/** @type {WhatEffectNode} */
|
|
372
384
|
const e = {
|
|
373
385
|
fn,
|
|
374
386
|
deps: [], // array of subscriber sets (cheaper than Set for typical 1-3 deps)
|
|
@@ -385,7 +397,7 @@ function _createEffect(fn, lazy) {
|
|
|
385
397
|
_cleanup: null, // cleanup function returned by effect fn (declared upfront for shape)
|
|
386
398
|
_epoch: 0, // incremented on cleanup — used by signal lastTracked cache
|
|
387
399
|
};
|
|
388
|
-
if (__DEV__ && __devtools) __devtools.onEffectCreate(e);
|
|
400
|
+
if (__DEV__ && __devtools) __devtools.onEffectCreate?.(e);
|
|
389
401
|
return e;
|
|
390
402
|
}
|
|
391
403
|
|
|
@@ -409,12 +421,12 @@ function _runEffect(e) {
|
|
|
409
421
|
const result = e.fn();
|
|
410
422
|
if (typeof result === 'function') e._cleanup = result;
|
|
411
423
|
} catch (err) {
|
|
412
|
-
if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });
|
|
424
|
+
if (__devtools?.onError) __devtools.onError?.(err, { type: 'effect', effect: e });
|
|
413
425
|
if (__DEV__) console.warn('[what] Error in stable effect:', err);
|
|
414
426
|
} finally {
|
|
415
427
|
currentEffect = prev;
|
|
416
428
|
}
|
|
417
|
-
if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun(e);
|
|
429
|
+
if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun?.(e);
|
|
418
430
|
return;
|
|
419
431
|
}
|
|
420
432
|
|
|
@@ -426,7 +438,7 @@ function _runEffect(e) {
|
|
|
426
438
|
// Run effect cleanup from previous run
|
|
427
439
|
if (e._cleanup) {
|
|
428
440
|
try { e._cleanup(); } catch (err) {
|
|
429
|
-
if (__DEV__ && __devtools?.onError) __devtools.onError(err, { type: 'effect-cleanup', effect: e });
|
|
441
|
+
if (__DEV__ && __devtools?.onError) __devtools.onError?.(err, { type: 'effect-cleanup', effect: e });
|
|
430
442
|
if (__DEV__) console.warn('[what] Error in effect cleanup:', err);
|
|
431
443
|
}
|
|
432
444
|
e._cleanup = null;
|
|
@@ -441,7 +453,7 @@ function _runEffect(e) {
|
|
|
441
453
|
}
|
|
442
454
|
} catch (err) {
|
|
443
455
|
if (err === NEEDS_UPSTREAM) throw err; // Iterative eval sentinel — not a real error
|
|
444
|
-
if (__DEV__ && __devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });
|
|
456
|
+
if (__DEV__ && __devtools?.onError) __devtools.onError?.(err, { type: 'effect', effect: e });
|
|
445
457
|
throw err;
|
|
446
458
|
} finally {
|
|
447
459
|
currentEffect = prev;
|
|
@@ -458,12 +470,12 @@ function _runEffect(e) {
|
|
|
458
470
|
e._stable = true;
|
|
459
471
|
}
|
|
460
472
|
|
|
461
|
-
if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun(e);
|
|
473
|
+
if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun?.(e);
|
|
462
474
|
}
|
|
463
475
|
|
|
464
476
|
function _disposeEffect(e) {
|
|
465
477
|
e.disposed = true;
|
|
466
|
-
if (__DEV__ && __devtools) __devtools.onEffectDispose(e);
|
|
478
|
+
if (__DEV__ && __devtools) __devtools.onEffectDispose?.(e);
|
|
467
479
|
cleanup(e);
|
|
468
480
|
// Run cleanup on dispose
|
|
469
481
|
if (e._cleanup) {
|
|
@@ -509,11 +521,11 @@ function _processSubscriber(e) {
|
|
|
509
521
|
try {
|
|
510
522
|
const result = e.fn();
|
|
511
523
|
if (typeof result === 'function') {
|
|
512
|
-
if (e._cleanup) try { e._cleanup(); } catch
|
|
524
|
+
if (e._cleanup) try { e._cleanup(); } catch { /* ignore */ }
|
|
513
525
|
e._cleanup = result;
|
|
514
526
|
}
|
|
515
527
|
} catch (err) {
|
|
516
|
-
if (__DEV__ && __devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });
|
|
528
|
+
if (__DEV__ && __devtools?.onError) __devtools.onError?.(err, { type: 'effect', effect: e });
|
|
517
529
|
if (__DEV__) console.warn('[what] Error in stable effect:', err);
|
|
518
530
|
} finally {
|
|
519
531
|
currentEffect = prev;
|
|
@@ -617,7 +629,7 @@ function flush() {
|
|
|
617
629
|
_runEffect(e);
|
|
618
630
|
} catch (err) {
|
|
619
631
|
if (err === NEEDS_UPSTREAM) throw err;
|
|
620
|
-
if (__DEV__ && __devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });
|
|
632
|
+
if (__DEV__ && __devtools?.onError) __devtools.onError?.(err, { type: 'effect', effect: e });
|
|
621
633
|
// Surface in production too — an uncaught reactive-update error is a
|
|
622
634
|
// real bug; staying silent (as the old throw-out-of-flush did once it
|
|
623
635
|
// escaped) hides it. console.error never aborts the batch.
|
|
@@ -786,6 +798,7 @@ export function createRoot(fn) {
|
|
|
786
798
|
const prevRoot = currentRoot;
|
|
787
799
|
const prevOwner = currentOwner;
|
|
788
800
|
const root = {
|
|
801
|
+
/** @type {Array<() => void>} */
|
|
789
802
|
disposals: [],
|
|
790
803
|
owner: currentOwner, // parent owner for ownership tree
|
|
791
804
|
children: [], // child roots (ownership tree)
|
|
@@ -856,6 +869,7 @@ export function _createItemScope(fn) {
|
|
|
856
869
|
const prevRoot = currentRoot;
|
|
857
870
|
const prevOwner = currentOwner;
|
|
858
871
|
const scope = {
|
|
872
|
+
/** @type {Array<() => void>} */
|
|
859
873
|
disposals: [],
|
|
860
874
|
owner: null, // No parent registration
|
|
861
875
|
children: [], // Kept for compat with effects that create sub-roots
|
|
@@ -914,6 +928,7 @@ if (__DEV__ && typeof WeakRef !== 'undefined') {
|
|
|
914
928
|
// needed before the devtools entry point runs; once devtools install, the
|
|
915
929
|
// buffer is drained and subsequent creations flow through the real hooks.
|
|
916
930
|
const PREINSTALL_CAP = 2000;
|
|
931
|
+
/** @type {{ signals: Set<any>, effects: Set<any>, components: any[] }} */
|
|
917
932
|
const buffer = { signals: new Set(), effects: new Set(), components: [] };
|
|
918
933
|
__devtools = {
|
|
919
934
|
__isPreinstallBuffer: true,
|
|
@@ -940,9 +955,10 @@ if (__DEV__ && typeof WeakRef !== 'undefined') {
|
|
|
940
955
|
* __setDevToolsHooks replaces the placeholder. Returns arrays of live refs.
|
|
941
956
|
*/
|
|
942
957
|
export function __drainPreinstallBuffer() {
|
|
943
|
-
if (!__DEV__) return { signals: [], effects: [], components: [] };
|
|
958
|
+
if (!__DEV__) return /** @type {{ signals: any[], effects: any[], components: any[] }} */ ({ signals: [], effects: [], components: [] });
|
|
944
959
|
// If the current __devtools is the real one (no __isPreinstallBuffer), the
|
|
945
960
|
// caller installed late and there is nothing to drain from this side.
|
|
961
|
+
/** @type {{ signals: any[], effects: any[], components: any[] }} */
|
|
946
962
|
const out = { signals: [], effects: [], components: [] };
|
|
947
963
|
const buf = (typeof __preinstallSnapshot !== 'undefined') ? __preinstallSnapshot : null;
|
|
948
964
|
if (!buf) return out;
|
|
@@ -954,6 +970,7 @@ export function __drainPreinstallBuffer() {
|
|
|
954
970
|
|
|
955
971
|
// Capture the placeholder buffer at module load so __drainPreinstallBuffer
|
|
956
972
|
// can return it AFTER __setDevToolsHooks has replaced __devtools.
|
|
973
|
+
/** @type {WhatDevToolsHooks['__buffer'] | null} */
|
|
957
974
|
let __preinstallSnapshot = null;
|
|
958
975
|
if (__DEV__ && __devtools?.__isPreinstallBuffer) {
|
|
959
976
|
__preinstallSnapshot = __devtools.__buffer;
|