@bquery/bquery 1.7.0 → 1.8.2
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 +760 -716
- package/dist/{a11y-C5QOVvRn.js → a11y-DVBCy09c.js} +3 -3
- package/dist/a11y-DVBCy09c.js.map +1 -0
- package/dist/a11y.es.mjs +1 -1
- package/dist/component/library.d.ts.map +1 -1
- package/dist/{component-CuuTijA6.js → component-L3-JfOFz.js} +5 -5
- package/dist/component-L3-JfOFz.js.map +1 -0
- package/dist/component.es.mjs +1 -1
- package/dist/{config-BW35FKuA.js → config-DhT9auRm.js} +1 -1
- package/dist/{config-BW35FKuA.js.map → config-DhT9auRm.js.map} +1 -1
- package/dist/{constraints-3lV9yyBw.js → constraints-D5RHQLmP.js} +1 -1
- package/dist/constraints-D5RHQLmP.js.map +1 -0
- package/dist/core/collection.d.ts +86 -0
- package/dist/core/collection.d.ts.map +1 -1
- package/dist/core/element.d.ts +28 -0
- package/dist/core/element.d.ts.map +1 -1
- package/dist/core/shared.d.ts +6 -0
- package/dist/core/shared.d.ts.map +1 -1
- package/dist/core-DdtZHzsS.js +168 -0
- package/dist/core-DdtZHzsS.js.map +1 -0
- package/dist/{core-Cjl7GUu8.js → core-EMYSLzaT.js} +289 -259
- package/dist/core-EMYSLzaT.js.map +1 -0
- package/dist/core.es.mjs +48 -47
- package/dist/{custom-directives-7wAShnnd.js → custom-directives-Dr4C5lVV.js} +1 -1
- package/dist/custom-directives-Dr4C5lVV.js.map +1 -0
- package/dist/{devtools-D2fQLhDN.js → devtools-BhB2iDPT.js} +2 -2
- package/dist/devtools-BhB2iDPT.js.map +1 -0
- package/dist/devtools.es.mjs +1 -1
- package/dist/{dnd-B8EgyzaI.js → dnd-NwZBYh4l.js} +1 -1
- package/dist/dnd-NwZBYh4l.js.map +1 -0
- package/dist/dnd.es.mjs +1 -1
- package/dist/{env-NeVmr4Gf.js → env-CTdvLaH2.js} +1 -1
- package/dist/env-CTdvLaH2.js.map +1 -0
- package/dist/forms/create-form.d.ts.map +1 -1
- package/dist/forms/index.d.ts +3 -2
- package/dist/forms/index.d.ts.map +1 -1
- package/dist/forms/types.d.ts +46 -0
- package/dist/forms/types.d.ts.map +1 -1
- package/dist/forms/use-field.d.ts +34 -0
- package/dist/forms/use-field.d.ts.map +1 -0
- package/dist/forms/validators.d.ts +25 -0
- package/dist/forms/validators.d.ts.map +1 -1
- package/dist/forms-UcRHsYxC.js +227 -0
- package/dist/forms-UcRHsYxC.js.map +1 -0
- package/dist/forms.es.mjs +14 -12
- package/dist/full.d.ts +17 -26
- package/dist/full.d.ts.map +1 -1
- package/dist/full.es.mjs +206 -181
- package/dist/full.iife.js +33 -33
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +33 -33
- package/dist/full.umd.js.map +1 -1
- package/dist/function-Cybd57JV.js +33 -0
- package/dist/function-Cybd57JV.js.map +1 -0
- package/dist/{i18n-BnnhTFOS.js → i18n-kuF6Ekj6.js} +3 -3
- package/dist/i18n-kuF6Ekj6.js.map +1 -0
- package/dist/i18n.es.mjs +1 -1
- package/dist/index.es.mjs +251 -228
- package/dist/media/breakpoints.d.ts.map +1 -1
- package/dist/media/types.d.ts +2 -2
- package/dist/media/types.d.ts.map +1 -1
- package/dist/{media-Di2Ta22s.js → media-i-fB5WxI.js} +3 -3
- package/dist/media-i-fB5WxI.js.map +1 -0
- package/dist/media.es.mjs +1 -1
- package/dist/{motion-qPj_TYGv.js → motion-BJsAuULb.js} +2 -2
- package/dist/motion-BJsAuULb.js.map +1 -0
- package/dist/motion.es.mjs +1 -1
- package/dist/{mount-SM07RUa6.js → mount-B4Y8bk8Z.js} +5 -5
- package/dist/mount-B4Y8bk8Z.js.map +1 -0
- package/dist/{platform-CPbCprb6.js → platform-Dw2gE3zI.js} +3 -3
- package/dist/{platform-CPbCprb6.js.map → platform-Dw2gE3zI.js.map} +1 -1
- package/dist/platform.es.mjs +2 -2
- package/dist/plugin/registry.d.ts.map +1 -1
- package/dist/{plugin-cPoOHFLY.js → plugin-C2WuC8SF.js} +20 -18
- package/dist/plugin-C2WuC8SF.js.map +1 -0
- package/dist/plugin.es.mjs +1 -1
- package/dist/reactive/async-data.d.ts +28 -3
- package/dist/reactive/async-data.d.ts.map +1 -1
- package/dist/reactive/computed.d.ts +3 -0
- package/dist/reactive/computed.d.ts.map +1 -1
- package/dist/reactive/effect.d.ts +3 -0
- package/dist/reactive/effect.d.ts.map +1 -1
- package/dist/reactive/http.d.ts +194 -0
- package/dist/reactive/http.d.ts.map +1 -0
- package/dist/reactive/index.d.ts +2 -2
- package/dist/reactive/index.d.ts.map +1 -1
- package/dist/reactive/pagination.d.ts +126 -0
- package/dist/reactive/pagination.d.ts.map +1 -0
- package/dist/reactive/polling.d.ts +55 -0
- package/dist/reactive/polling.d.ts.map +1 -0
- package/dist/reactive/readonly.d.ts +20 -1
- package/dist/reactive/readonly.d.ts.map +1 -1
- package/dist/reactive/rest.d.ts +293 -0
- package/dist/reactive/rest.d.ts.map +1 -0
- package/dist/reactive/scope.d.ts +140 -0
- package/dist/reactive/scope.d.ts.map +1 -0
- package/dist/reactive/signal.d.ts +16 -2
- package/dist/reactive/signal.d.ts.map +1 -1
- package/dist/reactive/to-value.d.ts +57 -0
- package/dist/reactive/to-value.d.ts.map +1 -0
- package/dist/reactive/websocket.d.ts +285 -0
- package/dist/reactive/websocket.d.ts.map +1 -0
- package/dist/reactive-DwkhUJfP.js +1148 -0
- package/dist/reactive-DwkhUJfP.js.map +1 -0
- package/dist/reactive.es.mjs +38 -19
- package/dist/{registry-CWf368tT.js → registry-B08iilIh.js} +1 -1
- package/dist/{registry-CWf368tT.js.map → registry-B08iilIh.js.map} +1 -1
- package/dist/router/constraints.d.ts.map +1 -1
- package/dist/router/index.d.ts +1 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/state.d.ts +25 -2
- package/dist/router/state.d.ts.map +1 -1
- package/dist/router-CQikC9Ed.js +492 -0
- package/dist/router-CQikC9Ed.js.map +1 -0
- package/dist/router.es.mjs +9 -8
- package/dist/ssr/hydrate.d.ts.map +1 -1
- package/dist/{ssr-B2qd_WBB.js → ssr-_dAcGdzu.js} +4 -4
- package/dist/ssr-_dAcGdzu.js.map +1 -0
- package/dist/ssr.es.mjs +1 -1
- package/dist/store/persisted.d.ts.map +1 -1
- package/dist/{store-DWpyH6p5.js → store-Cb3gPRve.js} +7 -7
- package/dist/store-Cb3gPRve.js.map +1 -0
- package/dist/store.es.mjs +2 -2
- package/dist/storybook.es.mjs.map +1 -1
- package/dist/{testing-CsqjNUyy.js → testing-C5Sjfsna.js} +8 -8
- package/dist/testing-C5Sjfsna.js.map +1 -0
- package/dist/testing.es.mjs +1 -1
- package/dist/{type-guards-Do9DWgNp.js → type-guards-BMX2c0LP.js} +1 -1
- package/dist/{type-guards-Do9DWgNp.js.map → type-guards-BMX2c0LP.js.map} +1 -1
- package/dist/untrack-D0fnO5k2.js +36 -0
- package/dist/untrack-D0fnO5k2.js.map +1 -0
- package/dist/view/custom-directives.d.ts.map +1 -1
- package/dist/view.es.mjs +4 -4
- package/package.json +178 -177
- package/src/a11y/announce.ts +131 -131
- package/src/a11y/audit.ts +314 -314
- package/src/a11y/index.ts +68 -68
- package/src/a11y/media-preferences.ts +255 -255
- package/src/a11y/roving-tab-index.ts +164 -164
- package/src/a11y/skip-link.ts +255 -255
- package/src/a11y/trap-focus.ts +184 -184
- package/src/a11y/types.ts +183 -183
- package/src/component/component.ts +599 -599
- package/src/component/html.ts +153 -153
- package/src/component/index.ts +52 -52
- package/src/component/library.ts +540 -542
- package/src/component/scope.ts +212 -212
- package/src/component/types.ts +310 -310
- package/src/core/collection.ts +876 -707
- package/src/core/element.ts +1015 -981
- package/src/core/env.ts +60 -60
- package/src/core/index.ts +49 -49
- package/src/core/shared.ts +77 -62
- package/src/core/utils/index.ts +148 -148
- package/src/devtools/devtools.ts +410 -410
- package/src/devtools/index.ts +48 -48
- package/src/devtools/types.ts +104 -104
- package/src/dnd/draggable.ts +296 -296
- package/src/dnd/droppable.ts +228 -228
- package/src/dnd/index.ts +62 -62
- package/src/dnd/sortable.ts +307 -307
- package/src/dnd/types.ts +293 -293
- package/src/forms/create-form.ts +320 -278
- package/src/forms/index.ts +70 -65
- package/src/forms/types.ts +203 -154
- package/src/forms/use-field.ts +231 -0
- package/src/forms/validators.ts +294 -265
- package/src/full.ts +554 -480
- package/src/i18n/formatting.ts +67 -67
- package/src/i18n/i18n.ts +200 -200
- package/src/i18n/index.ts +67 -67
- package/src/i18n/translate.ts +182 -182
- package/src/i18n/types.ts +171 -171
- package/src/index.ts +108 -108
- package/src/media/battery.ts +116 -116
- package/src/media/breakpoints.ts +129 -131
- package/src/media/clipboard.ts +80 -80
- package/src/media/device-sensors.ts +158 -158
- package/src/media/geolocation.ts +119 -119
- package/src/media/index.ts +76 -76
- package/src/media/media-query.ts +92 -92
- package/src/media/network.ts +115 -115
- package/src/media/types.ts +177 -177
- package/src/media/viewport.ts +84 -84
- package/src/motion/index.ts +57 -57
- package/src/motion/morph.ts +151 -151
- package/src/motion/parallax.ts +120 -120
- package/src/motion/reduced-motion.ts +66 -66
- package/src/motion/types.ts +271 -271
- package/src/motion/typewriter.ts +164 -164
- package/src/plugin/index.ts +37 -37
- package/src/plugin/registry.ts +284 -269
- package/src/plugin/types.ts +137 -137
- package/src/reactive/async-data.ts +250 -29
- package/src/reactive/computed.ts +144 -130
- package/src/reactive/effect.ts +29 -6
- package/src/reactive/http.ts +790 -0
- package/src/reactive/index.ts +60 -0
- package/src/reactive/pagination.ts +317 -0
- package/src/reactive/polling.ts +179 -0
- package/src/reactive/readonly.ts +52 -8
- package/src/reactive/rest.ts +859 -0
- package/src/reactive/scope.ts +276 -0
- package/src/reactive/signal.ts +61 -1
- package/src/reactive/to-value.ts +71 -0
- package/src/reactive/websocket.ts +849 -0
- package/src/router/bq-link.ts +279 -279
- package/src/router/constraints.ts +204 -201
- package/src/router/index.ts +49 -49
- package/src/router/match.ts +312 -312
- package/src/router/path-pattern.ts +52 -52
- package/src/router/query.ts +38 -38
- package/src/router/router.ts +421 -402
- package/src/router/state.ts +51 -3
- package/src/router/types.ts +139 -139
- package/src/router/use-route.ts +68 -68
- package/src/router/utils.ts +157 -157
- package/src/security/index.ts +12 -12
- package/src/ssr/hydrate.ts +84 -82
- package/src/ssr/index.ts +70 -70
- package/src/ssr/render.ts +508 -508
- package/src/ssr/serialize.ts +296 -296
- package/src/ssr/types.ts +81 -81
- package/src/store/create-store.ts +467 -467
- package/src/store/index.ts +27 -27
- package/src/store/persisted.ts +245 -249
- package/src/store/types.ts +247 -247
- package/src/store/utils.ts +135 -135
- package/src/storybook/index.ts +480 -480
- package/src/testing/index.ts +42 -42
- package/src/testing/testing.ts +593 -593
- package/src/testing/types.ts +170 -170
- package/src/view/custom-directives.ts +28 -30
- package/src/view/evaluate.ts +292 -292
- package/src/view/process.ts +108 -108
- package/dist/a11y-C5QOVvRn.js.map +0 -1
- package/dist/component-CuuTijA6.js.map +0 -1
- package/dist/constraints-3lV9yyBw.js.map +0 -1
- package/dist/core-Cjl7GUu8.js.map +0 -1
- package/dist/core-DnlyjbF2.js +0 -112
- package/dist/core-DnlyjbF2.js.map +0 -1
- package/dist/custom-directives-7wAShnnd.js.map +0 -1
- package/dist/devtools-D2fQLhDN.js.map +0 -1
- package/dist/dnd-B8EgyzaI.js.map +0 -1
- package/dist/env-NeVmr4Gf.js.map +0 -1
- package/dist/forms-C3yovgH9.js +0 -141
- package/dist/forms-C3yovgH9.js.map +0 -1
- package/dist/i18n-BnnhTFOS.js.map +0 -1
- package/dist/media-Di2Ta22s.js.map +0 -1
- package/dist/motion-qPj_TYGv.js.map +0 -1
- package/dist/mount-SM07RUa6.js.map +0 -1
- package/dist/plugin-cPoOHFLY.js.map +0 -1
- package/dist/reactive-Cfv0RK6x.js +0 -233
- package/dist/reactive-Cfv0RK6x.js.map +0 -1
- package/dist/router-BrthaP_z.js +0 -473
- package/dist/router-BrthaP_z.js.map +0 -1
- package/dist/ssr-B2qd_WBB.js.map +0 -1
- package/dist/store-DWpyH6p5.js.map +0 -1
- package/dist/testing-CsqjNUyy.js.map +0 -1
- package/dist/untrack-DJVQQ2WM.js +0 -33
- package/dist/untrack-DJVQQ2WM.js.map +0 -1
package/src/plugin/types.ts
CHANGED
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Public types for the bQuery plugin system.
|
|
3
|
-
*
|
|
4
|
-
* Plugins extend bQuery by registering custom directives and Web Components
|
|
5
|
-
* through a single unified interface.
|
|
6
|
-
*
|
|
7
|
-
* @module bquery/plugin
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { CleanupFn } from '../reactive/index';
|
|
11
|
-
import type { BindingContext } from '../view/types';
|
|
12
|
-
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
// Custom Directive
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* A custom directive handler that is invoked when the view module encounters
|
|
19
|
-
* a `bq-{name}` attribute during mount processing.
|
|
20
|
-
*
|
|
21
|
-
* @param el - The DOM element carrying the directive attribute
|
|
22
|
-
* @param expression - The raw attribute value (expression string)
|
|
23
|
-
* @param context - The current binding context (data / signals)
|
|
24
|
-
* @param cleanups - Array where the handler should push any cleanup functions
|
|
25
|
-
*
|
|
26
|
-
* @example
|
|
27
|
-
* ```ts
|
|
28
|
-
* const tooltipDirective: CustomDirectiveHandler = (el, expression, ctx, cleanups) => {
|
|
29
|
-
* const tip = document.createElement('span');
|
|
30
|
-
* tip.textContent = String(expression);
|
|
31
|
-
* el.appendChild(tip);
|
|
32
|
-
* cleanups.push(() => tip.remove());
|
|
33
|
-
* };
|
|
34
|
-
* ```
|
|
35
|
-
*/
|
|
36
|
-
export type CustomDirectiveHandler = (
|
|
37
|
-
el: Element,
|
|
38
|
-
expression: string,
|
|
39
|
-
context: BindingContext,
|
|
40
|
-
cleanups: CleanupFn[]
|
|
41
|
-
) => void;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Descriptor for a custom directive registered by a plugin.
|
|
45
|
-
*/
|
|
46
|
-
export interface CustomDirective {
|
|
47
|
-
/** The directive name (without prefix). e.g. `'tooltip'` → `bq-tooltip` */
|
|
48
|
-
readonly name: string;
|
|
49
|
-
/** The handler function called when the directive is encountered. */
|
|
50
|
-
readonly handler: CustomDirectiveHandler;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// Plugin Install Context
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Context object provided to a plugin's `install` function.
|
|
59
|
-
*
|
|
60
|
-
* Plugins use these helpers to register their contributions into bQuery's
|
|
61
|
-
* global registries without directly importing internal modules.
|
|
62
|
-
*/
|
|
63
|
-
export interface PluginInstallContext {
|
|
64
|
-
/**
|
|
65
|
-
* Register a custom view directive that will be recognized during
|
|
66
|
-
* `mount()` processing.
|
|
67
|
-
*
|
|
68
|
-
* @param name - Directive name **without** the `bq-` prefix (e.g. `'tooltip'`)
|
|
69
|
-
* @param handler - The handler called for each element with the directive
|
|
70
|
-
*
|
|
71
|
-
* @example
|
|
72
|
-
* ```ts
|
|
73
|
-
* ctx.directive('focus', (el) => {
|
|
74
|
-
* (el as HTMLElement).focus();
|
|
75
|
-
* });
|
|
76
|
-
* ```
|
|
77
|
-
*/
|
|
78
|
-
directive(name: string, handler: CustomDirectiveHandler): void;
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Register a Web Component via the native `customElements.define()` API.
|
|
82
|
-
*
|
|
83
|
-
* @param tagName - Custom element tag (e.g. `'my-counter'`)
|
|
84
|
-
* @param constructor - The `HTMLElement` subclass
|
|
85
|
-
* @param options - Optional `ElementDefinitionOptions` (e.g. `{ extends: 'div' }`)
|
|
86
|
-
*
|
|
87
|
-
* @example
|
|
88
|
-
* ```ts
|
|
89
|
-
* ctx.component('my-counter', MyCounterElement);
|
|
90
|
-
* ```
|
|
91
|
-
*/
|
|
92
|
-
component(
|
|
93
|
-
tagName: string,
|
|
94
|
-
constructor: CustomElementConstructor,
|
|
95
|
-
options?: ElementDefinitionOptions
|
|
96
|
-
): void;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ---------------------------------------------------------------------------
|
|
100
|
-
// Plugin Interface
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* A bQuery plugin.
|
|
105
|
-
*
|
|
106
|
-
* Plugins are plain objects with a `name` and an `install` function.
|
|
107
|
-
* Call `use(plugin)` to activate a plugin before creating routers, stores,
|
|
108
|
-
* or mounting views.
|
|
109
|
-
*
|
|
110
|
-
* @example
|
|
111
|
-
* ```ts
|
|
112
|
-
* import { use } from '@bquery/bquery/plugin';
|
|
113
|
-
*
|
|
114
|
-
* const myPlugin: BQueryPlugin = {
|
|
115
|
-
* name: 'my-plugin',
|
|
116
|
-
* install(ctx, options) {
|
|
117
|
-
* ctx.directive('highlight', (el, expr) => {
|
|
118
|
-
* (el as HTMLElement).style.background = String(expr);
|
|
119
|
-
* });
|
|
120
|
-
* },
|
|
121
|
-
* };
|
|
122
|
-
*
|
|
123
|
-
* use(myPlugin, { color: 'yellow' });
|
|
124
|
-
* ```
|
|
125
|
-
*/
|
|
126
|
-
export interface BQueryPlugin<TOptions = unknown> {
|
|
127
|
-
/** Unique human-readable name for the plugin. */
|
|
128
|
-
readonly name: string;
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Called once when the plugin is registered via `use()`.
|
|
132
|
-
*
|
|
133
|
-
* @param context - Helpers for registering directives, components, etc.
|
|
134
|
-
* @param options - User-provided options forwarded from `use(plugin, options)`.
|
|
135
|
-
*/
|
|
136
|
-
install(context: PluginInstallContext, options?: TOptions): void;
|
|
137
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the bQuery plugin system.
|
|
3
|
+
*
|
|
4
|
+
* Plugins extend bQuery by registering custom directives and Web Components
|
|
5
|
+
* through a single unified interface.
|
|
6
|
+
*
|
|
7
|
+
* @module bquery/plugin
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CleanupFn } from '../reactive/index';
|
|
11
|
+
import type { BindingContext } from '../view/types';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Custom Directive
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A custom directive handler that is invoked when the view module encounters
|
|
19
|
+
* a `bq-{name}` attribute during mount processing.
|
|
20
|
+
*
|
|
21
|
+
* @param el - The DOM element carrying the directive attribute
|
|
22
|
+
* @param expression - The raw attribute value (expression string)
|
|
23
|
+
* @param context - The current binding context (data / signals)
|
|
24
|
+
* @param cleanups - Array where the handler should push any cleanup functions
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* const tooltipDirective: CustomDirectiveHandler = (el, expression, ctx, cleanups) => {
|
|
29
|
+
* const tip = document.createElement('span');
|
|
30
|
+
* tip.textContent = String(expression);
|
|
31
|
+
* el.appendChild(tip);
|
|
32
|
+
* cleanups.push(() => tip.remove());
|
|
33
|
+
* };
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export type CustomDirectiveHandler = (
|
|
37
|
+
el: Element,
|
|
38
|
+
expression: string,
|
|
39
|
+
context: BindingContext,
|
|
40
|
+
cleanups: CleanupFn[]
|
|
41
|
+
) => void;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Descriptor for a custom directive registered by a plugin.
|
|
45
|
+
*/
|
|
46
|
+
export interface CustomDirective {
|
|
47
|
+
/** The directive name (without prefix). e.g. `'tooltip'` → `bq-tooltip` */
|
|
48
|
+
readonly name: string;
|
|
49
|
+
/** The handler function called when the directive is encountered. */
|
|
50
|
+
readonly handler: CustomDirectiveHandler;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Plugin Install Context
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Context object provided to a plugin's `install` function.
|
|
59
|
+
*
|
|
60
|
+
* Plugins use these helpers to register their contributions into bQuery's
|
|
61
|
+
* global registries without directly importing internal modules.
|
|
62
|
+
*/
|
|
63
|
+
export interface PluginInstallContext {
|
|
64
|
+
/**
|
|
65
|
+
* Register a custom view directive that will be recognized during
|
|
66
|
+
* `mount()` processing.
|
|
67
|
+
*
|
|
68
|
+
* @param name - Directive name **without** the `bq-` prefix (e.g. `'tooltip'`)
|
|
69
|
+
* @param handler - The handler called for each element with the directive
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* ctx.directive('focus', (el) => {
|
|
74
|
+
* (el as HTMLElement).focus();
|
|
75
|
+
* });
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
directive(name: string, handler: CustomDirectiveHandler): void;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Register a Web Component via the native `customElements.define()` API.
|
|
82
|
+
*
|
|
83
|
+
* @param tagName - Custom element tag (e.g. `'my-counter'`)
|
|
84
|
+
* @param constructor - The `HTMLElement` subclass
|
|
85
|
+
* @param options - Optional `ElementDefinitionOptions` (e.g. `{ extends: 'div' }`)
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* ctx.component('my-counter', MyCounterElement);
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
component(
|
|
93
|
+
tagName: string,
|
|
94
|
+
constructor: CustomElementConstructor,
|
|
95
|
+
options?: ElementDefinitionOptions
|
|
96
|
+
): void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Plugin Interface
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A bQuery plugin.
|
|
105
|
+
*
|
|
106
|
+
* Plugins are plain objects with a `name` and an `install` function.
|
|
107
|
+
* Call `use(plugin)` to activate a plugin before creating routers, stores,
|
|
108
|
+
* or mounting views.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* import { use } from '@bquery/bquery/plugin';
|
|
113
|
+
*
|
|
114
|
+
* const myPlugin: BQueryPlugin = {
|
|
115
|
+
* name: 'my-plugin',
|
|
116
|
+
* install(ctx, options) {
|
|
117
|
+
* ctx.directive('highlight', (el, expr) => {
|
|
118
|
+
* (el as HTMLElement).style.background = String(expr);
|
|
119
|
+
* });
|
|
120
|
+
* },
|
|
121
|
+
* };
|
|
122
|
+
*
|
|
123
|
+
* use(myPlugin, { color: 'yellow' });
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export interface BQueryPlugin<TOptions = unknown> {
|
|
127
|
+
/** Unique human-readable name for the plugin. */
|
|
128
|
+
readonly name: string;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Called once when the plugin is registered via `use()`.
|
|
132
|
+
*
|
|
133
|
+
* @param context - Helpers for registering directives, components, etc.
|
|
134
|
+
* @param options - User-provided options forwarded from `use(plugin, options)`.
|
|
135
|
+
*/
|
|
136
|
+
install(context: PluginInstallContext, options?: TOptions): void;
|
|
137
|
+
}
|
|
@@ -47,15 +47,27 @@ export interface AsyncDataState<TData> {
|
|
|
47
47
|
execute: () => Promise<TData | undefined>;
|
|
48
48
|
/** Alias for execute(). */
|
|
49
49
|
refresh: () => Promise<TData | undefined>;
|
|
50
|
+
/** Abort the current in-flight request (useFetch only; no-op for useAsyncData). */
|
|
51
|
+
abort: () => void;
|
|
50
52
|
/** Clear data, error, and status back to the initial state. */
|
|
51
53
|
clear: () => void;
|
|
52
54
|
/** Dispose reactive watchers and prevent future executions. */
|
|
53
55
|
dispose: () => void;
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
/** Configuration for automatic request retries in useFetch(). */
|
|
59
|
+
export interface UseFetchRetryConfig {
|
|
60
|
+
/** Maximum number of retry attempts (default: 3). */
|
|
61
|
+
count: number;
|
|
62
|
+
/** Delay in ms between retries, or a function receiving the attempt index. */
|
|
63
|
+
delay?: number | ((attempt: number) => number);
|
|
64
|
+
/** Predicate deciding whether to retry. Defaults to network / 5xx errors. */
|
|
65
|
+
retryOn?: (error: Error, attempt: number) => boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
56
68
|
/** Options for useFetch(). */
|
|
57
69
|
export interface UseFetchOptions<TResponse = unknown, TData = TResponse>
|
|
58
|
-
extends UseAsyncDataOptions<TResponse, TData>, Omit<RequestInit, 'body' | 'headers'> {
|
|
70
|
+
extends UseAsyncDataOptions<TResponse, TData>, Omit<RequestInit, 'body' | 'headers' | 'signal'> {
|
|
59
71
|
/** Base URL prepended to relative URLs. */
|
|
60
72
|
baseUrl?: string;
|
|
61
73
|
/** Query parameters appended to the request URL. */
|
|
@@ -68,6 +80,14 @@ export interface UseFetchOptions<TResponse = unknown, TData = TResponse>
|
|
|
68
80
|
parseAs?: BqueryFetchParseAs;
|
|
69
81
|
/** Custom fetch implementation for testing or adapters. */
|
|
70
82
|
fetcher?: typeof fetch;
|
|
83
|
+
/** Request timeout in milliseconds. 0 means no timeout. */
|
|
84
|
+
timeout?: number;
|
|
85
|
+
/** External AbortSignal for request cancellation. */
|
|
86
|
+
signal?: AbortSignal;
|
|
87
|
+
/** Retry configuration. Pass a number for simple retry count, or a config object. */
|
|
88
|
+
retry?: number | UseFetchRetryConfig;
|
|
89
|
+
/** Custom status validation. Returns `true` for acceptable statuses. */
|
|
90
|
+
validateStatus?: (status: number) => boolean;
|
|
71
91
|
}
|
|
72
92
|
|
|
73
93
|
/** Input accepted by useFetch(). */
|
|
@@ -340,23 +360,101 @@ export const useAsyncData = <TResult, TData = TResult>(
|
|
|
340
360
|
pending,
|
|
341
361
|
execute,
|
|
342
362
|
refresh: execute,
|
|
363
|
+
abort: () => {},
|
|
343
364
|
clear,
|
|
344
365
|
dispose,
|
|
345
366
|
};
|
|
346
367
|
};
|
|
347
368
|
|
|
369
|
+
/** @internal */
|
|
370
|
+
const DEFAULT_VALIDATE_STATUS = (status: number): boolean => status >= 200 && status < 300;
|
|
371
|
+
|
|
372
|
+
/** @internal */
|
|
373
|
+
const isDomExceptionNamed = (error: unknown, name: string): error is DOMException =>
|
|
374
|
+
error instanceof DOMException && error.name === name;
|
|
375
|
+
|
|
376
|
+
/** @internal */
|
|
377
|
+
const isTimeoutDomException = (error: unknown): error is DOMException =>
|
|
378
|
+
isDomExceptionNamed(error, 'TimeoutError');
|
|
379
|
+
|
|
380
|
+
/** @internal */
|
|
381
|
+
const isAbortDomException = (error: unknown): error is DOMException =>
|
|
382
|
+
isDomExceptionNamed(error, 'AbortError');
|
|
383
|
+
|
|
384
|
+
/** @internal */
|
|
385
|
+
const DEFAULT_RETRY_ON = (error: Error): boolean => {
|
|
386
|
+
if (
|
|
387
|
+
isAbortDomException(error) ||
|
|
388
|
+
isTimeoutDomException(error) ||
|
|
389
|
+
(error as Error & { code?: string }).code === 'ABORT' ||
|
|
390
|
+
(error as Error & { code?: string }).code === 'TIMEOUT'
|
|
391
|
+
) {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
const status = (error as Error & { status?: number }).status;
|
|
395
|
+
return status === undefined || status >= 500;
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
/** @internal */
|
|
399
|
+
const normalizeRetryConfig = (retry: UseFetchOptions['retry']): UseFetchRetryConfig | undefined => {
|
|
400
|
+
if (retry == null) return undefined;
|
|
401
|
+
if (typeof retry === 'number') return { count: retry };
|
|
402
|
+
return retry;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
/** @internal */
|
|
406
|
+
const resolveRetryDelay = (delay: UseFetchRetryConfig['delay'], attempt: number): number => {
|
|
407
|
+
if (delay == null) return Math.min(1000 * 2 ** attempt, 30_000);
|
|
408
|
+
if (typeof delay === 'number') return delay;
|
|
409
|
+
return delay(attempt);
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
/** @internal */
|
|
413
|
+
const sleepWithSignal = (ms: number, abortSignal?: AbortSignal): Promise<void> =>
|
|
414
|
+
new Promise<void>((resolve, reject) => {
|
|
415
|
+
if (abortSignal?.aborted) {
|
|
416
|
+
reject(abortSignal.reason ?? new DOMException('The operation was aborted.', 'AbortError'));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
let cleanedUp = false;
|
|
420
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
421
|
+
|
|
422
|
+
const onAbort = (): void => {
|
|
423
|
+
if (cleanedUp) return;
|
|
424
|
+
cleanedUp = true;
|
|
425
|
+
clearTimeout(timer);
|
|
426
|
+
abortSignal?.removeEventListener('abort', onAbort);
|
|
427
|
+
reject(abortSignal?.reason ?? new DOMException('The operation was aborted.', 'AbortError'));
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
timer = setTimeout(() => {
|
|
431
|
+
if (cleanedUp) return;
|
|
432
|
+
cleanedUp = true;
|
|
433
|
+
abortSignal?.removeEventListener('abort', onAbort);
|
|
434
|
+
resolve();
|
|
435
|
+
}, ms);
|
|
436
|
+
|
|
437
|
+
abortSignal?.addEventListener('abort', onAbort, { once: true });
|
|
438
|
+
});
|
|
439
|
+
|
|
348
440
|
/**
|
|
349
441
|
* Reactive fetch composable using the browser Fetch API.
|
|
350
442
|
*
|
|
443
|
+
* Supports timeout, abort, retry, and custom status validation in addition
|
|
444
|
+
* to the core useFetch features (query params, JSON body, baseUrl, watch).
|
|
445
|
+
*
|
|
351
446
|
* @template TResponse - Raw parsed response type
|
|
352
447
|
* @template TData - Stored response type after optional transformation
|
|
353
448
|
* @param input - Request URL, Request object, or lazy input factory
|
|
354
449
|
* @param options - Request and reactive state options
|
|
355
|
-
* @returns Reactive fetch state with execute(), refresh(),
|
|
450
|
+
* @returns Reactive fetch state with execute(), refresh(), abort(), clear(), and dispose()
|
|
356
451
|
*
|
|
357
452
|
* @example
|
|
358
453
|
* ```ts
|
|
359
|
-
* const users = useFetch<{ id: number; name: string }[]>('/api/users'
|
|
454
|
+
* const users = useFetch<{ id: number; name: string }[]>('/api/users', {
|
|
455
|
+
* timeout: 5000,
|
|
456
|
+
* retry: 3,
|
|
457
|
+
* });
|
|
360
458
|
* ```
|
|
361
459
|
*/
|
|
362
460
|
export const useFetch = <TResponse = unknown, TData = TResponse>(
|
|
@@ -366,8 +464,22 @@ export const useFetch = <TResponse = unknown, TData = TResponse>(
|
|
|
366
464
|
const fetchConfig = getBqueryConfig().fetch;
|
|
367
465
|
const parseAs = options.parseAs ?? fetchConfig?.parseAs ?? 'json';
|
|
368
466
|
const fetcher = options.fetcher ?? fetch;
|
|
467
|
+
const validateStatus = options.validateStatus ?? DEFAULT_VALIDATE_STATUS;
|
|
468
|
+
|
|
469
|
+
let currentAbortController: AbortController | null = null;
|
|
470
|
+
const normalizeAbortLikeError = (reason: unknown, didTimeout: boolean): Error => {
|
|
471
|
+
const isTimeout =
|
|
472
|
+
didTimeout ||
|
|
473
|
+
isTimeoutDomException(reason) ||
|
|
474
|
+
isTimeoutDomException(currentAbortController?.signal.reason);
|
|
475
|
+
|
|
476
|
+
return Object.assign(
|
|
477
|
+
new Error(isTimeout ? `Request timeout of ${options.timeout}ms exceeded` : 'Request aborted'),
|
|
478
|
+
{ code: isTimeout ? 'TIMEOUT' : 'ABORT' }
|
|
479
|
+
);
|
|
480
|
+
};
|
|
369
481
|
|
|
370
|
-
|
|
482
|
+
const state = useAsyncData<TResponse, TData>(async () => {
|
|
371
483
|
const requestInput = resolveInput(input);
|
|
372
484
|
const requestUrl =
|
|
373
485
|
typeof requestInput === 'string' || requestInput instanceof URL
|
|
@@ -380,7 +492,7 @@ export const useFetch = <TResponse = unknown, TData = TResponse>(
|
|
|
380
492
|
appendQuery(requestUrl, options.query);
|
|
381
493
|
}
|
|
382
494
|
|
|
383
|
-
const
|
|
495
|
+
const baseHeaders = toHeaders(
|
|
384
496
|
fetchConfig?.headers,
|
|
385
497
|
requestInput instanceof Request ? requestInput.headers : undefined,
|
|
386
498
|
options.headers
|
|
@@ -393,23 +505,51 @@ export const useFetch = <TResponse = unknown, TData = TResponse>(
|
|
|
393
505
|
throw new Error(`Cannot send a request body with ${bodylessMethod} requests`);
|
|
394
506
|
}
|
|
395
507
|
const requestInitMethod = resolveRequestInitMethod(explicitMethod, requestInput, method);
|
|
396
|
-
const
|
|
508
|
+
const retryConfig = normalizeRetryConfig(options.retry);
|
|
509
|
+
const maxAttempts = (retryConfig?.count ?? 0) + 1;
|
|
510
|
+
|
|
511
|
+
// Abort controller: compose timeout + external signal + manual abort
|
|
512
|
+
const abortController = new AbortController();
|
|
513
|
+
currentAbortController = abortController;
|
|
514
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
515
|
+
let didTimeout = false;
|
|
516
|
+
let externalAbortHandler: (() => void) | undefined;
|
|
517
|
+
|
|
518
|
+
if (options.signal) {
|
|
519
|
+
if (options.signal.aborted) {
|
|
520
|
+
abortController.abort(options.signal.reason);
|
|
521
|
+
} else {
|
|
522
|
+
externalAbortHandler = () => abortController.abort(options.signal?.reason);
|
|
523
|
+
options.signal.addEventListener('abort', externalAbortHandler, { once: true });
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (options.timeout && options.timeout > 0) {
|
|
528
|
+
timeoutId = setTimeout(() => {
|
|
529
|
+
didTimeout = true;
|
|
530
|
+
abortController.abort(new DOMException('Request timeout', 'TimeoutError'));
|
|
531
|
+
}, options.timeout);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const baseRequestInit: Omit<RequestInit, 'body' | 'signal'> = {
|
|
397
535
|
...options,
|
|
398
536
|
method: requestInitMethod,
|
|
399
|
-
headers,
|
|
400
|
-
body: serializeBody(options.body, headers),
|
|
537
|
+
headers: baseHeaders,
|
|
401
538
|
};
|
|
402
539
|
|
|
403
|
-
delete (
|
|
404
|
-
delete (
|
|
405
|
-
delete (
|
|
406
|
-
delete (
|
|
407
|
-
delete (
|
|
408
|
-
delete (
|
|
409
|
-
delete (
|
|
410
|
-
delete (
|
|
411
|
-
delete (
|
|
412
|
-
delete (
|
|
540
|
+
delete (baseRequestInit as Partial<UseFetchOptions>).baseUrl;
|
|
541
|
+
delete (baseRequestInit as Partial<UseFetchOptions>).query;
|
|
542
|
+
delete (baseRequestInit as Partial<UseFetchOptions>).parseAs;
|
|
543
|
+
delete (baseRequestInit as Partial<UseFetchOptions>).fetcher;
|
|
544
|
+
delete (baseRequestInit as Partial<UseFetchOptions>).defaultValue;
|
|
545
|
+
delete (baseRequestInit as Partial<UseFetchOptions>).immediate;
|
|
546
|
+
delete (baseRequestInit as Partial<UseFetchOptions>).watch;
|
|
547
|
+
delete (baseRequestInit as Partial<UseFetchOptions>).transform;
|
|
548
|
+
delete (baseRequestInit as Partial<UseFetchOptions>).onSuccess;
|
|
549
|
+
delete (baseRequestInit as Partial<UseFetchOptions>).onError;
|
|
550
|
+
delete (baseRequestInit as Partial<UseFetchOptions>).timeout;
|
|
551
|
+
delete (baseRequestInit as Partial<UseFetchOptions>).retry;
|
|
552
|
+
delete (baseRequestInit as Partial<UseFetchOptions>).validateStatus;
|
|
413
553
|
|
|
414
554
|
let requestTarget: Request | string | URL = requestUrl ?? requestInput;
|
|
415
555
|
if (
|
|
@@ -417,22 +557,103 @@ export const useFetch = <TResponse = unknown, TData = TResponse>(
|
|
|
417
557
|
requestUrl &&
|
|
418
558
|
requestUrl.toString() !== requestInput.url
|
|
419
559
|
) {
|
|
420
|
-
// Rebuild Request inputs when query params changed so the updated URL is preserved.
|
|
421
|
-
// String/URL inputs already use `requestUrl` directly, so only Request objects need rebuilding.
|
|
422
560
|
requestTarget = new Request(requestUrl.toString(), toRequestInit(requestInput));
|
|
423
561
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
562
|
+
|
|
563
|
+
const createAttemptRequestInit = (): RequestInit => {
|
|
564
|
+
const headers = new Headers(baseHeaders);
|
|
565
|
+
return {
|
|
566
|
+
...baseRequestInit,
|
|
567
|
+
headers,
|
|
568
|
+
body: serializeBody(options.body, headers),
|
|
569
|
+
signal: abortController.signal,
|
|
570
|
+
};
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
if (
|
|
574
|
+
maxAttempts > 1 &&
|
|
575
|
+
typeof ReadableStream !== 'undefined' &&
|
|
576
|
+
options.body instanceof ReadableStream
|
|
577
|
+
) {
|
|
578
|
+
throw new Error('Cannot retry requests with ReadableStream bodies');
|
|
432
579
|
}
|
|
433
580
|
|
|
434
|
-
|
|
581
|
+
if (
|
|
582
|
+
maxAttempts > 1 &&
|
|
583
|
+
typeof Request !== 'undefined' &&
|
|
584
|
+
requestTarget instanceof Request &&
|
|
585
|
+
requestTarget.body !== null
|
|
586
|
+
) {
|
|
587
|
+
throw new Error('Cannot retry requests with non-replayable Request bodies');
|
|
588
|
+
}
|
|
589
|
+
let lastError: Error | undefined;
|
|
590
|
+
|
|
591
|
+
try {
|
|
592
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
593
|
+
try {
|
|
594
|
+
const response = await fetcher(requestTarget, createAttemptRequestInit());
|
|
595
|
+
|
|
596
|
+
if (!validateStatus(response.status)) {
|
|
597
|
+
throw Object.assign(new Error(`Request failed with status ${response.status}`), {
|
|
598
|
+
response,
|
|
599
|
+
status: response.status,
|
|
600
|
+
statusText: response.statusText,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return await parseResponse<TResponse>(response, parseAs);
|
|
605
|
+
} catch (error) {
|
|
606
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
607
|
+
|
|
608
|
+
// Abort errors should not be retried
|
|
609
|
+
if (
|
|
610
|
+
abortController.signal.aborted ||
|
|
611
|
+
isAbortDomException(normalizedError) ||
|
|
612
|
+
isTimeoutDomException(normalizedError)
|
|
613
|
+
) {
|
|
614
|
+
throw normalizeAbortLikeError(
|
|
615
|
+
abortController.signal.aborted ? abortController.signal.reason : normalizedError,
|
|
616
|
+
didTimeout
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
lastError = normalizedError;
|
|
621
|
+
|
|
622
|
+
const shouldRetry = retryConfig
|
|
623
|
+
? (retryConfig.retryOn ?? DEFAULT_RETRY_ON)(normalizedError, attempt)
|
|
624
|
+
: false;
|
|
625
|
+
|
|
626
|
+
if (!shouldRetry || attempt >= maxAttempts - 1) {
|
|
627
|
+
throw normalizedError;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
await sleepWithSignal(
|
|
631
|
+
resolveRetryDelay(retryConfig!.delay, attempt),
|
|
632
|
+
abortController.signal
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
throw lastError!;
|
|
638
|
+
} finally {
|
|
639
|
+
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
|
640
|
+
if (options.signal && externalAbortHandler) {
|
|
641
|
+
options.signal.removeEventListener('abort', externalAbortHandler);
|
|
642
|
+
}
|
|
643
|
+
if (currentAbortController === abortController) {
|
|
644
|
+
currentAbortController = null;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
435
647
|
}, options);
|
|
648
|
+
|
|
649
|
+
// Override abort with real abort logic
|
|
650
|
+
state.abort = (): void => {
|
|
651
|
+
if (currentAbortController) {
|
|
652
|
+
currentAbortController.abort(new DOMException('Request aborted', 'AbortError'));
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
return state;
|
|
436
657
|
};
|
|
437
658
|
|
|
438
659
|
/**
|