@bquery/bquery 1.2.0 → 1.4.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/README.md +127 -27
- package/dist/batch-x7b2eZST.js +13 -0
- package/dist/batch-x7b2eZST.js.map +1 -0
- package/dist/component/component.d.ts +69 -0
- package/dist/component/component.d.ts.map +1 -0
- package/dist/component/html.d.ts +35 -0
- package/dist/component/html.d.ts.map +1 -0
- package/dist/component/index.d.ts +3 -126
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component/props.d.ts +18 -0
- package/dist/component/props.d.ts.map +1 -0
- package/dist/component/types.d.ts +77 -0
- package/dist/component/types.d.ts.map +1 -0
- package/dist/component.es.mjs +90 -59
- package/dist/component.es.mjs.map +1 -1
- package/dist/core/collection.d.ts +55 -3
- package/dist/core/collection.d.ts.map +1 -1
- package/dist/core/dom.d.ts +6 -0
- package/dist/core/dom.d.ts.map +1 -0
- package/dist/core/element.d.ts +31 -4
- package/dist/core/element.d.ts.map +1 -1
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/utils/array.d.ts +74 -0
- package/dist/core/utils/array.d.ts.map +1 -0
- package/dist/core/utils/function.d.ts +87 -0
- package/dist/core/utils/function.d.ts.map +1 -0
- package/dist/core/utils/index.d.ts +70 -0
- package/dist/core/utils/index.d.ts.map +1 -0
- package/dist/core/utils/misc.d.ts +63 -0
- package/dist/core/utils/misc.d.ts.map +1 -0
- package/dist/core/utils/number.d.ts +65 -0
- package/dist/core/utils/number.d.ts.map +1 -0
- package/dist/core/utils/object.d.ts +133 -0
- package/dist/core/utils/object.d.ts.map +1 -0
- package/dist/core/utils/string.d.ts +80 -0
- package/dist/core/utils/string.d.ts.map +1 -0
- package/dist/core/utils/type-guards.d.ts +79 -0
- package/dist/core/utils/type-guards.d.ts.map +1 -0
- package/dist/core-BhpuvPhy.js +170 -0
- package/dist/core-BhpuvPhy.js.map +1 -0
- package/dist/core.es.mjs +495 -489
- package/dist/core.es.mjs.map +1 -1
- package/dist/full.d.ts +2 -2
- package/dist/full.d.ts.map +1 -1
- package/dist/full.es.mjs +87 -64
- package/dist/full.es.mjs.map +1 -1
- package/dist/full.iife.js +2 -2
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +2 -2
- package/dist/full.umd.js.map +1 -1
- package/dist/index.es.mjs +138 -68
- package/dist/index.es.mjs.map +1 -1
- package/dist/motion/animate.d.ts +25 -0
- package/dist/motion/animate.d.ts.map +1 -0
- package/dist/motion/easing.d.ts +30 -0
- package/dist/motion/easing.d.ts.map +1 -0
- package/dist/motion/flip.d.ts +55 -0
- package/dist/motion/flip.d.ts.map +1 -0
- package/dist/motion/index.d.ts +11 -138
- package/dist/motion/index.d.ts.map +1 -1
- package/dist/motion/keyframes.d.ts +21 -0
- package/dist/motion/keyframes.d.ts.map +1 -0
- package/dist/motion/reduced-motion.d.ts +12 -0
- package/dist/motion/reduced-motion.d.ts.map +1 -0
- package/dist/motion/scroll.d.ts +15 -0
- package/dist/motion/scroll.d.ts.map +1 -0
- package/dist/motion/spring.d.ts +42 -0
- package/dist/motion/spring.d.ts.map +1 -0
- package/dist/motion/stagger.d.ts +22 -0
- package/dist/motion/stagger.d.ts.map +1 -0
- package/dist/motion/timeline.d.ts +21 -0
- package/dist/motion/timeline.d.ts.map +1 -0
- package/dist/motion/transition.d.ts +22 -0
- package/dist/motion/transition.d.ts.map +1 -0
- package/dist/motion/types.d.ts +182 -0
- package/dist/motion/types.d.ts.map +1 -0
- package/dist/motion.es.mjs +320 -61
- package/dist/motion.es.mjs.map +1 -1
- package/dist/persisted-DHoi3uEs.js +278 -0
- package/dist/persisted-DHoi3uEs.js.map +1 -0
- package/dist/platform/storage.d.ts.map +1 -1
- package/dist/platform.es.mjs +12 -7
- package/dist/platform.es.mjs.map +1 -1
- package/dist/reactive/batch.d.ts +13 -0
- package/dist/reactive/batch.d.ts.map +1 -0
- package/dist/reactive/computed.d.ts +50 -0
- package/dist/reactive/computed.d.ts.map +1 -0
- package/dist/reactive/core.d.ts +72 -0
- package/dist/reactive/core.d.ts.map +1 -0
- package/dist/reactive/effect.d.ts +15 -0
- package/dist/reactive/effect.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/internals.d.ts +42 -0
- package/dist/reactive/internals.d.ts.map +1 -0
- package/dist/reactive/linked.d.ts +36 -0
- package/dist/reactive/linked.d.ts.map +1 -0
- package/dist/reactive/persisted.d.ts +14 -0
- package/dist/reactive/persisted.d.ts.map +1 -0
- package/dist/reactive/readonly.d.ts +26 -0
- package/dist/reactive/readonly.d.ts.map +1 -0
- package/dist/reactive/signal.d.ts +13 -312
- package/dist/reactive/signal.d.ts.map +1 -1
- package/dist/reactive/type-guards.d.ts +20 -0
- package/dist/reactive/type-guards.d.ts.map +1 -0
- package/dist/reactive/untrack.d.ts +29 -0
- package/dist/reactive/untrack.d.ts.map +1 -0
- package/dist/reactive/watch.d.ts +42 -0
- package/dist/reactive/watch.d.ts.map +1 -0
- package/dist/reactive.es.mjs +30 -163
- package/dist/reactive.es.mjs.map +1 -1
- package/dist/router/index.d.ts +6 -252
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/links.d.ts +44 -0
- package/dist/router/links.d.ts.map +1 -0
- package/dist/router/match.d.ts +20 -0
- package/dist/router/match.d.ts.map +1 -0
- package/dist/router/navigation.d.ts +45 -0
- package/dist/router/navigation.d.ts.map +1 -0
- package/dist/router/query.d.ts +16 -0
- package/dist/router/query.d.ts.map +1 -0
- package/dist/router/router.d.ts +34 -0
- package/dist/router/router.d.ts.map +1 -0
- package/dist/router/state.d.ts +27 -0
- package/dist/router/state.d.ts.map +1 -0
- package/dist/router/types.d.ts +88 -0
- package/dist/router/types.d.ts.map +1 -0
- package/dist/router/utils.d.ts +65 -0
- package/dist/router/utils.d.ts.map +1 -0
- package/dist/router.es.mjs +168 -132
- package/dist/router.es.mjs.map +1 -1
- package/dist/sanitize-Cxvxa-DX.js +283 -0
- package/dist/sanitize-Cxvxa-DX.js.map +1 -0
- package/dist/security/constants.d.ts +42 -0
- package/dist/security/constants.d.ts.map +1 -0
- package/dist/security/csp.d.ts +24 -0
- package/dist/security/csp.d.ts.map +1 -0
- package/dist/security/index.d.ts +4 -2
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/sanitize-core.d.ts +13 -0
- package/dist/security/sanitize-core.d.ts.map +1 -0
- package/dist/security/sanitize.d.ts +5 -57
- package/dist/security/sanitize.d.ts.map +1 -1
- package/dist/security/trusted-types.d.ts +25 -0
- package/dist/security/trusted-types.d.ts.map +1 -0
- package/dist/security/types.d.ts +36 -0
- package/dist/security/types.d.ts.map +1 -0
- package/dist/security.es.mjs +50 -277
- package/dist/security.es.mjs.map +1 -1
- package/dist/store/create-store.d.ts +15 -0
- package/dist/store/create-store.d.ts.map +1 -0
- package/dist/store/define-store.d.ts +28 -0
- package/dist/store/define-store.d.ts.map +1 -0
- package/dist/store/devtools.d.ts +22 -0
- package/dist/store/devtools.d.ts.map +1 -0
- package/dist/store/index.d.ts +10 -286
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/mapping.d.ts +28 -0
- package/dist/store/mapping.d.ts.map +1 -0
- package/dist/store/persisted.d.ts +13 -0
- package/dist/store/persisted.d.ts.map +1 -0
- package/dist/store/plugins.d.ts +13 -0
- package/dist/store/plugins.d.ts.map +1 -0
- package/dist/store/registry.d.ts +28 -0
- package/dist/store/registry.d.ts.map +1 -0
- package/dist/store/types.d.ts +71 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/utils.d.ts +28 -0
- package/dist/store/utils.d.ts.map +1 -0
- package/dist/store/watch.d.ts +23 -0
- package/dist/store/watch.d.ts.map +1 -0
- package/dist/store.es.mjs +22 -224
- package/dist/store.es.mjs.map +1 -1
- package/dist/type-guards-BdKlYYlS.js +32 -0
- package/dist/type-guards-BdKlYYlS.js.map +1 -0
- package/dist/untrack-DNnnqdlR.js +6 -0
- package/dist/untrack-DNnnqdlR.js.map +1 -0
- package/dist/view/directives/bind.d.ts +7 -0
- package/dist/view/directives/bind.d.ts.map +1 -0
- package/dist/view/directives/class.d.ts +8 -0
- package/dist/view/directives/class.d.ts.map +1 -0
- package/dist/view/directives/for.d.ts +23 -0
- package/dist/view/directives/for.d.ts.map +1 -0
- package/dist/view/directives/html.d.ts +7 -0
- package/dist/view/directives/html.d.ts.map +1 -0
- package/dist/view/directives/if.d.ts +7 -0
- package/dist/view/directives/if.d.ts.map +1 -0
- package/dist/view/directives/index.d.ts +12 -0
- package/dist/view/directives/index.d.ts.map +1 -0
- package/dist/view/directives/model.d.ts +7 -0
- package/dist/view/directives/model.d.ts.map +1 -0
- package/dist/view/directives/on.d.ts +7 -0
- package/dist/view/directives/on.d.ts.map +1 -0
- package/dist/view/directives/ref.d.ts +7 -0
- package/dist/view/directives/ref.d.ts.map +1 -0
- package/dist/view/directives/show.d.ts +7 -0
- package/dist/view/directives/show.d.ts.map +1 -0
- package/dist/view/directives/style.d.ts +7 -0
- package/dist/view/directives/style.d.ts.map +1 -0
- package/dist/view/directives/text.d.ts +7 -0
- package/dist/view/directives/text.d.ts.map +1 -0
- package/dist/view/evaluate.d.ts +43 -0
- package/dist/view/evaluate.d.ts.map +1 -0
- package/dist/view/index.d.ts +3 -93
- package/dist/view/index.d.ts.map +1 -1
- package/dist/view/mount.d.ts +69 -0
- package/dist/view/mount.d.ts.map +1 -0
- package/dist/view/process.d.ts +26 -0
- package/dist/view/process.d.ts.map +1 -0
- package/dist/view/types.d.ts +36 -0
- package/dist/view/types.d.ts.map +1 -0
- package/dist/view.es.mjs +358 -251
- package/dist/view.es.mjs.map +1 -1
- package/dist/watch-DXXv3iAI.js +58 -0
- package/dist/watch-DXXv3iAI.js.map +1 -0
- package/package.json +14 -14
- package/src/component/component.ts +289 -0
- package/src/component/html.ts +53 -0
- package/src/component/index.ts +40 -414
- package/src/component/props.ts +116 -0
- package/src/component/types.ts +85 -0
- package/src/core/collection.ts +181 -7
- package/src/core/dom.ts +38 -0
- package/src/core/element.ts +59 -25
- package/src/core/index.ts +48 -4
- package/src/core/utils/array.ts +102 -0
- package/src/core/utils/function.ts +151 -0
- package/src/core/utils/index.ts +83 -0
- package/src/core/utils/misc.ts +82 -0
- package/src/core/utils/number.ts +78 -0
- package/src/core/utils/object.ts +206 -0
- package/src/core/utils/string.ts +112 -0
- package/src/core/utils/type-guards.ts +112 -0
- package/src/full.ts +187 -150
- package/src/index.ts +36 -36
- package/src/motion/animate.ts +113 -0
- package/src/motion/easing.ts +40 -0
- package/src/motion/flip.ts +176 -0
- package/src/motion/index.ts +41 -358
- package/src/motion/keyframes.ts +46 -0
- package/src/motion/reduced-motion.ts +17 -0
- package/src/motion/scroll.ts +57 -0
- package/src/motion/spring.ts +150 -0
- package/src/motion/stagger.ts +43 -0
- package/src/motion/timeline.ts +246 -0
- package/src/motion/transition.ts +51 -0
- package/src/motion/types.ts +198 -0
- package/src/platform/storage.ts +215 -208
- package/src/reactive/batch.ts +22 -0
- package/src/reactive/computed.ts +92 -0
- package/src/reactive/core.ts +114 -0
- package/src/reactive/effect.ts +54 -0
- package/src/reactive/index.ts +23 -22
- package/src/reactive/internals.ts +122 -0
- package/src/reactive/linked.ts +56 -0
- package/src/reactive/persisted.ts +74 -0
- package/src/reactive/readonly.ts +35 -0
- package/src/reactive/signal.ts +20 -520
- package/src/reactive/type-guards.ts +22 -0
- package/src/reactive/untrack.ts +31 -0
- package/src/reactive/watch.ts +73 -0
- package/src/router/index.ts +41 -718
- package/src/router/links.ts +130 -0
- package/src/router/match.ts +106 -0
- package/src/router/navigation.ts +71 -0
- package/src/router/query.ts +35 -0
- package/src/router/router.ts +211 -0
- package/src/router/state.ts +46 -0
- package/src/router/types.ts +93 -0
- package/src/router/utils.ts +116 -0
- package/src/security/constants.ts +209 -0
- package/src/security/csp.ts +77 -0
- package/src/security/index.ts +4 -12
- package/src/security/sanitize-core.ts +364 -0
- package/src/security/sanitize.ts +66 -625
- package/src/security/trusted-types.ts +69 -0
- package/src/security/types.ts +40 -0
- package/src/store/create-store.ts +329 -0
- package/src/store/define-store.ts +48 -0
- package/src/store/devtools.ts +45 -0
- package/src/store/index.ts +22 -848
- package/src/store/mapping.ts +73 -0
- package/src/store/persisted.ts +61 -0
- package/src/store/plugins.ts +32 -0
- package/src/store/registry.ts +51 -0
- package/src/store/types.ts +94 -0
- package/src/store/utils.ts +141 -0
- package/src/store/watch.ts +52 -0
- package/src/view/directives/bind.ts +23 -0
- package/src/view/directives/class.ts +70 -0
- package/src/view/directives/for.ts +275 -0
- package/src/view/directives/html.ts +19 -0
- package/src/view/directives/if.ts +30 -0
- package/src/view/directives/index.ts +11 -0
- package/src/view/directives/model.ts +56 -0
- package/src/view/directives/on.ts +41 -0
- package/src/view/directives/ref.ts +41 -0
- package/src/view/directives/show.ts +26 -0
- package/src/view/directives/style.ts +47 -0
- package/src/view/directives/text.ts +15 -0
- package/src/view/evaluate.ts +290 -0
- package/src/view/index.ts +112 -1041
- package/src/view/mount.ts +200 -0
- package/src/view/process.ts +92 -0
- package/src/view/types.ts +44 -0
- package/dist/core/utils.d.ts +0 -313
- package/dist/core/utils.d.ts.map +0 -1
- package/src/core/utils.ts +0 -444
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trusted Types helpers for CSP compatibility.
|
|
3
|
+
*
|
|
4
|
+
* @module bquery/security
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { POLICY_NAME } from './constants';
|
|
8
|
+
import { sanitizeHtmlCore } from './sanitize-core';
|
|
9
|
+
import type { TrustedHTML, TrustedTypePolicy, TrustedTypesWindow } from './types';
|
|
10
|
+
|
|
11
|
+
/** Cached Trusted Types policy */
|
|
12
|
+
let cachedPolicy: TrustedTypePolicy | null = null;
|
|
13
|
+
|
|
14
|
+
/** Whether policy initialization has been attempted (to avoid retry spam) */
|
|
15
|
+
let policyInitAttempted = false;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if Trusted Types API is available.
|
|
19
|
+
* @returns True if Trusted Types are supported
|
|
20
|
+
*/
|
|
21
|
+
export const isTrustedTypesSupported = (): boolean => {
|
|
22
|
+
return (
|
|
23
|
+
typeof window !== 'undefined' &&
|
|
24
|
+
typeof (window as TrustedTypesWindow).trustedTypes !== 'undefined'
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get or create the bQuery Trusted Types policy.
|
|
30
|
+
* @returns The Trusted Types policy or null if unsupported
|
|
31
|
+
*/
|
|
32
|
+
export const getTrustedTypesPolicy = (): TrustedTypePolicy | null => {
|
|
33
|
+
if (cachedPolicy) return cachedPolicy;
|
|
34
|
+
if (policyInitAttempted) return null;
|
|
35
|
+
|
|
36
|
+
if (typeof window === 'undefined') return null;
|
|
37
|
+
|
|
38
|
+
const win = window as TrustedTypesWindow;
|
|
39
|
+
if (!win.trustedTypes) return null;
|
|
40
|
+
|
|
41
|
+
policyInitAttempted = true;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
cachedPolicy = win.trustedTypes.createPolicy(POLICY_NAME, {
|
|
45
|
+
createHTML: (input: string) => sanitizeHtmlCore(input),
|
|
46
|
+
});
|
|
47
|
+
return cachedPolicy;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// Policy may already exist or be blocked by CSP
|
|
50
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
51
|
+
console.warn(`bQuery: Could not create Trusted Types policy "${POLICY_NAME}": ${errorMessage}`);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a Trusted HTML value for use with Trusted Types-enabled sites.
|
|
58
|
+
* Falls back to regular string when Trusted Types are unavailable.
|
|
59
|
+
*
|
|
60
|
+
* @param html - The HTML string to wrap
|
|
61
|
+
* @returns Trusted HTML value or sanitized string
|
|
62
|
+
*/
|
|
63
|
+
export const createTrustedHtml = (html: string): TrustedHTML | string => {
|
|
64
|
+
const policy = getTrustedTypesPolicy();
|
|
65
|
+
if (policy) {
|
|
66
|
+
return policy.createHTML(html);
|
|
67
|
+
}
|
|
68
|
+
return sanitizeHtmlCore(html);
|
|
69
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security types for sanitization, CSP compatibility, and Trusted Types.
|
|
3
|
+
*
|
|
4
|
+
* @module bquery/security
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sanitizer configuration options.
|
|
9
|
+
*/
|
|
10
|
+
export interface SanitizeOptions {
|
|
11
|
+
/** Allow these additional tags (default: none) */
|
|
12
|
+
allowTags?: string[];
|
|
13
|
+
/** Allow these additional attributes (default: none) */
|
|
14
|
+
allowAttributes?: string[];
|
|
15
|
+
/** Allow data-* attributes (default: true) */
|
|
16
|
+
allowDataAttributes?: boolean;
|
|
17
|
+
/** Strip all tags and return plain text (default: false) */
|
|
18
|
+
stripAllTags?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Window interface extended with Trusted Types */
|
|
22
|
+
export interface TrustedTypesWindow extends Window {
|
|
23
|
+
trustedTypes?: {
|
|
24
|
+
createPolicy: (
|
|
25
|
+
name: string,
|
|
26
|
+
rules: { createHTML?: (input: string) => string }
|
|
27
|
+
) => TrustedTypePolicy;
|
|
28
|
+
isHTML?: (value: unknown) => boolean;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Trusted Types policy interface */
|
|
33
|
+
export interface TrustedTypePolicy {
|
|
34
|
+
createHTML: (input: string) => TrustedHTML;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Trusted HTML type placeholder for environments without Trusted Types */
|
|
38
|
+
export interface TrustedHTML {
|
|
39
|
+
toString(): string;
|
|
40
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store creation logic.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
batch,
|
|
7
|
+
computed,
|
|
8
|
+
signal,
|
|
9
|
+
untrack,
|
|
10
|
+
type ReadonlySignal,
|
|
11
|
+
type Signal,
|
|
12
|
+
} from '../reactive/index';
|
|
13
|
+
import { notifyDevtoolsStateChange, registerDevtoolsStore } from './devtools';
|
|
14
|
+
import { applyPlugins } from './plugins';
|
|
15
|
+
import { getStore, hasStore, registerStore } from './registry';
|
|
16
|
+
import type { Getters, Store, StoreDefinition, StoreSubscriber } from './types';
|
|
17
|
+
import { deepClone, detectNestedMutations, isDev } from './utils';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates a reactive store with state, getters, and actions.
|
|
21
|
+
*
|
|
22
|
+
* @template S - State type
|
|
23
|
+
* @template G - Getters type
|
|
24
|
+
* @template A - Actions type
|
|
25
|
+
* @param definition - Store definition
|
|
26
|
+
* @returns The reactive store instance
|
|
27
|
+
*/
|
|
28
|
+
export const createStore = <
|
|
29
|
+
S extends Record<string, unknown>,
|
|
30
|
+
G extends Record<string, unknown> = Record<string, never>,
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
A extends Record<string, (...args: any[]) => any> = Record<string, never>,
|
|
33
|
+
>(
|
|
34
|
+
definition: StoreDefinition<S, G, A>
|
|
35
|
+
): Store<S, G, A> => {
|
|
36
|
+
const { id, state: stateFactory, getters = {} as Getters<S, G>, actions = {} as A } = definition;
|
|
37
|
+
|
|
38
|
+
// Check for duplicate store IDs
|
|
39
|
+
if (hasStore(id)) {
|
|
40
|
+
console.warn(`bQuery store: Store "${id}" already exists. Returning existing instance.`);
|
|
41
|
+
return getStore(id) as Store<S, G, A>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Create initial state
|
|
45
|
+
const initialState = stateFactory();
|
|
46
|
+
|
|
47
|
+
// Create signals for each state property
|
|
48
|
+
const stateSignals = new Map<keyof S, Signal<unknown>>();
|
|
49
|
+
for (const key of Object.keys(initialState) as Array<keyof S>) {
|
|
50
|
+
stateSignals.set(key, signal(initialState[key]));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Subscribers for $subscribe
|
|
54
|
+
const subscribers: Array<StoreSubscriber<S>> = [];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Gets the current state.
|
|
58
|
+
*
|
|
59
|
+
* For subscriber notifications (where a plain object snapshot is needed),
|
|
60
|
+
* this creates a shallow copy. For internal reads, use stateProxy directly.
|
|
61
|
+
*
|
|
62
|
+
* **Note:** Returns a shallow snapshot. Nested object mutations will NOT
|
|
63
|
+
* trigger reactive updates. This differs from frameworks like Pinia that
|
|
64
|
+
* use deep reactivity. To update nested state, replace the entire object.
|
|
65
|
+
*
|
|
66
|
+
* Uses `untrack()` to prevent accidental dependency tracking when called
|
|
67
|
+
* from within reactive contexts (e.g., `effect()` or `computed()`).
|
|
68
|
+
*
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
const getCurrentState = (): S =>
|
|
72
|
+
untrack(() => {
|
|
73
|
+
return { ...stateProxy };
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Notifies subscribers of state changes.
|
|
78
|
+
* Short-circuits if there are no subscribers and devtools aren't active
|
|
79
|
+
* to avoid unnecessary snapshot overhead.
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
const notifySubscribers = (): void => {
|
|
83
|
+
// Early return if no subscribers and no devtools hook
|
|
84
|
+
const hasDevtools =
|
|
85
|
+
typeof window !== 'undefined' &&
|
|
86
|
+
typeof window.__BQUERY_DEVTOOLS__?.onStateChange === 'function';
|
|
87
|
+
if (subscribers.length === 0 && !hasDevtools) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const currentState = getCurrentState();
|
|
92
|
+
for (const callback of subscribers) {
|
|
93
|
+
callback(currentState);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
notifyDevtoolsStateChange(id, currentState);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Cached state proxy that lazily reads signal values.
|
|
101
|
+
* Uses a Proxy to avoid creating new objects on each access.
|
|
102
|
+
*
|
|
103
|
+
* **Note:** This returns a shallow snapshot of the state. Nested object
|
|
104
|
+
* mutations will NOT trigger reactive updates. For nested reactivity,
|
|
105
|
+
* replace the entire object or use signals for nested properties.
|
|
106
|
+
*
|
|
107
|
+
* @internal
|
|
108
|
+
*/
|
|
109
|
+
const stateProxy = new Proxy({} as S, {
|
|
110
|
+
get: (_, prop: string | symbol) => {
|
|
111
|
+
const key = prop as keyof S;
|
|
112
|
+
if (stateSignals.has(key)) {
|
|
113
|
+
return stateSignals.get(key)!.value;
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
},
|
|
117
|
+
ownKeys: () => Array.from(stateSignals.keys()) as string[],
|
|
118
|
+
getOwnPropertyDescriptor: (_, prop) => {
|
|
119
|
+
if (stateSignals.has(prop as keyof S)) {
|
|
120
|
+
return { enumerable: true, configurable: true };
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
},
|
|
124
|
+
has: (_, prop) => stateSignals.has(prop as keyof S),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Create computed getters
|
|
128
|
+
const getterComputed = new Map<keyof G, ReadonlySignal<unknown>>();
|
|
129
|
+
|
|
130
|
+
// Build the store proxy
|
|
131
|
+
const store = {} as Store<S, G, A>;
|
|
132
|
+
|
|
133
|
+
// Define state properties with getters/setters
|
|
134
|
+
for (const key of Object.keys(initialState) as Array<keyof S>) {
|
|
135
|
+
Object.defineProperty(store, key, {
|
|
136
|
+
get: () => stateSignals.get(key)!.value,
|
|
137
|
+
set: (value: unknown) => {
|
|
138
|
+
stateSignals.get(key)!.value = value;
|
|
139
|
+
notifySubscribers();
|
|
140
|
+
},
|
|
141
|
+
enumerable: true,
|
|
142
|
+
configurable: false,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Define getters as computed properties
|
|
147
|
+
for (const key of Object.keys(getters) as Array<keyof G>) {
|
|
148
|
+
const getterFn = getters[key];
|
|
149
|
+
|
|
150
|
+
// Create computed that reads from state signals via proxy (more efficient)
|
|
151
|
+
const computedGetter = computed(() => {
|
|
152
|
+
const state = stateProxy;
|
|
153
|
+
// For getter dependencies, pass a proxy that reads from computed getters
|
|
154
|
+
const getterProxy = new Proxy({} as G, {
|
|
155
|
+
get: (_, prop: string | symbol) => {
|
|
156
|
+
const propKey = prop as keyof G;
|
|
157
|
+
if (getterComputed.has(propKey)) {
|
|
158
|
+
return getterComputed.get(propKey)!.value;
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
return getterFn(state, getterProxy);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
getterComputed.set(key, computedGetter as unknown as ReadonlySignal<unknown>);
|
|
167
|
+
|
|
168
|
+
Object.defineProperty(store, key, {
|
|
169
|
+
get: () => computedGetter.value,
|
|
170
|
+
enumerable: true,
|
|
171
|
+
configurable: false,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Bind actions to the store context
|
|
176
|
+
for (const key of Object.keys(actions) as Array<keyof A>) {
|
|
177
|
+
const actionFn = actions[key];
|
|
178
|
+
|
|
179
|
+
// Wrap action to enable 'this' binding
|
|
180
|
+
(store as Record<string, unknown>)[key as string] = function (...args: unknown[]) {
|
|
181
|
+
// Create a context that allows 'this.property' access
|
|
182
|
+
const context = new Proxy(store, {
|
|
183
|
+
get: (target, prop) => {
|
|
184
|
+
if (typeof prop === 'string' && stateSignals.has(prop as keyof S)) {
|
|
185
|
+
return stateSignals.get(prop as keyof S)!.value;
|
|
186
|
+
}
|
|
187
|
+
return (target as Record<string, unknown>)[prop as string];
|
|
188
|
+
},
|
|
189
|
+
set: (target, prop, value) => {
|
|
190
|
+
if (typeof prop === 'string' && stateSignals.has(prop as keyof S)) {
|
|
191
|
+
stateSignals.get(prop as keyof S)!.value = value;
|
|
192
|
+
notifySubscribers();
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
// Allow non-state property assignments (e.g., temporary variables in actions)
|
|
196
|
+
// by delegating to the target object rather than returning false
|
|
197
|
+
return Reflect.set(target, prop, value);
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return actionFn.apply(context, args);
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Add store utility methods
|
|
206
|
+
Object.defineProperties(store, {
|
|
207
|
+
$id: {
|
|
208
|
+
value: id,
|
|
209
|
+
writable: false,
|
|
210
|
+
enumerable: false,
|
|
211
|
+
},
|
|
212
|
+
$reset: {
|
|
213
|
+
value: () => {
|
|
214
|
+
const fresh = stateFactory();
|
|
215
|
+
batch(() => {
|
|
216
|
+
for (const [key, sig] of stateSignals) {
|
|
217
|
+
sig.value = fresh[key];
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
notifySubscribers();
|
|
221
|
+
},
|
|
222
|
+
writable: false,
|
|
223
|
+
enumerable: false,
|
|
224
|
+
},
|
|
225
|
+
$subscribe: {
|
|
226
|
+
value: (callback: StoreSubscriber<S>) => {
|
|
227
|
+
subscribers.push(callback);
|
|
228
|
+
return () => {
|
|
229
|
+
const index = subscribers.indexOf(callback);
|
|
230
|
+
if (index > -1) subscribers.splice(index, 1);
|
|
231
|
+
};
|
|
232
|
+
},
|
|
233
|
+
writable: false,
|
|
234
|
+
enumerable: false,
|
|
235
|
+
},
|
|
236
|
+
$patch: {
|
|
237
|
+
value: (partial: Partial<S> | ((state: S) => void)) => {
|
|
238
|
+
batch(() => {
|
|
239
|
+
if (typeof partial === 'function') {
|
|
240
|
+
// Capture state before mutation for nested mutation detection
|
|
241
|
+
const stateBefore = isDev ? deepClone(getCurrentState()) : null;
|
|
242
|
+
const signalValuesBefore = isDev
|
|
243
|
+
? new Map(Array.from(stateSignals.entries()).map(([k, s]) => [k, s.value]))
|
|
244
|
+
: null;
|
|
245
|
+
|
|
246
|
+
// Mutation function
|
|
247
|
+
const state = getCurrentState();
|
|
248
|
+
partial(state);
|
|
249
|
+
|
|
250
|
+
// Detect nested mutations in development mode
|
|
251
|
+
if (isDev && stateBefore && signalValuesBefore) {
|
|
252
|
+
const mutatedKeys = detectNestedMutations(stateBefore, state, signalValuesBefore);
|
|
253
|
+
if (mutatedKeys.length > 0) {
|
|
254
|
+
console.warn(
|
|
255
|
+
`[bQuery store "${id}"] Nested mutation detected in $patch() for keys: ${mutatedKeys
|
|
256
|
+
.map(String)
|
|
257
|
+
.join(', ')}.\n` +
|
|
258
|
+
'Nested object mutations do not trigger reactive updates because the store uses shallow reactivity.\n' +
|
|
259
|
+
'To fix this, either:\n' +
|
|
260
|
+
' 1. Replace the entire object: state.user = { ...state.user, name: "New" }\n' +
|
|
261
|
+
' 2. Use $patchDeep() for automatic deep cloning\n' +
|
|
262
|
+
'See: https://bquery.dev/guide/store#deep-reactivity'
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const [key, value] of Object.entries(state) as Array<[keyof S, unknown]>) {
|
|
268
|
+
if (stateSignals.has(key)) {
|
|
269
|
+
stateSignals.get(key)!.value = value;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
// Partial object
|
|
274
|
+
for (const [key, value] of Object.entries(partial) as Array<[keyof S, unknown]>) {
|
|
275
|
+
if (stateSignals.has(key)) {
|
|
276
|
+
stateSignals.get(key)!.value = value;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
notifySubscribers();
|
|
282
|
+
},
|
|
283
|
+
writable: false,
|
|
284
|
+
enumerable: false,
|
|
285
|
+
},
|
|
286
|
+
$patchDeep: {
|
|
287
|
+
value: (partial: Partial<S> | ((state: S) => void)) => {
|
|
288
|
+
batch(() => {
|
|
289
|
+
if (typeof partial === 'function') {
|
|
290
|
+
// Deep clone state before mutation to ensure new references
|
|
291
|
+
const state = deepClone(getCurrentState());
|
|
292
|
+
partial(state);
|
|
293
|
+
|
|
294
|
+
for (const [key, value] of Object.entries(state) as Array<[keyof S, unknown]>) {
|
|
295
|
+
if (stateSignals.has(key)) {
|
|
296
|
+
stateSignals.get(key)!.value = value;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
// Deep clone each value in partial to ensure new references
|
|
301
|
+
for (const [key, value] of Object.entries(partial) as Array<[keyof S, unknown]>) {
|
|
302
|
+
if (stateSignals.has(key)) {
|
|
303
|
+
stateSignals.get(key)!.value = deepClone(value);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
notifySubscribers();
|
|
309
|
+
},
|
|
310
|
+
writable: false,
|
|
311
|
+
enumerable: false,
|
|
312
|
+
},
|
|
313
|
+
$state: {
|
|
314
|
+
get: () => getCurrentState(),
|
|
315
|
+
enumerable: false,
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Register store
|
|
320
|
+
registerStore(id, store);
|
|
321
|
+
|
|
322
|
+
// Apply plugins
|
|
323
|
+
applyPlugins(store, definition);
|
|
324
|
+
|
|
325
|
+
// Notify devtools
|
|
326
|
+
registerDevtoolsStore(id, store);
|
|
327
|
+
|
|
328
|
+
return store;
|
|
329
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store factory helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createStore } from './create-store';
|
|
6
|
+
import { getStore, hasStore } from './registry';
|
|
7
|
+
import type { Store, StoreDefinition } from './types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates a store factory that returns the store instance.
|
|
11
|
+
*
|
|
12
|
+
* The store is lazily created on first call and cached in the global store
|
|
13
|
+
* registry. Subsequent calls return the same instance. After calling
|
|
14
|
+
* `destroyStore(id)`, the next factory call will create a fresh store.
|
|
15
|
+
*
|
|
16
|
+
* @param id - Store identifier
|
|
17
|
+
* @param definition - Store definition without id
|
|
18
|
+
* @returns A function that returns the store instance
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const useCounter = defineStore('counter', {
|
|
23
|
+
* state: () => ({ count: 0 }),
|
|
24
|
+
* actions: { increment() { this.count++; } },
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* const counter = useCounter();
|
|
28
|
+
* counter.increment();
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export const defineStore = <
|
|
32
|
+
S extends Record<string, unknown>,
|
|
33
|
+
G extends Record<string, unknown> = Record<string, never>,
|
|
34
|
+
A extends Record<string, (...args: unknown[]) => unknown> = Record<string, never>,
|
|
35
|
+
>(
|
|
36
|
+
id: string,
|
|
37
|
+
definition: Omit<StoreDefinition<S, G, A>, 'id'>
|
|
38
|
+
): (() => Store<S, G, A>) => {
|
|
39
|
+
// Check registry first to avoid noisy warnings from createStore()
|
|
40
|
+
// when the factory is called multiple times (intended usage pattern).
|
|
41
|
+
// createStore() only called when store doesn't exist or was destroyed.
|
|
42
|
+
return () => {
|
|
43
|
+
if (hasStore(id)) {
|
|
44
|
+
return getStore(id) as Store<S, G, A>;
|
|
45
|
+
}
|
|
46
|
+
return createStore({ id, ...definition });
|
|
47
|
+
};
|
|
48
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devtools integration for stores.
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare global {
|
|
7
|
+
interface Window {
|
|
8
|
+
__BQUERY_DEVTOOLS__?: {
|
|
9
|
+
stores: Map<string, unknown>;
|
|
10
|
+
onStoreCreated?: (id: string, store: unknown) => void;
|
|
11
|
+
onStateChange?: (id: string, state: unknown) => void;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type DevtoolsHook = {
|
|
17
|
+
stores: Map<string, unknown>;
|
|
18
|
+
onStoreCreated?: (id: string, store: unknown) => void;
|
|
19
|
+
onStateChange?: (id: string, state: unknown) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const ensureDevtools = (): DevtoolsHook | undefined => {
|
|
23
|
+
if (typeof window === 'undefined') return undefined;
|
|
24
|
+
if (!window.__BQUERY_DEVTOOLS__) {
|
|
25
|
+
window.__BQUERY_DEVTOOLS__ = { stores: new Map() };
|
|
26
|
+
}
|
|
27
|
+
return window.__BQUERY_DEVTOOLS__;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const registerDevtoolsStore = (id: string, store: unknown): void => {
|
|
31
|
+
const devtools = ensureDevtools();
|
|
32
|
+
if (!devtools) return;
|
|
33
|
+
devtools.stores.set(id, store);
|
|
34
|
+
devtools.onStoreCreated?.(id, store);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const unregisterDevtoolsStore = (id: string): void => {
|
|
38
|
+
if (typeof window === 'undefined' || !window.__BQUERY_DEVTOOLS__) return;
|
|
39
|
+
window.__BQUERY_DEVTOOLS__.stores.delete(id);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const notifyDevtoolsStateChange = (id: string, state: unknown): void => {
|
|
43
|
+
if (typeof window === 'undefined') return;
|
|
44
|
+
window.__BQUERY_DEVTOOLS__?.onStateChange?.(id, state);
|
|
45
|
+
};
|