From 3bb9e119922455efc4e03f6b8a22283c082054c9 Mon Sep 17 00:00:00 2001 From: Esther Brunner Date: Mon, 15 Jul 2024 16:41:57 +0200 Subject: [PATCH] 0.7.1 - fix performance issue with derived signals - better type checking for attributeMap / contextMap - fix some types --- README.md | 4 +- cause-effect.js | 19 +- cause-effect.min.js | 2 +- eslint.config.js | 15 +- index.js | 29 +- index.min.js | 2 +- package-lock.json | 588 +++++++++++++++++++++++++------ package.json | 12 +- rollup.config.js | 2 +- src/cause-effect.d.ts | 18 +- src/cause-effect.ts | 29 +- src/lib/debug-element.d.ts | 20 +- src/lib/debug-element.ts | 26 +- src/lib/highlight-targets.d.ts | 4 +- src/lib/highlight-targets.ts | 4 +- src/lib/ui-component.d.ts | 12 +- src/lib/ui-component.ts | 10 +- src/lib/ui-ref.d.ts | 4 +- src/lib/ui-ref.ts | 4 +- src/lib/visibility-observer.d.ts | 6 +- src/lib/visibility-observer.ts | 6 +- src/ui-element.d.ts | 27 +- src/ui-element.ts | 64 +++- ui-component.js | 35 +- ui-component.min.js | 2 +- 25 files changed, 702 insertions(+), 242 deletions(-) diff --git a/README.md b/README.md index 14e1680..1d44d67 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ In the `connectedCallback()` you setup references to inner elements, add event l `UIElement` is fast. In fact, faster than any JavaScript framework. Only direct fine-grained DOM updates in vanilla JavaScript can beat its performance. But then, you have no loose coupling of components and need to parse attributes and track changes yourself. This tends to get tedious and messy rather quickly. `UIElement` provides a structured way to keep your components simple, consistent and self-contained. -`UIElement` is tiny. 914 bytes gzipped over the wire. And it has zero dependiences. If you want to understand how it works, you have to study the source code of [one single file](./index.js). +`UIElement` is tiny. 930 bytes gzipped over the wire. And it has zero dependiences. If you want to understand how it works, you have to study the source code of [one single file](./index.js). That's all. @@ -203,7 +203,7 @@ It consists of three functions: - `derive()` returns a getter function for the current value of the derived computation - `effect()` accepts a callback function to be exectuted when used signals change -Cause & Effect is possibly the simplest way to turn JavaScript into a reactive language – with just 312 bytes gezipped code. Unlike the [TC39 Signals Proposal](https://github.com/tc39/proposal-signals), Cause & Effect uses a much simpler approach, effectively just decorator functions around signal getters and setters. All work is done synchronously and eagerly. As long as your computed functions are pure and DOM side effects are kept to a minimum, this should pose no issues and is even faster than doing all the checks and memoization in the more sophisticated push-then-pull approach of the Signals Proposal. +Cause & Effect is possibly the simplest way to turn JavaScript into a reactive language – with just 340 bytes gezipped code. Unlike the [TC39 Signals Proposal](https://github.com/tc39/proposal-signals), Cause & Effect uses a much simpler approach, effectively just decorator functions around signal getters and setters. All work is done synchronously and eagerly. As long as your computed functions are pure and DOM side effects are kept to a minimum, this should pose no issues and is even faster than doing all the checks and memoization in the more sophisticated push-then-pull approach of the Signals Proposal. If you however want to use side-effects or expensive work in computed function or updating / rendering in the DOM in effects takes longer than an animation frame, you might encounter glitches. If that's what you are doing, you are better off with a mature, full-fledged JavaScript framework. diff --git a/cause-effect.js b/cause-effect.js index 3549827..329a893 100644 --- a/cause-effect.js +++ b/cause-effect.js @@ -21,7 +21,7 @@ const isState = (value) => isFunction(value) && isFunction(value.set); * Define a reactive state * * @since 0.1.0 - * @param {unknown} value - initial value of the state; may be a function for derived state + * @param {any} value - initial value of the state; may be a function for derived state * @returns {UIState} getter function for the current value with a `set` method to update the value */ const cause = (value) => { @@ -46,10 +46,19 @@ const cause = (value) => { * Create a derived state from an existing state * * @since 0.1.0 - * @param {() => unknown} fn - existing state to derive from - * @returns {() => unknown} derived state + * @param {() => any} fn - existing state to derive from + * @returns {() => any} derived state */ -const derive = (fn) => fn; +const derive = (fn) => { + const computed = () => { + const prev = active; + active = computed; + const value = fn(); + active = prev; + return value; + }; + return computed; +}; /** * Define what happens when a reactive state changes * @@ -66,7 +75,7 @@ const effect = (fn) => { targets.get(element).add(domFn); }); active = prev; - queueMicrotask(() => { + (targets.size || cleanup) && queueMicrotask(() => { for (const domFns of targets.values()) { for (const domFn of domFns) domFn(); diff --git a/cause-effect.min.js b/cause-effect.min.js index 3e75760..94209dc 100644 --- a/cause-effect.min.js +++ b/cause-effect.min.js @@ -1 +1 @@ -let t;const e=t=>"function"==typeof t,s=t=>e(t)&&e(t.set),o=o=>{const c=()=>(t&&c.effects.add(t),o);return c.effects=new Set,c.set=t=>{const f=o;if(o=e(t)&&!s(t)?t(f):t,!Object.is(o,f))for(const t of c.effects)t()},c},c=t=>t,f=s=>{const o=new Map,c=()=>{const f=t;t=c;const n=s(((t,e)=>{!o.has(t)&&o.set(t,new Set),o.get(t).add(e)}));t=f,queueMicrotask((()=>{for(const t of o.values())for(const e of t)e();e(n)&&n()}))};c.targets=o,c()};export{o as cause,c as derive,f as effect,e as isFunction,s as isState}; \ No newline at end of file +let t;const e=t=>"function"==typeof t,s=t=>e(t)&&e(t.set),o=o=>{const n=()=>(t&&n.effects.add(t),o);return n.effects=new Set,n.set=t=>{const c=o;if(o=e(t)&&!s(t)?t(c):t,!Object.is(o,c))for(const t of n.effects)t()},n},n=e=>{const s=()=>{const o=t;t=s;const n=e();return t=o,n};return s},c=s=>{const o=new Map,n=()=>{const c=t;t=n;const f=s(((t,e)=>{!o.has(t)&&o.set(t,new Set),o.get(t).add(e)}));t=c,(o.size||f)&&queueMicrotask((()=>{for(const t of o.values())for(const e of t)e();e(f)&&f()}))};n.targets=o,n()};export{o as cause,n as derive,c as effect,e as isFunction,s as isState}; \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index 74b9fc9..4c51836 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,9 +1,14 @@ // @ts-nocheck -import globals from "globals"; -import pluginJs from "@eslint/js"; - +import globals from 'globals'; +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; export default [ - {languageOptions: { globals: globals.browser }}, - pluginJs.configs.recommended, + { + languageOptions: { + globals: globals.browser + }, + }, + eslint.configs.recommended, + ...tseslint.configs.recommended, ]; \ No newline at end of file diff --git a/index.js b/index.js index aa4c1c6..248be59 100644 --- a/index.js +++ b/index.js @@ -21,7 +21,7 @@ const isState = (value) => isFunction(value) && isFunction(value.set); * Define a reactive state * * @since 0.1.0 - * @param {unknown} value - initial value of the state; may be a function for derived state + * @param {any} value - initial value of the state; may be a function for derived state * @returns {UIState} getter function for the current value with a `set` method to update the value */ const cause = (value) => { @@ -58,7 +58,7 @@ const effect = (fn) => { targets.get(element).add(domFn); }); active = prev; - queueMicrotask(() => { + (targets.size || cleanup) && queueMicrotask(() => { for (const domFns of targets.values()) { for (const domFn of domFns) domFn(); @@ -105,13 +105,24 @@ class ContextRequestEvent extends Event { } } +/* === Internal function === */ +/** + * Parse a attribute or context mapping value into a key-value pair + * + * @param {[PropertyKey, UIAttributeParser | UIContextParser] | UIAttributeParser | UIContextParser} value + * @param {PropertyKey} defaultKey + * @returns {[PropertyKey, UIAttributeParser | UIContextParser]} + */ +const getArrayMapping = (value, defaultKey) => { + return Array.isArray(value) ? value : [defaultKey, isFunction(value) ? value : (v) => v]; +}; /* === Default export === */ /** * Base class for reactive custom elements * * @class UIElement * @extends HTMLElement - * @type {UIElement} + * @type {IUIElement} */ class UIElement extends HTMLElement { /** @@ -154,11 +165,9 @@ class UIElement extends HTMLElement { attributeChangedCallback(name, old, value) { if (value !== old) { const input = this.attributeMap[name]; - const [key, parser] = Array.isArray(input) - ? input : - [name, input]; - this.set(key, isFunction(parser) - ? parser(value, this, old) + const [key, fn] = getArrayMapping(input, name); + this.set(key, isFunction(fn) + ? fn(value, this, old) : value); } } @@ -180,9 +189,7 @@ class UIElement extends HTMLElement { proto.consumedContexts?.forEach((context) => { const event = new ContextRequestEvent(context, (value) => { const input = this.contextMap[context]; - const [key, fn] = Array.isArray(input) - ? input - : [context, input]; + const [key, fn] = getArrayMapping(input, context); this.#states.set(key || context, isFunction(fn) ? fn(value, this) : value); diff --git a/index.min.js b/index.min.js index 914da0c..63f29f3 100644 --- a/index.min.js +++ b/index.min.js @@ -1 +1 @@ -let t;const e=t=>"function"==typeof t,s=t=>e(t)&&e(t.set),n=n=>{const c=()=>(t&&c.effects.add(t),n);return c.effects=new Set,c.set=t=>{const o=n;if(n=e(t)&&!s(t)?t(o):t,!Object.is(n,o))for(const t of c.effects)t()},c},c=s=>{const n=new Map,c=()=>{const o=t;t=c;const a=s(((t,e)=>{!n.has(t)&&n.set(t,new Set),n.get(t).add(e)}));t=o,queueMicrotask((()=>{for(const t of n.values())for(const e of t)e();e(a)&&a()}))};c.targets=n,c()},o="context-request";class a extends Event{context;callback;subscribe;constructor(t,e,s=!1){super(o,{bubbles:!0,composed:!0}),this.context=t,this.callback=e,this.subscribe=s}}class r extends HTMLElement{static define(t,e=customElements){try{e.get(t)||e.define(t,this)}catch(t){console.error(t)}}attributeMap={};contextMap={};#t=new Map;attributeChangedCallback(t,s,n){if(n!==s){const c=this.attributeMap[t],[o,a]=Array.isArray(c)?c:[t,c];this.set(o,e(a)?a(n,this,s):n)}}connectedCallback(){const t=Object.getPrototypeOf(this),s=t.providedContexts||[];s.length&&this.addEventListener(o,(t=>{const{context:n,callback:c}=t;s.includes(n)&&e(c)&&(t.stopPropagation(),c(this.#t.get(n)))})),setTimeout((()=>{t.consumedContexts?.forEach((t=>{const s=new a(t,(s=>{const n=this.contextMap[t],[c,o]=Array.isArray(n)?n:[t,n];this.#t.set(c||t,e(o)?o(s,this):s)}));this.dispatchEvent(s)}))}))}has(t){return this.#t.has(t)}get(t){const s=t=>e(t)?s(t()):t;return s(this.#t.get(t))}set(t,e,c=!0){if(this.#t.has(t)){const n=this.#t.get(t);c&&s(n)&&n.set(e)}else{const c=s(e)?e:n(e);this.#t.set(t,c)}}delete(t){return this.#t.delete(t)}async pass(t,s,c=customElements){await c.whenDefined(t.localName);for(const[c,o]of Object.entries(s))t.set(c,n(e(o)?o:this.#t.get(o)))}targets(t){const e=new Set;for(const s of this.#t.get(t).effects)for(const t of s.targets.keys())e.add(t);return e}}export{r as default,c as effect}; \ No newline at end of file +let t;const e=t=>"function"==typeof t,s=t=>e(t)&&e(t.set),n=n=>{const c=()=>(t&&c.effects.add(t),n);return c.effects=new Set,c.set=t=>{const o=n;if(n=e(t)&&!s(t)?t(o):t,!Object.is(n,o))for(const t of c.effects)t()},c},c=s=>{const n=new Map,c=()=>{const o=t;t=c;const a=s(((t,e)=>{!n.has(t)&&n.set(t,new Set),n.get(t).add(e)}));t=o,(n.size||a)&&queueMicrotask((()=>{for(const t of n.values())for(const e of t)e();e(a)&&a()}))};c.targets=n,c()},o="context-request";class a extends Event{context;callback;subscribe;constructor(t,e,s=!1){super(o,{bubbles:!0,composed:!0}),this.context=t,this.callback=e,this.subscribe=s}}const i=(t,s)=>Array.isArray(t)?t:[s,e(t)?t:t=>t];class r extends HTMLElement{static define(t,e=customElements){try{e.get(t)||e.define(t,this)}catch(t){console.error(t)}}attributeMap={};contextMap={};#t=new Map;attributeChangedCallback(t,s,n){if(n!==s){const c=this.attributeMap[t],[o,a]=i(c,t);this.set(o,e(a)?a(n,this,s):n)}}connectedCallback(){const t=Object.getPrototypeOf(this),s=t.providedContexts||[];s.length&&this.addEventListener(o,(t=>{const{context:n,callback:c}=t;s.includes(n)&&e(c)&&(t.stopPropagation(),c(this.#t.get(n)))})),setTimeout((()=>{t.consumedContexts?.forEach((t=>{const s=new a(t,(s=>{const n=this.contextMap[t],[c,o]=i(n,t);this.#t.set(c||t,e(o)?o(s,this):s)}));this.dispatchEvent(s)}))}))}has(t){return this.#t.has(t)}get(t){const s=t=>e(t)?s(t()):t;return s(this.#t.get(t))}set(t,e,c=!0){if(this.#t.has(t)){const n=this.#t.get(t);c&&s(n)&&n.set(e)}else{const c=s(e)?e:n(e);this.#t.set(t,c)}}delete(t){return this.#t.delete(t)}async pass(t,s,c=customElements){await c.whenDefined(t.localName);for(const[c,o]of Object.entries(s))t.set(c,n(e(o)?o:this.#t.get(o)))}targets(t){const e=new Set;for(const s of this.#t.get(t).effects)for(const t of s.targets.keys())e.add(t);return e}}export{r as default,c as effect}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b243cb1..43dc354 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,21 @@ { "name": "@efflore/ui-element", - "version": "0.7.0", + "version": "0.7.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@efflore/ui-element", - "version": "0.7.0", + "version": "0.7.1", "license": "MIT", "devDependencies": { - "@eslint/js": "^9.6.0", + "@eslint/js": "^9.7.0", "@esm-bundle/chai": "^4.3.4-fix.0", "@rollup/plugin-typescript": "^11.1.6", + "@types/eslint__js": "^8.42.3", "@web/test-runner": "^0.18.2", "@web/test-runner-playwright": "^0.11.0", - "eslint": "^9.6.0", + "eslint": "^8.56.0", "globals": "^15.8.0", "npm-run-all": "^4.1.5", "playwright": "^1.45.1", @@ -22,7 +23,8 @@ "sinon": "^18.0.0", "terser": "^5.31.2", "tslib": "^2.6.3", - "typescript": "^5.5.3" + "typescript": "^5.5.3", + "typescript-eslint": "^7.16.0" } }, "node_modules/@75lb/deep-merge": { @@ -107,51 +109,25 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/config-array": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.0.tgz", - "integrity": "sha512-A68TBu6/1mHHuc5YJL0U0VVeGNiklLAL6rRmhTCP2B5XjWLMnrX+HkO+IAXyHvks5cyyY1jjK5ITPQ1HGS2EVA==", - "dev": true, - "dependencies": { - "@eslint/object-schema": "^2.1.4", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", + "espree": "^9.6.0", + "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -159,37 +135,43 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, "engines": { - "node": ">=18" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/js": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.6.0.tgz", - "integrity": "sha512-D9B0/3vNg44ZeWbYMpBoXqNP4j6eQD5vNwIlGAuFRRzK/WtT/jvDQW3Bi9kkf3PMDMlM7Yi+73VLUsn5bJcl8A==", + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "node_modules/@eslint/js": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.7.0.tgz", + "integrity": "sha512-ChuWDQenef8OSFnvuxv0TCVxEwmu3+hPNKvM9B34qpM0rDRbjL8t5QkQeHHeAfsKQjuH9wS82WeCi1J/owatng==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -204,6 +186,21 @@ "@types/chai": "^4.2.12" } }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -217,18 +214,12 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", - "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", - "dev": true, - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", @@ -762,6 +753,25 @@ "integrity": "sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==", "dev": true }, + "node_modules/@types/eslint": { + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint__js": { + "version": "8.42.3", + "resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz", + "integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==", + "dev": true, + "dependencies": { + "@types/eslint": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -828,6 +838,12 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, "node_modules/@types/keygrip": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", @@ -938,6 +954,221 @@ "@types/node": "*" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz", + "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.16.0", + "@typescript-eslint/type-utils": "7.16.0", + "@typescript-eslint/utils": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz", + "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.16.0", + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/typescript-estree": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", + "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz", + "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.16.0", + "@typescript-eslint/utils": "7.16.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", + "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", + "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz", + "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.16.0", + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/typescript-estree": "7.16.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", + "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.16.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/@web/browser-logs": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-0.4.0.tgz", @@ -2244,6 +2475,18 @@ "node": ">=8" } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2458,37 +2701,41 @@ } }, "node_modules/eslint": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.6.0.tgz", - "integrity": "sha512-ElQkdLMEEqQNM9Njff+2Y4q2afHk7JpkPvrd7Xh7xefwgQynqPxwf55J7di9+MEibWUGdNjFF9ITG9Pck5M84w==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/config-array": "^0.17.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.6.0", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", + "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.0.1", - "eslint-visitor-keys": "^4.0.0", - "espree": "^10.1.0", - "esquery": "^1.5.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", + "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -2502,40 +2749,49 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://eslint.org/donate" + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-scope": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", - "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/eslint/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2623,6 +2879,21 @@ "node": ">=10.13.0" } }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2674,6 +2945,18 @@ "node": ">=8" } }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint/node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2690,17 +2973,17 @@ } }, "node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "dependencies": { - "acorn": "^8.12.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2952,15 +3235,15 @@ } }, "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "dependencies": { - "flat-cache": "^4.0.0" + "flat-cache": "^3.0.4" }, "engines": { - "node": ">=16.0.0" + "node": "^10.12.0 || >=12.0.0" } }, "node_modules/fill-range": { @@ -3004,16 +3287,17 @@ } }, "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.4" + "keyv": "^4.5.3", + "rimraf": "^3.0.2" }, "engines": { - "node": ">=16" + "node": "^10.12.0 || >=12.0.0" } }, "node_modules/flatted": { @@ -3054,6 +3338,12 @@ "node": ">=14.14" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3176,6 +3466,27 @@ "node": ">= 14" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -3253,6 +3564,12 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -3494,6 +3811,17 @@ "node": ">= 0.8.0" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -5384,6 +5712,22 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.1.tgz", @@ -6134,6 +6478,18 @@ "node": ">=12" } }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", @@ -6281,6 +6637,32 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.16.0.tgz", + "integrity": "sha512-kaVRivQjOzuoCXU6+hLnjo3/baxyzWVO5GrnExkFzETRYJKVHYkrJglOu2OCm8Hi9RPDWX1PTNNTpU5KRV0+RA==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "7.16.0", + "@typescript-eslint/parser": "7.16.0", + "@typescript-eslint/utils": "7.16.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/typical": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", diff --git a/package.json b/package.json index d55d57b..baabf48 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@efflore/ui-element", - "version": "0.7.0", + "version": "0.7.1", "description": "UIElement - minimal reactive framework based on Web Components", "main": "index.min.js", "types": "index.d.ts", @@ -10,7 +10,7 @@ "build:minify": "terser index.js -c -m -o index.min.js --module", "build:cause-effect": "terser cause-effect.js -c -m -o cause-effect.min.js --module", "build:ui-component": "terser ui-component.js -c -m -o ui-component.min.js --module", - "lint": "npx eslint index.js", + "lint": "npx eslint src/**/*.ts", "test": "run-s test:setup test:ci", "test:setup": "npx playwright install-deps", "test:ci": "web-test-runner \"test/*-test.html\" --node-resolve --playwright --browsers chromium firefox webkit", @@ -28,12 +28,13 @@ "license": "MIT", "type": "module", "devDependencies": { - "@eslint/js": "^9.6.0", + "@eslint/js": "^9.7.0", "@esm-bundle/chai": "^4.3.4-fix.0", "@rollup/plugin-typescript": "^11.1.6", + "@types/eslint__js": "^8.42.3", "@web/test-runner": "^0.18.2", "@web/test-runner-playwright": "^0.11.0", - "eslint": "^9.6.0", + "eslint": "^8.56.0", "globals": "^15.8.0", "npm-run-all": "^4.1.5", "playwright": "^1.45.1", @@ -41,6 +42,7 @@ "sinon": "^18.0.0", "terser": "^5.31.2", "tslib": "^2.6.3", - "typescript": "^5.5.3" + "typescript": "^5.5.3", + "typescript-eslint": "^7.16.0" } } diff --git a/rollup.config.js b/rollup.config.js index 533ba9b..4051677 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,8 +1,8 @@ import typescript from '@rollup/plugin-typescript'; export default { - input: 'index.ts', // input: 'src/cause-effect.ts', + input: 'index.ts', // input: 'src/lib/ui-component.ts', output: { dir: './', diff --git a/src/cause-effect.d.ts b/src/cause-effect.d.ts index 2bafe18..ece21b2 100644 --- a/src/cause-effect.d.ts +++ b/src/cause-effect.d.ts @@ -1,10 +1,10 @@ type UIDOMInstructionSet = Set<() => void>; type UIEffect = { (): void; - targets: Map; + targets?: Map; }; -type UIState = { - (): unknown; +type UIState = { + (): T; effects: Set; set(value: unknown): void; }; @@ -24,23 +24,23 @@ declare const isFunction: (fn: unknown) => fn is Function; * @param {unknown} value - variable to check if it is a reactive state * @returns {boolean} true if supplied parameter is a reactive state */ -declare const isState: (value: unknown) => value is UIState; +declare const isState: (value: unknown) => value is UIState; /** * Define a reactive state * * @since 0.1.0 - * @param {unknown} value - initial value of the state; may be a function for derived state + * @param {any} value - initial value of the state; may be a function for derived state * @returns {UIState} getter function for the current value with a `set` method to update the value */ -declare const cause: (value: unknown) => UIState; +declare const cause: (value: any) => UIState; /** * Create a derived state from an existing state * * @since 0.1.0 - * @param {() => unknown} fn - existing state to derive from - * @returns {() => unknown} derived state + * @param {() => any} fn - existing state to derive from + * @returns {() => any} derived state */ -declare const derive: (fn: () => unknown) => (() => unknown); +declare const derive: (fn: () => any) => (() => any); /** * Define what happens when a reactive state changes * diff --git a/src/cause-effect.ts b/src/cause-effect.ts index b74b6d4..13de3ec 100644 --- a/src/cause-effect.ts +++ b/src/cause-effect.ts @@ -4,11 +4,11 @@ type UIDOMInstructionSet = Set<() => void>; type UIEffect = { (): void; - targets: Map + targets?: Map }; -type UIState = { - (): unknown; +type UIState = { + (): T; effects: Set; set(value: unknown): void; } @@ -43,16 +43,16 @@ const isFunction = (fn: unknown): fn is Function => typeof fn === 'function'; * @param {unknown} value - variable to check if it is a reactive state * @returns {boolean} true if supplied parameter is a reactive state */ -const isState = (value: unknown): value is UIState => isFunction(value) && isFunction((value as UIState).set); +const isState = (value: unknown): value is UIState => isFunction(value) && isFunction((value as UIState).set); /** * Define a reactive state * * @since 0.1.0 - * @param {unknown} value - initial value of the state; may be a function for derived state + * @param {any} value - initial value of the state; may be a function for derived state * @returns {UIState} getter function for the current value with a `set` method to update the value */ -const cause = (value: unknown): UIState => { +const cause = (value: any): UIState => { const state = () => { // getter function active && state.effects.add(active); return value; @@ -75,10 +75,19 @@ const cause = (value: unknown): UIState => { * Create a derived state from an existing state * * @since 0.1.0 - * @param {() => unknown} fn - existing state to derive from - * @returns {() => unknown} derived state + * @param {() => any} fn - existing state to derive from + * @returns {() => any} derived state */ -const derive = (fn: () => unknown): (() => unknown) => fn; +const derive = (fn: () => any): (() => any) => { + const computed = () => { + const prev = active; + active = computed; + const value = fn(); + active = prev; + return value; + }; + return computed; +}; /** * Define what happens when a reactive state changes @@ -99,7 +108,7 @@ const effect = (fn: UIEffectCallback) => { targets.get(element).add(domFn); }); active = prev; - queueMicrotask(() => { + (targets.size || cleanup) && queueMicrotask(() => { for (const domFns of targets.values()) { for (const domFn of domFns) domFn(); diff --git a/src/lib/debug-element.d.ts b/src/lib/debug-element.d.ts index ebdeda0..38baaa5 100644 --- a/src/lib/debug-element.d.ts +++ b/src/lib/debug-element.d.ts @@ -1,4 +1,4 @@ -import UIElement from '../ui-element'; +import UIElement, { type IUIElement, type UIStateMap } from '../ui-element'; /** * @name UIElement DOM Utils * @version 0.7.0 @@ -35,22 +35,22 @@ declare class DebugElement extends UIElement { */ attributeChangedCallback(name: string, old: string | undefined, value: string | undefined): void; /** - * Wrap set() to log signal reads to the console + * Wrap get() to log signal reads to the console * * @since 0.5.0 - * @param {PropertyKey} key - * @returns {any} + * @param {PropertyKey} key - state to get + * @returns {unknown} - current value of the state */ - get(key: PropertyKey): any; + get(key: PropertyKey): unknown; /** * Wrap set() to log signal writes to the console * * @since 0.5.0 * @param {PropertyKey} key - state to be set - * @param {any} value - value to be set + * @param {unknown} value - value to be set * @param {boolean} [update=true] - whether to update the state */ - set(key: PropertyKey, value: any, update?: boolean): void; + set(key: PropertyKey, value: unknown, update?: boolean): void; /** * Wrap delete() to log signal deletions to the console * @@ -63,11 +63,11 @@ declare class DebugElement extends UIElement { * Wrap pass() to log passed signals to the console * * @since 0.7.0 - * @param {UIElement} element - UIElement to be passed to - * @param {import('../types.js').FxStateMap} states - states to be passed to the element + * @param {IUIElement} element - UIElement to be passed to + * @param {UIStateMap} states - states to be passed to the element * @param {CustomElementRegistry} [registry=customElements] - custom element registry */ - pass(element: UIElement, states: import('../ui-element').UIStateMap, registry?: CustomElementRegistry): Promise; + pass(element: IUIElement, states: UIStateMap, registry?: CustomElementRegistry): Promise; /** * Log messages in debug mode * diff --git a/src/lib/debug-element.ts b/src/lib/debug-element.ts index 816d2c7..4939451 100644 --- a/src/lib/debug-element.ts +++ b/src/lib/debug-element.ts @@ -1,4 +1,4 @@ -import UIElement from '../ui-element'; +import UIElement, { type IUIElement, type UIStateMap } from '../ui-element'; import { isDefined } from './ui-ref'; /** @@ -26,10 +26,10 @@ const elementName = (el: Element): string => * Return a string representation of a JavaScript variable * * @since 0.7.0 - * @param {any} value + * @param {unknown} value * @returns {string} */ -const valueString = (value: any): string => typeof value === 'string' +const valueString = (value: unknown): string => typeof value === 'string' ? `"${value}"` : typeof value === 'object' ? JSON.stringify(value) @@ -85,13 +85,13 @@ class DebugElement extends UIElement { } /** - * Wrap set() to log signal reads to the console + * Wrap get() to log signal reads to the console * * @since 0.5.0 - * @param {PropertyKey} key - * @returns {any} + * @param {PropertyKey} key - state to get + * @returns {unknown} - current value of the state */ - get(key: PropertyKey): any { + get(key: PropertyKey): unknown { const value = super.get(key); this.log(`Get current value of state ${valueString(key)} in ${elementName(this)} (value: ${valueString(value)}) and track its use in effect`); return value; @@ -102,12 +102,12 @@ class DebugElement extends UIElement { * * @since 0.5.0 * @param {PropertyKey} key - state to be set - * @param {any} value - value to be set + * @param {unknown} value - value to be set * @param {boolean} [update=true] - whether to update the state */ set( key: PropertyKey, - value: any, + value: unknown, update: boolean = true ): void { this.log(`Set ${update ? '' : 'default '}value of state ${valueString(key)} in ${elementName(this)} to ${valueString(value)} and trigger dependent effects`); @@ -130,12 +130,12 @@ class DebugElement extends UIElement { * Wrap pass() to log passed signals to the console * * @since 0.7.0 - * @param {UIElement} element - UIElement to be passed to - * @param {import('../types.js').FxStateMap} states - states to be passed to the element + * @param {IUIElement} element - UIElement to be passed to + * @param {UIStateMap} states - states to be passed to the element * @param {CustomElementRegistry} [registry=customElements] - custom element registry */ - async pass(element: UIElement, states: import('../ui-element').UIStateMap, registry: CustomElementRegistry = customElements) { - this.log(`Pass state(s) ${valueString(Object.keys(states))} to ${elementName(element)} from ${elementName(this)}`); + async pass(element: IUIElement, states: UIStateMap, registry: CustomElementRegistry = customElements) { + this.log(`Pass state(s) ${valueString(Object.keys(states))} to ${elementName(element as HTMLElement)} from ${elementName(this)}`); super.pass(element, states, registry); } diff --git a/src/lib/highlight-targets.d.ts b/src/lib/highlight-targets.d.ts index 84cc95c..833f202 100644 --- a/src/lib/highlight-targets.d.ts +++ b/src/lib/highlight-targets.d.ts @@ -1,4 +1,4 @@ -import type UIElement from "../ui-element"; +import type IUIElement from "../ui-element"; /** * Add event listeners to UIElement and sub-elements to auto-highlight targets when hovering or focusing on elements with given attribute * @@ -6,5 +6,5 @@ import type UIElement from "../ui-element"; * @param {UIElement} el - UIElement to apply event listeners to * @param {string} [className=EFFECT_CLASS] - CSS class to be added to highlighted targets */ -declare const highlightTargets: (el: UIElement, className?: string) => void; +declare const highlightTargets: (el: IUIElement, className?: string) => void; export { highlightTargets as default }; diff --git a/src/lib/highlight-targets.ts b/src/lib/highlight-targets.ts index d9e0d83..d0ac926 100644 --- a/src/lib/highlight-targets.ts +++ b/src/lib/highlight-targets.ts @@ -1,4 +1,4 @@ -import type UIElement from "../ui-element"; +import type IUIElement from "../ui-element"; import autoApply from "./auto-apply"; /* === Constants === */ @@ -16,7 +16,7 @@ const EFFECT_CLASS = 'ui-effect'; * @param {UIElement} el - UIElement to apply event listeners to * @param {string} [className=EFFECT_CLASS] - CSS class to be added to highlighted targets */ -const highlightTargets = (el: UIElement, className: string = EFFECT_CLASS) => { +const highlightTargets = (el: IUIElement, className: string = EFFECT_CLASS) => { [HOVER_SUFFIX, FOCUS_SUFFIX].forEach(suffix => { const [onOn, onOff] = suffix === HOVER_SUFFIX ? ['mouseenter','mouseleave'] diff --git a/src/lib/ui-component.d.ts b/src/lib/ui-component.d.ts index 19286f3..9032287 100644 --- a/src/lib/ui-component.d.ts +++ b/src/lib/ui-component.d.ts @@ -1,4 +1,4 @@ -import UIElement, { type UIAttributeMap } from "../ui-element"; +import UIElement, { type IUIElement, type UIAttributeMap } from "../ui-element"; import { effect } from "../cause-effect"; import { asBoolean, asInteger, asNumber, asString } from "./parse-attribute"; import uiRef from "./ui-ref"; @@ -8,11 +8,11 @@ import uiRef from "./ui-ref"; * @since 0.7.0 * @param {string} tag - custom element tag name * @param {UIAttributeMap} attributeMap - object of observed attributes and their corresponding state keys and parser functions - * @param {(connect: UIElement) => void} connect - callback to be called when the element is connected to the DOM - * @param {(disconnect: UIElement) => void} disconnect - callback to be called when the element is disconnected from the DOM + * @param {(connect: IUIElement) => void} connect - callback to be called when the element is connected to the DOM + * @param {(disconnect: IUIElement) => void} disconnect - callback to be called when the element is disconnected from the DOM * @returns {typeof FxComponent} - custom element class */ -declare const uiComponent: (tag: string, attributeMap: UIAttributeMap, connect: (connect: UIElement) => void, disconnect: (disconnect: UIElement) => void) => { +declare const uiComponent: (tag: string, attributeMap: UIAttributeMap, connect: (connect: IUIElement) => void, disconnect: (disconnect: IUIElement) => void) => { new (): { attributeMap: UIAttributeMap; connectedCallback(): void; @@ -22,9 +22,9 @@ declare const uiComponent: (tag: string, attributeMap: UIAttributeMap, connect: attributeChangedCallback(name: string, old: string | undefined, value: string | undefined): void; has(key: PropertyKey): boolean; get(key: PropertyKey): unknown; - set(key: PropertyKey, value: unknown | import("../cause-effect").UIState, update?: boolean): void; + set(key: PropertyKey, value: unknown | import("../cause-effect").UIState, update?: boolean): void; delete(key: PropertyKey): boolean; - pass(element: UIElement, states: import("../ui-element").UIStateMap, registry?: CustomElementRegistry): Promise; + pass(element: IUIElement, states: import("../ui-element").UIStateMap, registry?: CustomElementRegistry): Promise; targets(key: PropertyKey): Set; accessKey: string; readonly accessKeyLabel: string; diff --git a/src/lib/ui-component.ts b/src/lib/ui-component.ts index 536e5e9..af25089 100644 --- a/src/lib/ui-component.ts +++ b/src/lib/ui-component.ts @@ -1,4 +1,4 @@ -import UIElement, { type UIAttributeMap } from "../ui-element"; +import UIElement, { type IUIElement, type UIAttributeMap } from "../ui-element"; import { effect } from "../cause-effect"; import { asBoolean, asInteger, asNumber, asString } from "./parse-attribute"; import uiRef from "./ui-ref"; @@ -12,15 +12,15 @@ import { DEV_MODE } from "./debug-element"; * @since 0.7.0 * @param {string} tag - custom element tag name * @param {UIAttributeMap} attributeMap - object of observed attributes and their corresponding state keys and parser functions - * @param {(connect: UIElement) => void} connect - callback to be called when the element is connected to the DOM - * @param {(disconnect: UIElement) => void} disconnect - callback to be called when the element is disconnected from the DOM + * @param {(connect: IUIElement) => void} connect - callback to be called when the element is connected to the DOM + * @param {(disconnect: IUIElement) => void} disconnect - callback to be called when the element is disconnected from the DOM * @returns {typeof FxComponent} - custom element class */ const uiComponent = ( tag: string, attributeMap: UIAttributeMap = {}, - connect: (connect: UIElement) => void, - disconnect: (disconnect: UIElement) => void + connect: (connect: IUIElement) => void, + disconnect: (disconnect: IUIElement) => void ): typeof UIComponent => { const UIComponent = class extends UIElement { static observedAttributes = Object.keys(attributeMap); diff --git a/src/lib/ui-ref.d.ts b/src/lib/ui-ref.d.ts index c3356f8..23363b6 100644 --- a/src/lib/ui-ref.d.ts +++ b/src/lib/ui-ref.d.ts @@ -30,10 +30,10 @@ type UIRef = { /** * Check if a given variable is defined * - * @param {any} value - variable to check if it is defined + * @param {unknown} value - variable to check if it is defined * @returns {boolean} true if supplied parameter is defined */ -declare const isDefined: (value: any) => value is {} | null; +declare const isDefined: (value: unknown) => value is NonNullable; /** * Wrapper around a native DOM element for DOM manipulation * diff --git a/src/lib/ui-ref.ts b/src/lib/ui-ref.ts index deb9cae..e48d86c 100644 --- a/src/lib/ui-ref.ts +++ b/src/lib/ui-ref.ts @@ -63,10 +63,10 @@ const isStylable = (node: Element): node is HTMLElement | SVGElement | MathMLEle /** * Check if a given variable is defined * - * @param {any} value - variable to check if it is defined + * @param {unknown} value - variable to check if it is defined * @returns {boolean} true if supplied parameter is defined */ -const isDefined = (value: any): value is {} | null => typeof value !== 'undefined'; +const isDefined = (value: unknown): value is NonNullable => typeof value !== 'undefined'; /** * Wrapper around a native DOM element for DOM manipulation diff --git a/src/lib/visibility-observer.d.ts b/src/lib/visibility-observer.d.ts index 9c30402..a62d31c 100644 --- a/src/lib/visibility-observer.d.ts +++ b/src/lib/visibility-observer.d.ts @@ -1,12 +1,12 @@ -import type UIElement from '../ui-element'; +import type IUIElement from '../ui-element'; declare class VisibilityObserver { - observer: any; + observer: IntersectionObserver | void; /** * Set up IntersectionObserver for UIElement visibility state * * @param {UIElement} element */ - constructor(element: UIElement); + constructor(element: IUIElement); disconnect(): void; } export default VisibilityObserver; diff --git a/src/lib/visibility-observer.ts b/src/lib/visibility-observer.ts index c4eaaa8..6add44c 100644 --- a/src/lib/visibility-observer.ts +++ b/src/lib/visibility-observer.ts @@ -1,16 +1,16 @@ -import type UIElement from '../ui-element'; +import type IUIElement from '../ui-element'; const VISIBILITY_STATE = 'visible'; class VisibilityObserver { - observer = null; + observer: IntersectionObserver | void; /** * Set up IntersectionObserver for UIElement visibility state * * @param {UIElement} element */ - constructor(element: UIElement) { + constructor(element: IUIElement) { element.set(VISIBILITY_STATE, false); this.observer = new IntersectionObserver(([entry]) => { diff --git a/src/ui-element.d.ts b/src/ui-element.d.ts index 0c01e10..297af57 100644 --- a/src/ui-element.d.ts +++ b/src/ui-element.d.ts @@ -1,17 +1,30 @@ import { type UIState } from "./cause-effect"; -type UIAttributeParser = ((value: string | undefined, element: HTMLElement, old: string | undefined) => unknown) | undefined; +type UIAttributeParser = ((value: unknown | undefined, element?: HTMLElement, old?: unknown | undefined) => unknown) | undefined; type UIMappedAttributeParser = [PropertyKey, UIAttributeParser]; type UIAttributeMap = Record; -type UIStateMap = Record; -type UIContextParser = ((value: unknown | undefined, element: HTMLElement) => unknown) | undefined; +type UIStateMap = Record>; +type UIContextParser = ((value: unknown | undefined, element?: HTMLElement) => unknown) | undefined; type UIMappedContextParser = [PropertyKey, UIContextParser]; type UIContextMap = Record; +interface IUIElement extends Partial { + attributeMap: UIAttributeMap; + contextMap: UIContextMap; + connectedCallback(): void; + disconnectedCallback?(): void; + attributeChangedCallback(name: string, old: string | undefined, value: string | undefined): void; + has(key: PropertyKey): boolean; + get(key: PropertyKey): unknown; + set(key: PropertyKey, value: unknown | UIState, update?: boolean): void; + delete(key: PropertyKey): boolean; + pass(element: UIElement, states: UIStateMap, registry?: CustomElementRegistry): Promise; + targets(key: PropertyKey): Set; +} /** * Base class for reactive custom elements * * @class UIElement * @extends HTMLElement - * @type {UIElement} + * @type {IUIElement} */ declare class UIElement extends HTMLElement { #private; @@ -69,7 +82,7 @@ declare class UIElement extends HTMLElement { * @param {unknown} value - initial or new value; may be a function (gets old value as parameter) to be evaluated when value is retrieved * @param {boolean} [update=true] - if `true` (default), the state is updated; if `false`, just return existing value */ - set(key: PropertyKey, value: unknown | UIState, update?: boolean): void; + set(key: PropertyKey, value: unknown | UIState, update?: boolean): void; /** * Delete a state, also removing all effects dependent on the state * @@ -86,7 +99,7 @@ declare class UIElement extends HTMLElement { * @param {UIStateMap} states - object of states to be passed; target state keys as keys, source state keys or function as values * @param {CustomElementRegistry} [registry=customElements] - custom element registry to be used; defaults to `customElements` */ - pass(element: UIElement, states: UIStateMap, registry?: CustomElementRegistry): Promise; + pass(element: IUIElement, states: UIStateMap, registry?: CustomElementRegistry): Promise; /** * Return a Set of elements that have effects dependent on the given state * @@ -96,4 +109,4 @@ declare class UIElement extends HTMLElement { */ targets(key: PropertyKey): Set; } -export { type UIStateMap, type UIAttributeMap, type UIContextMap, UIElement as default }; +export { type IUIElement, type UIStateMap, type UIAttributeMap, type UIContextMap, UIElement as default }; diff --git a/src/ui-element.ts b/src/ui-element.ts index 6e01bfe..046e4ab 100644 --- a/src/ui-element.ts +++ b/src/ui-element.ts @@ -4,27 +4,57 @@ import { Context, CONTEXT_REQUEST, ContextRequestEvent } from "./context-request /* === Types === */ type UIAttributeParser = (( - value: string | undefined, - element: HTMLElement, - old: string | undefined + value: unknown | undefined, + element?: HTMLElement, + old?: unknown | undefined, ) => unknown) | undefined; type UIMappedAttributeParser = [PropertyKey, UIAttributeParser]; type UIAttributeMap = Record; -type UIStateMap = Record; +type UIStateMap = Record>; type UIContextParser = (( value: unknown | undefined, - element: HTMLElement + element?: HTMLElement ) => unknown) | undefined; type UIMappedContextParser = [PropertyKey, UIContextParser]; type UIContextMap = Record; -type UIStateContext = Context; +type UIStateContext = Context>; + +interface IUIElement extends Partial { + attributeMap: UIAttributeMap; + contextMap: UIContextMap; + connectedCallback(): void; + disconnectedCallback?(): void; + attributeChangedCallback(name: string, old: string | undefined, value: string | undefined): void; + has(key: PropertyKey): boolean; + get(key: PropertyKey): unknown; + set(key: PropertyKey, value: unknown | UIState, update?: boolean): void; + delete(key: PropertyKey): boolean; + pass(element: UIElement, states: UIStateMap, registry?: CustomElementRegistry): Promise; + targets(key: PropertyKey): Set; +} + +/* === Internal function === */ + +/** + * Parse a attribute or context mapping value into a key-value pair + * + * @param {[PropertyKey, UIAttributeParser | UIContextParser] | UIAttributeParser | UIContextParser} value + * @param {PropertyKey} defaultKey + * @returns {[PropertyKey, UIAttributeParser | UIContextParser]} + */ +const getArrayMapping = ( + value: [PropertyKey, UIAttributeParser | UIContextParser] | UIAttributeParser | UIContextParser, + defaultKey: PropertyKey +): [PropertyKey, UIAttributeParser | UIContextParser] => { + return Array.isArray(value) ? value : [defaultKey, isFunction(value) ? value : (v: unknown): unknown => v]; +}; /* === Default export === */ @@ -33,7 +63,7 @@ type UIStateContext = Context; * * @class UIElement * @extends HTMLElement - * @type {UIElement} + * @type {IUIElement} */ class UIElement extends HTMLElement { @@ -87,11 +117,9 @@ class UIElement extends HTMLElement { ): void { if (value !== old) { const input = this.attributeMap[name]; - const [key, parser] = Array.isArray(input) - ? input : - [name, input]; - this.set(key, isFunction(parser) - ? parser(value, this, old) + const [key, fn] = getArrayMapping(input, name); + this.set(key, isFunction(fn) + ? fn(value, this, old) : value ); } @@ -114,11 +142,9 @@ class UIElement extends HTMLElement { // context consumer setTimeout(() => { // wait for all custom elements to be defined proto.consumedContexts?.forEach((context: UIStateContext) => { - const event = new ContextRequestEvent(context, (value: UIState) => { + const event = new ContextRequestEvent(context, (value: UIState) => { const input = this.contextMap[context]; - const [key, fn] = Array.isArray(input) - ? input - : [context, input]; + const [key, fn] = getArrayMapping(input, context); this.#states.set(key || context, isFunction(fn) ? fn(value, this) : value @@ -164,7 +190,7 @@ class UIElement extends HTMLElement { */ set( key: PropertyKey, - value: unknown | UIState, + value: unknown | UIState, update: boolean = true ): void { if (this.#states.has(key)) { @@ -198,7 +224,7 @@ class UIElement extends HTMLElement { * @param {CustomElementRegistry} [registry=customElements] - custom element registry to be used; defaults to `customElements` */ async pass( - element: UIElement, + element: IUIElement, states: UIStateMap, registry: CustomElementRegistry = customElements ): Promise { @@ -228,4 +254,4 @@ class UIElement extends HTMLElement { } -export { type UIStateMap, type UIAttributeMap, type UIContextMap, UIElement as default }; \ No newline at end of file +export { type IUIElement, type UIStateMap, type UIAttributeMap, type UIContextMap, UIElement as default }; \ No newline at end of file diff --git a/ui-component.js b/ui-component.js index 34117dc..3cb0d8e 100644 --- a/ui-component.js +++ b/ui-component.js @@ -21,7 +21,7 @@ const isState = (value) => isFunction(value) && isFunction(value.set); * Define a reactive state * * @since 0.1.0 - * @param {unknown} value - initial value of the state; may be a function for derived state + * @param {any} value - initial value of the state; may be a function for derived state * @returns {UIState} getter function for the current value with a `set` method to update the value */ const cause = (value) => { @@ -58,7 +58,7 @@ const effect = (fn) => { targets.get(element).add(domFn); }); active = prev; - queueMicrotask(() => { + (targets.size || cleanup) && queueMicrotask(() => { for (const domFns of targets.values()) { for (const domFn of domFns) domFn(); @@ -105,13 +105,24 @@ class ContextRequestEvent extends Event { } } +/* === Internal function === */ +/** + * Parse a attribute or context mapping value into a key-value pair + * + * @param {[PropertyKey, UIAttributeParser | UIContextParser] | UIAttributeParser | UIContextParser} value + * @param {PropertyKey} defaultKey + * @returns {[PropertyKey, UIAttributeParser | UIContextParser]} + */ +const getArrayMapping = (value, defaultKey) => { + return Array.isArray(value) ? value : [defaultKey, isFunction(value) ? value : (v) => v]; +}; /* === Default export === */ /** * Base class for reactive custom elements * * @class UIElement * @extends HTMLElement - * @type {UIElement} + * @type {IUIElement} */ class UIElement extends HTMLElement { /** @@ -154,11 +165,9 @@ class UIElement extends HTMLElement { attributeChangedCallback(name, old, value) { if (value !== old) { const input = this.attributeMap[name]; - const [key, parser] = Array.isArray(input) - ? input : - [name, input]; - this.set(key, isFunction(parser) - ? parser(value, this, old) + const [key, fn] = getArrayMapping(input, name); + this.set(key, isFunction(fn) + ? fn(value, this, old) : value); } } @@ -180,9 +189,7 @@ class UIElement extends HTMLElement { proto.consumedContexts?.forEach((context) => { const event = new ContextRequestEvent(context, (value) => { const input = this.contextMap[context]; - const [key, fn] = Array.isArray(input) - ? input - : [context, input]; + const [key, fn] = getArrayMapping(input, context); this.#states.set(key || context, isFunction(fn) ? fn(value, this) : value); @@ -329,7 +336,7 @@ const isStylable = (node) => node instanceof HTMLElement /** * Check if a given variable is defined * - * @param {any} value - variable to check if it is defined + * @param {unknown} value - variable to check if it is defined * @returns {boolean} true if supplied parameter is defined */ const isDefined = (value) => typeof value !== 'undefined'; @@ -479,8 +486,8 @@ const highlightTargets = (el, className = EFFECT_CLASS) => { * @since 0.7.0 * @param {string} tag - custom element tag name * @param {UIAttributeMap} attributeMap - object of observed attributes and their corresponding state keys and parser functions - * @param {(connect: UIElement) => void} connect - callback to be called when the element is connected to the DOM - * @param {(disconnect: UIElement) => void} disconnect - callback to be called when the element is disconnected from the DOM + * @param {(connect: IUIElement) => void} connect - callback to be called when the element is connected to the DOM + * @param {(disconnect: IUIElement) => void} disconnect - callback to be called when the element is disconnected from the DOM * @returns {typeof FxComponent} - custom element class */ const uiComponent = (tag, attributeMap = {}, connect, disconnect) => { diff --git a/ui-component.min.js b/ui-component.min.js index 5e131f7..84b2540 100644 --- a/ui-component.min.js +++ b/ui-component.min.js @@ -1 +1 @@ -let t;const e=t=>"function"==typeof t,s=t=>e(t)&&e(t.set),o=o=>{const a=()=>(t&&a.effects.add(t),o);return a.effects=new Set,a.set=t=>{const c=o;if(o=e(t)&&!s(t)?t(c):t,!Object.is(o,c))for(const t of a.effects)t()},a},a=s=>{const o=new Map,a=()=>{const c=t;t=a;const n=s(((t,e)=>{!o.has(t)&&o.set(t,new Set),o.get(t).add(e)}));t=c,queueMicrotask((()=>{for(const t of o.values())for(const e of t)e();e(n)&&n()}))};a.targets=o,a()},c="context-request";class n extends Event{context;callback;subscribe;constructor(t,e,s=!1){super(c,{bubbles:!0,composed:!0}),this.context=t,this.callback=e,this.subscribe=s}}class r extends HTMLElement{static define(t,e=customElements){try{e.get(t)||e.define(t,this)}catch(t){console.error(t)}}attributeMap={};contextMap={};#t=new Map;attributeChangedCallback(t,s,o){if(o!==s){const a=this.attributeMap[t],[c,n]=Array.isArray(a)?a:[t,a];this.set(c,e(n)?n(o,this,s):o)}}connectedCallback(){const t=Object.getPrototypeOf(this),s=t.providedContexts||[];s.length&&this.addEventListener(c,(t=>{const{context:o,callback:a}=t;s.includes(o)&&e(a)&&(t.stopPropagation(),a(this.#t.get(o)))})),setTimeout((()=>{t.consumedContexts?.forEach((t=>{const s=new n(t,(s=>{const o=this.contextMap[t],[a,c]=Array.isArray(o)?o:[t,o];this.#t.set(a||t,e(c)?c(s,this):s)}));this.dispatchEvent(s)}))}))}has(t){return this.#t.has(t)}get(t){const s=t=>e(t)?s(t()):t;return s(this.#t.get(t))}set(t,e,a=!0){if(this.#t.has(t)){const o=this.#t.get(t);a&&s(o)&&o.set(e)}else{const a=s(e)?e:o(e);this.#t.set(t,a)}}delete(t){return this.#t.delete(t)}async pass(t,s,a=customElements){await a.whenDefined(t.localName);for(const[a,c]of Object.entries(s))t.set(a,o(e(c)?c:this.#t.get(c)))}targets(t){const e=new Set;for(const s of this.#t.get(t).effects)for(const t of s.targets.keys())e.add(t);return e}}const i=t=>"string"==typeof t,l=t=>parseInt(t,10),f=t=>parseFloat(t),u=t=>t,h="text",d="prop",g="attr",b="class",p="style",m=t=>void 0!==t,y=t=>{const e=t.shadowRoot||t,s=()=>t;var o;return s.first=t=>{const s=e.querySelector(t);return s&&y(s)},s.all=t=>Array.from(e.querySelectorAll(t)).map((t=>y(t))),s[h]={get:()=>t.textContent?.trim()||"",set:e=>{Array.from(t.childNodes).filter((t=>t.nodeType!==Node.COMMENT_NODE)).forEach((t=>t.remove())),t.append(document.createTextNode(e))}},s[d]={get:e=>t[e],set:(e,s)=>t[e]=s},s[g]={get:e=>t.getAttribute(e),set:(e,s)=>"boolean"==typeof s?t.toggleAttribute(e,s):m(s)?t.setAttribute(e,s):t.removeAttribute(e)},s[b]={get:e=>t.classList.contains(e),set:(e,s)=>t.classList.toggle(e,s)},((o=t)instanceof HTMLElement||o instanceof SVGElement||o instanceof MathMLElement)&&(s[p]={get:e=>t.style.getPropertyValue(e),set:(e,s)=>m(s)?t.style.setProperty(e,s):t.style.removeProperty(e)}),s},E=(t,e,s)=>{const o=`data-${t.localName}-${e}`,a=t=>{s(t,t.getAttribute(o)),t.removeAttribute(o)};t.hasAttribute(o)&&a(t);for(const e of t.querySelectorAll(`[${o}]`))a(e)},v="hover",A="focus",x=(t,e={},s,o)=>{const c=class extends r{static observedAttributes=Object.keys(e);attributeMap=e;connectedCallback(){var t;super.connectedCallback(),s&&s(this),t=this,[h,d,g,b,p].forEach((e=>{E(t,e,e===h?(s,o)=>{const c=o.trim(),n=y(s)[e],r=n.get();t.set(c,r,!1),a((e=>{if(t.has(c)){const o=t.get(c);e(s,(()=>n.set(m(o)?o:r)))}}))}:(s,o)=>{const c=(t,e)=>t.split(e).map((t=>t.trim()));c(o,";").forEach((o=>{const[n,r=n]=c(o,":"),i=y(s)[e];t.set(r,i.get(),!1),a((e=>{if(t.has(r)){const o=t.get(r);e(s,(()=>i.set(n,o)))}}))}))})})),((t,e="ui-effect")=>{[v,A].forEach((s=>{const[o,a]=s===v?["mouseenter","mouseleave"]:["focus","blur"];E(t,s,((s,c)=>{const n=c.trim(),r=(o,a)=>s.addEventListener(o,(()=>{for(const s of t.targets(n))s.classList.toggle(e,a)}));r(o,!0),r(a,!1)}))}))})(this)}disconnectedCallback(){o&&o(this)}};return c.define(t),c};export{r as UIElement,i as asBoolean,l as asInteger,f as asNumber,u as asString,x as default,a as effect,y as uiRef}; \ No newline at end of file +let t;const e=t=>"function"==typeof t,s=t=>e(t)&&e(t.set),o=o=>{const c=()=>(t&&c.effects.add(t),o);return c.effects=new Set,c.set=t=>{const n=o;if(o=e(t)&&!s(t)?t(n):t,!Object.is(o,n))for(const t of c.effects)t()},c},c=s=>{const o=new Map,c=()=>{const n=t;t=c;const a=s(((t,e)=>{!o.has(t)&&o.set(t,new Set),o.get(t).add(e)}));t=n,(o.size||a)&&queueMicrotask((()=>{for(const t of o.values())for(const e of t)e();e(a)&&a()}))};c.targets=o,c()},n="context-request";class a extends Event{context;callback;subscribe;constructor(t,e,s=!1){super(n,{bubbles:!0,composed:!0}),this.context=t,this.callback=e,this.subscribe=s}}const r=(t,s)=>Array.isArray(t)?t:[s,e(t)?t:t=>t];class i extends HTMLElement{static define(t,e=customElements){try{e.get(t)||e.define(t,this)}catch(t){console.error(t)}}attributeMap={};contextMap={};#t=new Map;attributeChangedCallback(t,s,o){if(o!==s){const c=this.attributeMap[t],[n,a]=r(c,t);this.set(n,e(a)?a(o,this,s):o)}}connectedCallback(){const t=Object.getPrototypeOf(this),s=t.providedContexts||[];s.length&&this.addEventListener(n,(t=>{const{context:o,callback:c}=t;s.includes(o)&&e(c)&&(t.stopPropagation(),c(this.#t.get(o)))})),setTimeout((()=>{t.consumedContexts?.forEach((t=>{const s=new a(t,(s=>{const o=this.contextMap[t],[c,n]=r(o,t);this.#t.set(c||t,e(n)?n(s,this):s)}));this.dispatchEvent(s)}))}))}has(t){return this.#t.has(t)}get(t){const s=t=>e(t)?s(t()):t;return s(this.#t.get(t))}set(t,e,c=!0){if(this.#t.has(t)){const o=this.#t.get(t);c&&s(o)&&o.set(e)}else{const c=s(e)?e:o(e);this.#t.set(t,c)}}delete(t){return this.#t.delete(t)}async pass(t,s,c=customElements){await c.whenDefined(t.localName);for(const[c,n]of Object.entries(s))t.set(c,o(e(n)?n:this.#t.get(n)))}targets(t){const e=new Set;for(const s of this.#t.get(t).effects)for(const t of s.targets.keys())e.add(t);return e}}const l=t=>"string"==typeof t,f=t=>parseInt(t,10),u=t=>parseFloat(t),h=t=>t,d="text",g="prop",b="attr",p="class",m="style",y=t=>void 0!==t,E=t=>{const e=t.shadowRoot||t,s=()=>t;var o;return s.first=t=>{const s=e.querySelector(t);return s&&E(s)},s.all=t=>Array.from(e.querySelectorAll(t)).map((t=>E(t))),s[d]={get:()=>t.textContent?.trim()||"",set:e=>{Array.from(t.childNodes).filter((t=>t.nodeType!==Node.COMMENT_NODE)).forEach((t=>t.remove())),t.append(document.createTextNode(e))}},s[g]={get:e=>t[e],set:(e,s)=>t[e]=s},s[b]={get:e=>t.getAttribute(e),set:(e,s)=>"boolean"==typeof s?t.toggleAttribute(e,s):y(s)?t.setAttribute(e,s):t.removeAttribute(e)},s[p]={get:e=>t.classList.contains(e),set:(e,s)=>t.classList.toggle(e,s)},((o=t)instanceof HTMLElement||o instanceof SVGElement||o instanceof MathMLElement)&&(s[m]={get:e=>t.style.getPropertyValue(e),set:(e,s)=>y(s)?t.style.setProperty(e,s):t.style.removeProperty(e)}),s},v=(t,e,s)=>{const o=`data-${t.localName}-${e}`,c=t=>{s(t,t.getAttribute(o)),t.removeAttribute(o)};t.hasAttribute(o)&&c(t);for(const e of t.querySelectorAll(`[${o}]`))c(e)},x="hover",A="focus",M=(t,e={},s,o)=>{const n=class extends i{static observedAttributes=Object.keys(e);attributeMap=e;connectedCallback(){var t;super.connectedCallback(),s&&s(this),t=this,[d,g,b,p,m].forEach((e=>{v(t,e,e===d?(s,o)=>{const n=o.trim(),a=E(s)[e],r=a.get();t.set(n,r,!1),c((e=>{if(t.has(n)){const o=t.get(n);e(s,(()=>a.set(y(o)?o:r)))}}))}:(s,o)=>{const n=(t,e)=>t.split(e).map((t=>t.trim()));n(o,";").forEach((o=>{const[a,r=a]=n(o,":"),i=E(s)[e];t.set(r,i.get(),!1),c((e=>{if(t.has(r)){const o=t.get(r);e(s,(()=>i.set(a,o)))}}))}))})})),((t,e="ui-effect")=>{[x,A].forEach((s=>{const[o,c]=s===x?["mouseenter","mouseleave"]:["focus","blur"];v(t,s,((s,n)=>{const a=n.trim(),r=(o,c)=>s.addEventListener(o,(()=>{for(const s of t.targets(a))s.classList.toggle(e,c)}));r(o,!0),r(c,!1)}))}))})(this)}disconnectedCallback(){o&&o(this)}};return n.define(t),n};export{i as UIElement,l as asBoolean,f as asInteger,u as asNumber,h as asString,M as default,c as effect,E as uiRef}; \ No newline at end of file