what-core 0.6.5 → 0.6.7

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/hooks.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ // What Framework - Hooks Type Definitions
2
+
3
+ import { Signal } from './index.js';
4
+
5
+ export function useState<T>(initial: T | (() => T)): [Signal<T>, (value: T | ((prev: T) => T)) => void];
6
+ export function useSignal<T>(initial: T): Signal<T>;
7
+ export function useComputed<T>(fn: () => T): Signal<T>;
8
+ export function useEffect(fn: () => void | (() => void), deps?: any[]): void;
9
+ export function useMemo<T>(fn: () => T, deps?: any[]): T;
10
+ export function useCallback<T extends (...args: any[]) => any>(fn: T, deps?: any[]): T;
11
+ export function useRef<T>(initial?: T): { current: T };
12
+ export function useContext<T>(context: { _defaultValue: T }): T;
13
+ export function createContext<T>(defaultValue?: T): { _defaultValue: T; Provider: (props: { value: T; children: any }) => any };
14
+ export function useReducer<S, A>(reducer: (state: S, action: A) => S, initialState: S, init?: (s: S) => S): [Signal<S>, (action: A) => void];
15
+ export function onMount(fn: () => void | (() => void)): void;
16
+ export function onCleanup(fn: () => void): void;
17
+
18
+ export interface Resource<T> {
19
+ (): T | undefined;
20
+ loading: Signal<boolean>;
21
+ error: Signal<Error | null>;
22
+ refetch: () => void;
23
+ mutate: (value: T) => void;
24
+ }
25
+ export function createResource<T>(fetcher: () => Promise<T>, options?: { initialValue?: T }): Resource<T>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-core",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "What Framework - The closest framework to vanilla JS",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -33,6 +33,7 @@
33
33
  "import": "./src/testing.js"
34
34
  },
35
35
  "./hooks": {
36
+ "types": "./hooks.d.ts",
36
37
  "import": "./src/hooks.js"
37
38
  },
38
39
  "./compiler": {
@@ -53,6 +54,7 @@
53
54
  "jsx-runtime.d.ts",
54
55
  "render.d.ts",
55
56
  "testing.d.ts",
57
+ "hooks.d.ts",
56
58
  "compiler.d.ts",
57
59
  "devtools.d.ts"
58
60
  ],
package/src/dom.js CHANGED
@@ -5,6 +5,7 @@
5
5
  import { effect, batch, untrack, signal, __DEV__, __devtools } from './reactive.js';
6
6
  import { reportError, _injectGetCurrentComponent, shallowEqual } from './components.js';
7
7
  import { _setComponentRef } from './helpers.js';
8
+ import { getDomAttributeName, isSafeUrlAttributeValue } from './security.js';
8
9
 
9
10
  // SVG elements that need namespace
10
11
  const SVG_ELEMENTS = new Set([
@@ -588,6 +589,14 @@ function applyProps(el, newProps, oldProps, isSvg) {
588
589
  }
589
590
 
590
591
  function setProp(el, key, value, isSvg) {
592
+ // Sanitize URL attributes before property assignment or setAttribute can commit them.
593
+ if (!isSafeUrlAttributeValue(key, value)) {
594
+ if (typeof console !== 'undefined') {
595
+ console.warn(`[what] Blocked unsafe URL in "${key}" attribute: ${value}`);
596
+ }
597
+ el.removeAttribute(getDomAttributeName(key));
598
+ return;
599
+ }
591
600
  // Reactive function props — wrap in effect for fine-grained updates
592
601
  if (typeof value === 'function' && !(key.startsWith('on') && key.length > 2) && key !== 'ref') {
593
602
  if (!el._propEffects) el._propEffects = {};
package/src/render.js CHANGED
@@ -5,6 +5,7 @@
5
5
  import { effect, untrack, createRoot, signal, __DEV__, __devtools } from './reactive.js';
6
6
  import { createDOM, disposeTree, getCurrentComponent, getComponentStack } from './dom.js';
7
7
  import { createWhatError, collectError } from './errors.js';
8
+ import { getDomAttributeName, isSafeUrlAttributeValue } from './security.js';
8
9
 
9
10
  export { effect, untrack };
10
11
 
@@ -22,20 +23,6 @@ export function _$createComponent(Component, props, children) {
22
23
  return createDOM({ tag: Component, props: props || {}, children: children || [], key: null, _vnode: true });
23
24
  }
24
25
 
25
- // --- URL Sanitization for DOM attributes ---
26
- // Rejects javascript:, data:, vbscript: protocols (case-insensitive, trimmed).
27
-
28
- const URL_ATTRS = new Set(['href', 'src', 'action', 'formaction', 'formAction']);
29
-
30
- function isSafeUrl(url) {
31
- if (typeof url !== 'string') return true; // non-string values are not URL-injection risks
32
- const normalized = url.trim().replace(/[\s\x00-\x1f]/g, '').toLowerCase();
33
- if (normalized.startsWith('javascript:')) return false;
34
- if (normalized.startsWith('data:')) return false;
35
- if (normalized.startsWith('vbscript:')) return false;
36
- return true;
37
- }
38
-
39
26
  // --- template(html) ---
40
27
  // Pre-parse HTML string into a <template> element. Returns a factory function
41
28
  // that clones the DOM tree via cloneNode(true) — 2-5x faster than createElement chains.
@@ -870,13 +857,12 @@ export function setProp(el, key, value) {
870
857
  if (key === 'key') return;
871
858
 
872
859
  // Sanitize URL attributes — reject dangerous protocols
873
- if (URL_ATTRS.has(key) || URL_ATTRS.has(key.toLowerCase())) {
874
- if (!isSafeUrl(value)) {
875
- if (typeof console !== 'undefined') {
876
- console.warn(`[what] Blocked unsafe URL in "${key}" attribute: ${value}`);
877
- }
878
- return;
860
+ if (!isSafeUrlAttributeValue(key, value)) {
861
+ if (typeof console !== 'undefined') {
862
+ console.warn(`[what] Blocked unsafe URL in "${key}" attribute: ${value}`);
879
863
  }
864
+ el.removeAttribute(getDomAttributeName(key));
865
+ return;
880
866
  }
881
867
 
882
868
  if (key === 'class' || key === 'className') {
@@ -0,0 +1,63 @@
1
+ // What Framework - DOM/SSR security helpers
2
+ // Shared by runtime render paths to keep URL-attribute handling consistent.
3
+
4
+ const URL_ATTRS = new Set([
5
+ 'href',
6
+ 'src',
7
+ 'action',
8
+ 'formaction',
9
+ 'poster',
10
+ 'cite',
11
+ 'background',
12
+ 'xlink:href',
13
+ ]);
14
+
15
+ const URL_LIST_ATTRS = new Set(['srcset']);
16
+
17
+ function normalizeAttrName(name) {
18
+ return String(name).toLowerCase();
19
+ }
20
+
21
+ function normalizeUrlForProtocolCheck(url) {
22
+ return String(url).trim().replace(/[\s\x00-\x1f\x7f]/g, '').toLowerCase();
23
+ }
24
+
25
+ export function isSafeUrlValue(value) {
26
+ if (typeof value !== 'string') return true;
27
+ const normalized = normalizeUrlForProtocolCheck(value);
28
+ return !(
29
+ normalized.startsWith('javascript:') ||
30
+ normalized.startsWith('data:') ||
31
+ normalized.startsWith('vbscript:')
32
+ );
33
+ }
34
+
35
+ function isSafeSrcsetValue(value) {
36
+ if (typeof value !== 'string') return true;
37
+ return value
38
+ .split(',')
39
+ .every(candidate => {
40
+ const url = candidate.trim().split(/\s+/, 1)[0] || '';
41
+ return url === '' || isSafeUrlValue(url);
42
+ });
43
+ }
44
+
45
+ export function isUrlAttribute(name) {
46
+ return URL_ATTRS.has(normalizeAttrName(name));
47
+ }
48
+
49
+ export function isUrlListAttribute(name) {
50
+ return URL_LIST_ATTRS.has(normalizeAttrName(name));
51
+ }
52
+
53
+ export function isSafeUrlAttributeValue(name, value) {
54
+ if (isUrlListAttribute(name)) return isSafeSrcsetValue(value);
55
+ if (isUrlAttribute(name)) return isSafeUrlValue(value);
56
+ return true;
57
+ }
58
+
59
+ export function getDomAttributeName(name) {
60
+ if (name === 'className') return 'class';
61
+ if (name === 'htmlFor') return 'for';
62
+ return normalizeAttrName(name) === 'formaction' ? 'formaction' : name;
63
+ }