diff --git a/.prettierrc b/.prettierrc index 10fae09d63..3c05b6fb9c 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,5 @@ { - "useTabs": true, + "useTabs": false, "singleQuote": true, "trailingComma": "none", "printWidth": 100, diff --git a/package-lock.json b/package-lock.json index b105746c9a..677155f73b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,60 +9,106 @@ "version": "0.0.1", "dependencies": { "blockly": "^12.5.1", + "idb": "^8.0.3", "socket.io": "^4.8.3", "socket.io-client": "^4.8.3", "uuid": "^14.0.0", - "yaml": "^2.8.4" + "yaml": "^2.9.0" }, "devDependencies": { "@sveltejs/adapter-auto": "^7.0.1", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.59.0", - "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@sveltejs/kit": "^2.59.1", + "@sveltejs/vite-plugin-svelte": "^7.1.2", "@types/uuid": "^11.0.0", - "@typescript-eslint/eslint-plugin": "^8.59.0", - "@typescript-eslint/parser": "^8.59.2", + "@typescript-eslint/eslint-plugin": "^8.59.3", + "@typescript-eslint/parser": "^8.59.3", "eslint": "^10.3.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.17.1", + "fake-indexeddb": "^6.2.5", "globals": "^17.6.0", + "jsdom": "^29.1.1", "prettier": "^3.8.3", - "prettier-plugin-svelte": "^3.5.1", + "prettier-plugin-svelte": "^3.5.2", "svelte": "^5.55.5", - "svelte-check": "^4.4.7", - "svelte-preprocess": "^6.0.3", + "svelte-check": "^4.4.8", "tslib": "^2.8.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.59.2", - "vite": "^8.0.10" + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.3", + "vite": "^8.0.12", + "vitest": "^4.1.6" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/@asamuzakjp/css-color": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.7.tgz", - "integrity": "sha512-Ok5fYhtwdyJQmU1PpEv6Si7Y+A4cYb8yNM9oiIJC9TzXPMuN9fvdonKJqcnz9TbFqV6bQ8z0giRq0iaOpGZV2g==", + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, "license": "MIT", "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "lru-cache": "^10.4.3" + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" } }, "node_modules/@csstools/color-helpers": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", - "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, "funding": [ { "type": "github", @@ -75,13 +121,14 @@ ], "license": "MIT-0", "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, "node_modules/@csstools/css-calc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.3.tgz", - "integrity": "sha512-XBG3talrhid44BY1x3MHzUx/aTG8+x/Zi57M4aTKK9RFB4aLlF3TTSzfzn8nWVHWL3FgAXAxmupmDd6VWww+pw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", + "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", + "dev": true, "funding": [ { "type": "github", @@ -94,17 +141,18 @@ ], "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-color-parser": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.9.tgz", - "integrity": "sha512-wILs5Zk7BU86UArYBJTPy/FMPPKVKHMj1ycCEyf3VUptol0JNRLFU/BZsJ4aiIHJEbSLiizzRrw8Pc1uAEDrXw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", + "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", + "dev": true, "funding": [ { "type": "github", @@ -117,21 +165,22 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^5.0.2", - "@csstools/css-calc": "^2.1.3" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.0" }, "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", - "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, "funding": [ { "type": "github", @@ -143,17 +192,19 @@ } ], "license": "MIT", + "peer": true, "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@csstools/css-tokenizer": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", - "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", + "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", + "dev": true, "funding": [ { "type": "github", @@ -164,32 +215,35 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } } }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "peer": true, + "engines": { + "node": ">=20.19.0" } }, "node_modules/@emnapi/wasi-threads": { @@ -297,39 +351,60 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18.18.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -337,6 +412,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -346,9 +422,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -360,9 +436,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -371,9 +447,9 @@ } }, "node_modules/@jridgewell/remapping": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.4.tgz", - "integrity": "sha512-aG+WvAz17rhbzhKNkSeMLgbkPPK82ovXdONvmucbGhUqcroRFLLVhoGAk4xEI17gHpXgNX3sr0/B1ybRUsbEWw==", + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -382,10 +458,11 @@ } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -398,10 +475,11 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -427,9 +505,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", - "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "version": "0.129.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz", + "integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==", "dev": true, "license": "MIT", "funding": { @@ -437,15 +515,16 @@ } }, "node_modules/@polka/url": { - "version": "1.0.0-next.28", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", - "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", - "dev": true + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz", + "integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==", "cpu": [ "arm64" ], @@ -460,9 +539,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz", + "integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==", "cpu": [ "arm64" ], @@ -477,9 +556,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", - "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz", + "integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==", "cpu": [ "x64" ], @@ -494,9 +573,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", - "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz", + "integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==", "cpu": [ "x64" ], @@ -511,9 +590,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", - "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz", + "integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==", "cpu": [ "arm" ], @@ -528,9 +607,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz", + "integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==", "cpu": [ "arm64" ], @@ -545,9 +624,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", - "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz", + "integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==", "cpu": [ "arm64" ], @@ -562,9 +641,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz", + "integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==", "cpu": [ "ppc64" ], @@ -579,9 +658,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz", + "integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==", "cpu": [ "s390x" ], @@ -596,9 +675,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz", + "integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==", "cpu": [ "x64" ], @@ -613,9 +692,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", - "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz", + "integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==", "cpu": [ "x64" ], @@ -630,9 +709,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz", + "integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==", "cpu": [ "arm64" ], @@ -647,9 +726,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", - "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz", + "integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==", "cpu": [ "wasm32" ], @@ -666,9 +745,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", - "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz", + "integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==", "cpu": [ "arm64" ], @@ -683,9 +762,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", - "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz", + "integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==", "cpu": [ "x64" ], @@ -700,28 +779,29 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", - "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz", + "integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==", "dev": true, "license": "MIT" }, "node_modules/@socket.io/component-emitter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", - "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" }, "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, "license": "MIT" }, "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", - "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -749,11 +829,12 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.59.0", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.59.0.tgz", - "integrity": "sha512-WeJaGKvDf3uVQB4bnDHhM+BXCY34LC1v0HiPqnSpvNkjB54r8DAUP1rpk73s+5zprIirEKtUcVfgh6+fPODjzQ==", + "version": "2.59.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.59.1.tgz", + "integrity": "sha512-d8OON70AphLdDesuTIl//M2O6fRTIicX8aYv8vhCiYEhTTI2OboKqey0Hu1A4VFhqwgqtq0vKDmPFGkw8kKmgw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", @@ -791,11 +872,12 @@ } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.0.0.tgz", - "integrity": "sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.1.2.tgz", + "integrity": "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "deepmerge": "^4.3.1", "magic-string": "^0.30.21", @@ -811,9 +893,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "dev": true, "license": "MIT", "optional": true, @@ -821,20 +903,40 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", "dependencies": { "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -843,9 +945,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -857,12 +959,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.0.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz", - "integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==", + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", + "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~7.21.0" } }, "node_modules/@types/trusted-types": { @@ -883,18 +985,27 @@ "uuid": "*" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", - "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", + "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/type-utils": "8.59.2", - "@typescript-eslint/utils": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/type-utils": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -907,32 +1018,23 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.59.2", + "@typescript-eslint/parser": "^8.59.3", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/@typescript-eslint/parser": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz", - "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz", + "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3" }, "engines": { @@ -947,33 +1049,15 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/parser/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/@typescript-eslint/project-service": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", - "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz", + "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.2", - "@typescript-eslint/types": "^8.59.2", + "@typescript-eslint/tsconfig-utils": "^8.59.3", + "@typescript-eslint/types": "^8.59.3", "debug": "^4.4.3" }, "engines": { @@ -987,33 +1071,15 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/project-service/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", - "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", + "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2" + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1024,9 +1090,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", - "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz", + "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==", "dev": true, "license": "MIT", "engines": { @@ -1041,15 +1107,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", - "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz", + "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/utils": "8.59.2", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -1065,30 +1131,13 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/@typescript-eslint/types": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz", - "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz", + "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1098,16 +1147,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", - "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz", + "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.59.2", - "@typescript-eslint/tsconfig-utils": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", + "@typescript-eslint/project-service": "8.59.3", + "@typescript-eslint/tsconfig-utils": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1125,35 +1174,17 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/@typescript-eslint/utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz", - "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz", + "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2" + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1168,13 +1199,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", - "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz", + "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/types": "8.59.3", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1198,53 +1229,169 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "node_modules/@vitest/expect": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", + "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", + "dev": true, + "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, - "engines": { - "node": ">= 0.6" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "node_modules/@vitest/mocker": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", + "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@vitest/spy": "4.1.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" }, - "engines": { - "node": ">=0.4.0" + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/@vitest/pretty-format": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", + "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", "dev": true, - "peerDependencies": { + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", + "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.6", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", + "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "@vitest/utils": "4.1.6", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", + "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", + "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "license": "MIT", "engines": { "node": ">= 14" } }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1268,23 +1415,55 @@ "node": ">= 0.4" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">= 0.4" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", "engines": { "node": "^4.5.0 || >= 5.9" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/blockly": { "version": "12.5.1", "resolved": "https://registry.npmjs.org/blockly/-/blockly-12.5.1.tgz", @@ -1297,10 +1476,171 @@ "node": ">=18" } }, + "node_modules/blockly/node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/blockly/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/blockly/node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/blockly/node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/blockly/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/blockly/node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/blockly/node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT" + }, + "node_modules/blockly/node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/blockly/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/blockly/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/blockly/node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/blockly/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -1310,21 +1650,22 @@ "node": "18 || 20 || >=22" } }, - "node_modules/brace-expansion/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { - "node": "18 || 20 || >=22" + "node": ">=18" } }, "node_modules/chokidar": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", - "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, + "license": "MIT", "dependencies": { "readdirp": "^4.0.1" }, @@ -1340,10 +1681,18 @@ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", @@ -1354,15 +1703,20 @@ } }, "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", "dependencies": { "object-assign": "^4", "vary": "^1" }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/cross-spawn": { @@ -1380,6 +1734,20 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1394,35 +1762,168 @@ } }, "node_modules/cssstyle": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.1.tgz", - "integrity": "sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/cssstyle/node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/cssstyle/node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/cssstyle/node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/cssstyle/node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^3.1.2", - "rrweb-cssom": "^0.8.0" - }, + "peer": true, "engines": { "node": ">=18" } }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, "license": "MIT", "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -1436,22 +1937,24 @@ } }, "node_modules/decimal.js": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", - "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "license": "MIT" }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1467,75 +1970,81 @@ } }, "node_modules/devalue": { - "version": "5.6.4", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", - "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.0.tgz", + "integrity": "sha512-2zA9pFEsnp7vWBZbXF5JAgAq0fsUIt/1XPbRiAmRV3lp/2C3upzH+sADiyy66aFCihoLEsrQHxNM5w1gIDfsBg==", "dev": true, "license": "MIT" }, "node_modules/engine.io": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", - "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.7.tgz", + "integrity": "sha512-DgOngfDKM2EviOH3Mr9m7ks1q8roetLy/IMmYthAYzbpInMbYc/GS+fWFA3rl1gvwKVsQrVV61fo5emD1y3OJQ==", "license": "MIT", "dependencies": { - "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", - "debug": "~4.3.1", + "debug": "~4.4.1", "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" + "ws": "~8.18.3" }, "engines": { "node": ">=10.2.0" } }, "node_modules/engine.io-client": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz", - "integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==", + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", + "debug": "~4.4.1", "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", + "ws": "~8.18.3", "xmlhttprequest-ssl": "~2.1.1" } }, "node_modules/engine.io-parser": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", - "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", "engines": { "node": ">=10.0.0" } }, - "node_modules/engine.io/node_modules/@types/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" - }, "node_modules/entities": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", - "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=0.12" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -1549,6 +2058,7 @@ "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -1663,17 +2173,19 @@ } }, "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -1684,6 +2196,7 @@ "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, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -1691,25 +2204,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", @@ -1723,22 +2217,14 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, + "license": "MIT", "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">= 4" } }, "node_modules/esm-env": { @@ -1749,31 +2235,31 @@ "license": "MIT" }, "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -1793,14 +2279,21 @@ } }, "node_modules/esrap": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz", - "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.7.tgz", + "integrity": "sha512-Dl7o7btn2YXca1VXx+PVl+lKuZdHBm8oCFuckUxqchMvNMdHMJ/qF31wtPaVyWvFYLQePkbXJrirWzbAP6Yamw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15", + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } } }, "node_modules/esrecurse": { @@ -1808,6 +2301,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -1820,19 +2314,51 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fake-indexeddb": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.2.5.tgz", + "integrity": "sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1851,7 +2377,8 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fdir": { "version": "6.5.0", @@ -1876,6 +2403,7 @@ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, + "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" }, @@ -1888,6 +2416,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -1904,6 +2433,7 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -1939,6 +2469,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -1960,15 +2491,16 @@ } }, "node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, "license": "MIT", "dependencies": { - "whatwg-encoding": "^3.1.1" + "@exodus/bytes": "^1.6.0" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/http-proxy-agent": { @@ -2009,10 +2541,16 @@ "node": ">=0.10.0" } }, + "node_modules/idb": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz", + "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==", + "license": "ISC" + }, "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -2024,6 +2562,7 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } @@ -2033,6 +2572,7 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2042,6 +2582,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -2060,6 +2601,7 @@ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "^1.0.6" } @@ -2072,34 +2614,37 @@ "license": "ISC" }, "node_modules/jsdom": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", - "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "cssstyle": "^4.2.1", - "data-urls": "^5.0.0", - "decimal.js": "^10.5.0", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.16", - "parse5": "^7.2.1", - "rrweb-cssom": "^0.8.0", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^5.1.1", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.1.1", - "ws": "^8.18.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" }, "peerDependencies": { "canvas": "^3.0.0" @@ -2110,32 +2655,12 @@ } } }, - "node_modules/jsdom/node_modules/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -2148,13 +2673,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -2164,6 +2691,7 @@ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -2180,6 +2708,7 @@ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -2454,6 +2983,7 @@ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } @@ -2462,13 +2992,15 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -2480,10 +3012,14 @@ } }, "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, "node_modules/magic-string": { "version": "0.30.21", @@ -2495,10 +3031,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2507,6 +3051,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -2515,13 +3060,13 @@ } }, "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -2535,15 +3080,17 @@ "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/mrmime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", - "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } @@ -2551,12 +3098,13 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -2576,26 +3124,29 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/nwsapi": { - "version": "2.2.20", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", - "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", "license": "MIT" }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2612,17 +3163,18 @@ "license": "MIT" }, "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, + "license": "MIT", "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { "node": ">= 0.8.0" @@ -2633,6 +3185,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -2648,6 +3201,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -2659,12 +3213,13 @@ } }, "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, "license": "MIT", "dependencies": { - "entities": "^6.0.0" + "entities": "^8.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -2675,6 +3230,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2689,6 +3245,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2702,6 +3265,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2710,9 +3274,9 @@ } }, "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { @@ -2729,6 +3293,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -2743,6 +3308,7 @@ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", "dev": true, + "license": "MIT", "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" @@ -2768,10 +3334,11 @@ } }, "node_modules/postcss-load-config/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "dev": true, + "license": "ISC", "engines": { "node": ">= 6" } @@ -2831,9 +3398,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, "license": "MIT", "dependencies": { @@ -2849,6 +3416,7 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } @@ -2859,6 +3427,7 @@ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -2870,9 +3439,9 @@ } }, "node_modules/prettier-plugin-svelte": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz", - "integrity": "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.2.tgz", + "integrity": "sha512-ItFouLvzSFE3ulNl4DKoWM3BGcbDCNVpIyy/Y3F2gC3aNiGLxtFUdffVqO5Z5hhYG+DFT5KULWaxmeFFpdbvaQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2884,32 +3453,44 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/readdirp": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.1.tgz", - "integrity": "sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 14.16.0" + "node": ">= 14.18.0" }, "funding": { "type": "individual", "url": "https://paulmillr.com/funding/" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rolldown": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", - "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz", + "integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.127.0", - "@rolldown/pluginutils": "1.0.0-rc.17" + "@oxc-project/types": "=0.129.0", + "@rolldown/pluginutils": "1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" @@ -2918,21 +3499,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.17", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", - "@rolldown/binding-darwin-x64": "1.0.0-rc.17", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + "@rolldown/binding-android-arm64": "1.0.0", + "@rolldown/binding-darwin-arm64": "1.0.0", + "@rolldown/binding-darwin-x64": "1.0.0", + "@rolldown/binding-freebsd-x64": "1.0.0", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0", + "@rolldown/binding-linux-arm64-gnu": "1.0.0", + "@rolldown/binding-linux-arm64-musl": "1.0.0", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0", + "@rolldown/binding-linux-s390x-gnu": "1.0.0", + "@rolldown/binding-linux-x64-gnu": "1.0.0", + "@rolldown/binding-linux-x64-musl": "1.0.0", + "@rolldown/binding-openharmony-arm64": "1.0.0", + "@rolldown/binding-wasm32-wasi": "1.0.0", + "@rolldown/binding-win32-arm64-msvc": "1.0.0", + "@rolldown/binding-win32-x64-msvc": "1.0.0" } }, "node_modules/rrweb-cssom": { @@ -2946,6 +3527,7 @@ "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", "dev": true, + "license": "MIT", "dependencies": { "mri": "^1.1.0" }, @@ -2972,9 +3554,9 @@ } }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "dev": true, "license": "ISC", "bin": { @@ -2985,9 +3567,9 @@ } }, "node_modules/set-cookie-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz", - "integrity": "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", "dev": true, "license": "MIT" }, @@ -3014,11 +3596,19 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/sirv": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", - "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", "dev": true, + "license": "MIT", "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", @@ -3047,13 +3637,13 @@ } }, "node_modules/socket.io-adapter": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", - "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", "license": "MIT", "dependencies": { - "debug": "~4.3.4", - "ws": "~8.17.1" + "debug": "~4.4.1", + "ws": "~8.18.3" } }, "node_modules/socket.io-client": { @@ -3071,23 +3661,6 @@ "node": ">=10.0.0" } }, - "node_modules/socket.io-client/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/socket.io-parser": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", @@ -3101,40 +3674,6 @@ "node": ">=10.0.0" } }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3145,12 +3684,27 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/svelte": { "version": "5.55.5", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.5.tgz", "integrity": "sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -3174,9 +3728,9 @@ } }, "node_modules/svelte-check": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.7.tgz", - "integrity": "sha512-JRafFTRmaPUOqmri4u1WuIKgBLiHi6wIaB57i99pmHq5BAc3ioIpzdUN/RX32ij9GhI6ALMHKvnVxu68sFZlag==", + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.8.tgz", + "integrity": "sha512-67adfgBox5eNSNIvIIwgFizKGdcRrGpiMoNO2obHcYuLz7iTa8Xgm/NGU3ntMFnNm8K1grFOIG6HhMLX/vcN8w==", "dev": true, "license": "MIT", "dependencies": { @@ -3198,9 +3752,9 @@ } }, "node_modules/svelte-eslint-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.4.0.tgz", - "integrity": "sha512-fjPzOfipR5S7gQ/JvI9r2H8y9gMGXO3JtmrylHLLyahEMquXI0lrebcjT+9/hNgDej0H7abTyox5HpHmW1PSWA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.6.1.tgz", + "integrity": "sha512-hhvSH6kRj46UzrBVO5TaotD+Iuvruj5ccKBcO4wAhVcPTLmIc/c32D8UllBTYO0on4LzYuM0rNzf1lM/gBlkSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3209,11 +3763,12 @@ "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", - "postcss-selector-parser": "^7.0.0" + "postcss-selector-parser": "^7.0.0", + "semver": "^7.7.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0", - "pnpm": "10.18.3" + "pnpm": "10.33.0" }, "funding": { "url": "https://github.com/sponsors/ota-meshi" @@ -3227,6 +3782,23 @@ } } }, + "node_modules/svelte-eslint-parser/node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", @@ -3240,59 +3812,22 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/svelte-preprocess": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-6.0.3.tgz", - "integrity": "sha512-PLG2k05qHdhmRG7zR/dyo5qKvakhm8IJ+hD2eFRQmMLHp7X3eJnjeupUtvuRpbNiF31RjVw45W+abDwHEmP5OA==", + "node_modules/svelte-eslint-parser/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, - "hasInstallScript": true, - "engines": { - "node": ">= 18.0.0" + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" }, - "peerDependencies": { - "@babel/core": "^7.10.2", - "coffeescript": "^2.5.1", - "less": "^3.11.3 || ^4.0.0", - "postcss": "^7 || ^8", - "postcss-load-config": ">=3", - "pug": "^3.0.0", - "sass": "^1.26.8", - "stylus": ">=0.55", - "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.100 || ^5.0.0", - "typescript": "^5.0.0" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "coffeescript": { - "optional": true - }, - "less": { - "optional": true - }, - "postcss": { - "optional": true - }, - "postcss-load-config": { - "optional": true - }, - "pug": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "typescript": { - "optional": true - } + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/symbol-tree": { @@ -3301,6 +3836,23 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -3318,22 +3870,34 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tldts": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", - "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", + "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", + "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^6.1.86" + "tldts-core": "^7.0.30" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", + "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", + "dev": true, "license": "MIT" }, "node_modules/totalist": { @@ -3341,32 +3905,35 @@ "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/tough-cookie": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { - "tldts": "^6.1.32" + "tldts": "^7.0.5" }, "engines": { "node": ">=16" } }, "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, "license": "MIT", "dependencies": { "punycode": "^2.3.1" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/ts-api-utils": { @@ -3386,13 +3953,15 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true + "dev": true, + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -3401,11 +3970,12 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3415,16 +3985,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.2.tgz", - "integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz", + "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.59.2", - "@typescript-eslint/parser": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/utils": "8.59.2" + "@typescript-eslint/eslint-plugin": "8.59.3", + "@typescript-eslint/parser": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3438,10 +4008,20 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", + "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", "license": "MIT" }, "node_modules/uri-js": { @@ -3478,21 +4058,23 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/vite": { - "version": "8.0.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", - "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "version": "8.0.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz", + "integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.10", - "rolldown": "1.0.0-rc.17", + "postcss": "^8.5.14", + "rolldown": "1.0.0", "tinyglobby": "^0.2.16" }, "bin": { @@ -3509,7 +4091,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", + "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -3561,9 +4143,9 @@ } }, "node_modules/vitefu": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz", - "integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", "dev": true, "license": "MIT", "workspaces": [ @@ -3572,7 +4154,7 @@ "tests/projects/workspace/packages/*" ], "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "vite": { @@ -3580,6 +4162,96 @@ } } }, + "node_modules/vitest": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", + "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.6", + "@vitest/mocker": "4.1.6", + "@vitest/pretty-format": "4.1.6", + "@vitest/runner": "4.1.6", + "@vitest/snapshot": "4.1.6", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.6", + "@vitest/browser-preview": "4.1.6", + "@vitest/browser-webdriverio": "4.1.6", + "@vitest/coverage-istanbul": "4.1.6", + "@vitest/coverage-v8": "4.1.6", + "@vitest/ui": "4.1.6", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -3593,18 +4265,20 @@ } }, "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=12" + "node": ">=20" } }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" @@ -3614,25 +4288,28 @@ } }, "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, "license": "MIT", "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/which": { @@ -3651,10 +4328,37 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -3688,18 +4392,19 @@ "license": "MIT" }, "node_modules/xmlhttprequest-ssl": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz", - "integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", "engines": { "node": ">=0.4.0" } }, "node_modules/yaml": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", - "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -3715,6 +4420,7 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -3723,9 +4429,9 @@ } }, "node_modules/zimmerframe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", - "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", "dev": true, "license": "MIT" } diff --git a/package.json b/package.json index 82fa7f21a5..fb10e66c1e 100644 --- a/package.json +++ b/package.json @@ -8,38 +8,42 @@ "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "test": "vitest run", "lint": "prettier --check . && eslint .", "format": "prettier --write ." }, "devDependencies": { "@sveltejs/adapter-auto": "^7.0.1", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.59.0", - "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@sveltejs/kit": "^2.59.1", + "@sveltejs/vite-plugin-svelte": "^7.1.2", "@types/uuid": "^11.0.0", - "@typescript-eslint/eslint-plugin": "^8.59.0", - "@typescript-eslint/parser": "^8.59.2", + "@typescript-eslint/eslint-plugin": "^8.59.3", + "@typescript-eslint/parser": "^8.59.3", "eslint": "^10.3.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.17.1", "globals": "^17.6.0", "prettier": "^3.8.3", - "prettier-plugin-svelte": "^3.5.1", + "fake-indexeddb": "^6.2.5", + "jsdom": "^29.1.1", + "prettier-plugin-svelte": "^3.5.2", "svelte": "^5.55.5", - "svelte-check": "^4.4.7", - "svelte-preprocess": "^6.0.3", + "svelte-check": "^4.4.8", "tslib": "^2.8.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.59.2", - "vite": "^8.0.10" + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.3", + "vite": "^8.0.12", + "vitest": "^4.1.6" }, "type": "module", "dependencies": { "blockly": "^12.5.1", + "idb": "^8.0.3", "socket.io": "^4.8.3", "socket.io-client": "^4.8.3", "uuid": "^14.0.0", - "yaml": "^2.8.4" + "yaml": "^2.9.0" }, "overrides": { "cookie": "1.0.2" diff --git a/src/components/sidebar/SidebarEntry.svelte b/src/components/sidebar/SidebarEntry.svelte index 100f521a40..e5cf4ab901 100644 --- a/src/components/sidebar/SidebarEntry.svelte +++ b/src/components/sidebar/SidebarEntry.svelte @@ -1,344 +1,374 @@ - - - -
over = false} - ondragover={dragOver} - ondragstart={startDrag} - ondrop={drop} - onkeypress={(e) => { - if (e.key === 'Enter') onclick?.(e); - }} - out:fly={{x: (direction === "left" ? -100 : 100), duration: 500}} - role='menuitem' - tabindex='0'> - {@render children?.()} - {#if data} -
- {#if data instanceof FabledSkill} - - - edit - - - {/if} - - {#if !browser || ('showOpenFilePicker' in window)} - {#key sync} -
toggleSyncLocal(data).then(() => sync = isSyncLocal(data))} - onkeypress={(event) => {if (event?.key === 'Enter') toggleSyncLocal(data).then(() => sync = isSyncLocal(data));}} - class:activeSync={sync} - tabindex='0' - role='button' - class='sync' - title='Local Sync: {sync ? "On" : "Off"}'> - sync -
- {/key} - {/if} - -
saveData(data, e)} - onkeypress={(event) => {if (event?.key === 'Enter') saveData(data, event);}} - tabindex='0' - role='button' - class='download' - title='Save {data.dataType.substring(0, 1).toUpperCase()+data.dataType.substring(1)}'> - - save - -
-
cloneData(data, e)} - onkeypress={(event) => { if (event?.key === 'Enter') cloneData(data, event); }} - tabindex='0' - role='button' - class='clone' - title='Clone {data.dataType.substring(0, 1).toUpperCase()+data.dataType.substring(1)}'> - - content_copy - -
-
{ - event.stopPropagation(); - event.preventDefault(); - // If holding shift, delete without confirmation - if (event?.shiftKey) { - deleteProData(data); - return; - } - deleting = true - }} - onkeypress={(event) => { - if (event?.key === 'Enter') { - event.stopPropagation(); - event.preventDefault(); - // If holding shift, delete without confirmation - if (event?.shiftKey) { - deleteProData(data); - return; - } - deleting = true; - } - }} - tabindex='0' - role='button' - class='delete' - title='Delete {data.dataType.substring(0, 1).toUpperCase()+data.dataType.substring(1)}'> - - delete - -
-
- {/if} -
- -{#if deleting} - -

Do you really want to delete {data?.name}?

- -
-{/if} - - \ No newline at end of file + diff --git a/src/data/attribute-store.ts b/src/data/attribute-store.ts index 0f718c1e6b..df0591a115 100644 --- a/src/data/attribute-store.ts +++ b/src/data/attribute-store.ts @@ -1,258 +1,252 @@ -import type { Writable } from 'svelte/store'; -import { get, writable } from 'svelte/store'; -import { browser } from '$app/environment'; -import YAML from 'yaml'; -import FabledAttribute from '$api/fabled-attribute.svelte'; +import type { Writable } from 'svelte/store'; +import { get, writable } from 'svelte/store'; +import FabledAttribute from '$api/fabled-attribute.svelte'; import type { MultiAttributeYamlData } from '$api/types'; -import { sort } from '$api/api'; -import { parseYaml } from '$api/yaml'; -import { active, saveError } from './store'; -import { base } from '$app/paths'; -import { goto } from '$app/navigation'; -import { socketService } from '$api/socket/socket-connector'; -import { classStore } from './class-store.svelte'; +import { sort } from '$api/api'; +import { parseYaml } from '$api/yaml'; +import { active, saveError } from './store'; +import { base } from '$app/paths'; +import { goto } from '$app/navigation'; +import { socketService } from '$api/socket/socket-connector'; +import { classStore } from './class-store.svelte'; +import { + getPersistedAttribute, + listPersistedAttributeRecords, + savePersistedAttributes +} from './editor-persistence'; +import { getPersistenceFailureMessage } from './persistence-state'; class AttributeStore { - tooBig: Writable = writable(false); - acknowledged: Writable = writable(false); - - loadAttributesFromServer = async () => { - let serverAttributes: string = ''; - try { - serverAttributes = await socketService.getAttributeYaml(); - } catch (_) { - return; - } - - this.loadAttributesText(serverAttributes, 'server'); - }; - - removeServerAttributes = () => { - const tempAttributes = get(this.attributes); - this.attributes.set(tempAttributes.filter((attr) => attr.location !== 'server')); - }; - - constructor() { - socketService.onConnect(this.loadAttributesFromServer); - socketService.onDisconnect(this.removeServerAttributes); - - this.getDefaultAttributes().then((defaultAttributes) => { - setTimeout(() => { - const attributes = get(this.attributes); - if (attributes.length === 0) { - this.attributes.set(defaultAttributes); - } - }, 500); - }); - } - - private setupAttributeStore = ( - key: string, - def: T, - mapper: (data: string) => T, - setAction: (data: T) => T, - postLoad?: (saved: T) => void): Writable => { - let saved: T = def; - if (browser) { - const stored = localStorage.getItem(key); - if (stored) { - saved = mapper(stored); - if (postLoad) postLoad(saved); - } - } - - const { - subscribe, - set, - update - } = writable(saved); - return { - subscribe, - set: (value: T) => { - if (setAction) value = setAction(value); - return set(value); - }, - update - }; - }; - - getDefaultAttributes = async (): Promise => { - const yaml = parseYaml(await fetch('https://raw.githubusercontent.com/magemonkeystudio/fabled/dev/src/main/resources/attributes.yml').then(r => r.text())); - if (!yaml) return []; - return Object.keys(yaml).map((key: string) => { - const attrib: FabledAttribute = new FabledAttribute({ name: key }); - attrib.load(yaml[key]); - return attrib; - }); - - }; - - attributes: Writable = this.setupAttributeStore( - 'attribs', - [], - (data: string) => { - if (data.split('\n').length < 3 && data.charAt(0) !== '{') { // Old format - return data.replace('\n', '').split(',').map((key: string) => new FabledAttribute({ name: key })); - } - const yaml = parseYaml(data); - if (!yaml) return []; - return Object.keys(yaml).map((key: string) => { - const attrib: FabledAttribute = new FabledAttribute({ name: key }); - attrib.load(yaml[key]); - return attrib; - }); - }, - (value: FabledAttribute[]) => { - classStore.updateAllAttributes(value.map((attr: FabledAttribute) => attr.name)); - return sort(value); - }); - - getAttributeNames = (): string[] => { - return get(this.attributes).map((attr) => attr.name); - }; - - getAttribute = (name: string): FabledAttribute | undefined => { - for (const c of get(this.attributes)) { - if (c.name == name) return c; - } - - return undefined; - }; - - isAttributeNameTaken = (name: string): boolean => !!this.getAttribute(name); - - addAttribute = (name?: string): FabledAttribute => { - const allAttributes = get(this.attributes); - let index = allAttributes.length + 1; - while (!name && this.isAttributeNameTaken(name || 'attribute ' + index)) { - index++; - } - const attrib = new FabledAttribute({ name: (name || 'attribute ' + index) }); - allAttributes.push(attrib); - - this.attributes.set(allAttributes); - attrib.save(); - return attrib; - }; - - - loadAttributes = (e: ProgressEvent) => { - const text: string = e.target?.result; - if (!text) return; - - this.loadAttributesText(text); - }; - - /** - * Loads attribute data from a file - * e - event details - */ - loadAttributesText = (text: string, location: 'local' | 'server' = 'local') => { - const yaml = parseYaml(text); - if (!yaml) return; - - // Get the current attributes - const currentAttributes = get(this.attributes); - // Create a map of current attributes for easy lookup - const currentAttributesMap = new Map(currentAttributes.map(attr => [attr.name, attr])); - - // Merge the current attributes with the new ones - const mergedAttributes = [...currentAttributes]; - Object.keys(yaml).forEach((key: string) => { - // If the attribute already exists, ignore it - if (!currentAttributesMap.has(key)) { - // Otherwise, create a new attribute - const newAttribute = new FabledAttribute({ name: key, location }); - newAttribute.load(yaml[key]); - mergedAttributes.push(newAttribute); - } - }); - - this.attributes.set(mergedAttributes); - this.refreshAttributes(); - }; - - loadAttribute = (data: FabledAttribute) => { - if (data.loaded) return; - - if (data.location === 'local') { - const yamlData = parseYaml(localStorage.getItem('attribs') || ''); - if (!yamlData) return; - const attrib = yamlData[data.name]; - data.load(attrib); - } - }; - - cloneAttribute = (data: FabledAttribute): FabledAttribute => { - if (!data.loaded) this.loadAttribute(data); - - const attr: FabledAttribute[] = get(this.attributes); - let name = data.name + ' (Copy)'; - let i = 1; - while (this.isAttributeNameTaken(name)) { - name = data.name + ' (Copy ' + i + ')'; - i++; - } - const attribute = new FabledAttribute(); - const yamlData = data.serializeYaml(); - attribute.load(yamlData); - attribute.name = name; - attr.push(attribute); - - this.attributes.set(attr); - attribute.save(); - return attribute; - }; - - refreshAttributes = () => this.attributes.set(sort(get(this.attributes))); - - deleteAttribute = (data: FabledAttribute) => { - const filtered = get(this.attributes).filter(c => c != data); - const act = get(active); - this.attributes.set(filtered); - this.saveAll(); - - if (!(act instanceof FabledAttribute)) return; - - if (filtered.length === 0) { - goto(`${base}/`).then(() => { - }); - } else if (!filtered.find(attr => attr === get(active))) { - goto(`${base}/attribute/${filtered[0].name}/edit`).then(() => { - }); - } - }; - - saveAll = () => { - if (get(this.tooBig)) return; - - if (get(this.tooBig) && !get(this.acknowledged)) { - saveError.set({ name: 'Attributes', acknowledged: false }); - return; - } - - const attributeYaml: MultiAttributeYamlData = {}; - for (const attr of get(this.attributes)) { - attributeYaml[attr.name] = attr.serializeYaml(); - } - const yaml = YAML.stringify(attributeYaml, { lineWidth: 0, aliasDuplicateObjects: false }); - - try { - localStorage.setItem('attribs', yaml); - this.tooBig.set(false); - } catch (e: any) { - // If the data is too big - if (!e?.message?.includes('quota')) { - console.error('Attributes Save error', e); - } else { - localStorage.removeItem('attribs'); - this.tooBig.set(true); - saveError.set({ name: 'Attributes', acknowledged: false }); - } - } - - console.log('Saved attributes 😎'); - }; + loadAttributesFromServer = async () => { + let serverAttributes: string = ''; + try { + serverAttributes = await socketService.getAttributeYaml(); + } catch (_) { + return; + } + + this.loadAttributesText(serverAttributes, 'server'); + }; + + removeServerAttributes = () => { + const tempAttributes = get(this.attributes); + this.attributes.set(tempAttributes.filter((attr) => attr.location !== 'server')); + }; + + constructor() { + socketService.onConnect(this.loadAttributesFromServer); + socketService.onDisconnect(this.removeServerAttributes); + + this.getDefaultAttributes().then((defaultAttributes) => { + setTimeout(() => { + const attributes = get(this.attributes); + if (attributes.length === 0) { + this.attributes.set(defaultAttributes); + } + }, 500); + }); + } + + private setupAttributeStore = ( + _key: string, + def: T, + mapper: (data: string) => T, + setAction: (data: T) => T, + postLoad?: (saved: T) => void + ): Writable => { + let saved: T = def; + if (postLoad) postLoad(saved); + + const { subscribe, set, update } = writable(saved); + return { + subscribe, + set: (value: T) => { + if (setAction) value = setAction(value); + return set(value); + }, + update + }; + }; + + hydratePersistedData = async () => { + const attributes = listPersistedAttributeRecords().map((record) => { + const attribute = new FabledAttribute({ name: record.name, location: 'local' }); + attribute.load(record.data); + return attribute; + }); + + this.attributes.set(sort(attributes)); + }; + + getDefaultAttributes = async (): Promise => { + const yaml = parseYaml( + await fetch( + 'https://raw.githubusercontent.com/magemonkeystudio/fabled/dev/src/main/resources/attributes.yml' + ).then((r) => r.text()) + ); + if (!yaml) return []; + return Object.keys(yaml).map((key: string) => { + const attrib: FabledAttribute = new FabledAttribute({ name: key }); + attrib.load(yaml[key]); + return attrib; + }); + }; + + attributes: Writable = this.setupAttributeStore( + 'attributes', + [], + (_data: string) => [], + (value: FabledAttribute[]) => { + classStore.updateAllAttributes(value.map((attr: FabledAttribute) => attr.name)); + return sort(value); + } + ); + + getAttributeNames = (): string[] => { + return get(this.attributes).map((attr) => attr.name); + }; + + getAttribute = (name: string): FabledAttribute | undefined => { + for (const c of get(this.attributes)) { + if (c.name == name) return c; + } + + return undefined; + }; + + isAttributeNameTaken = (name: string): boolean => !!this.getAttribute(name); + + addAttribute = (name?: string): FabledAttribute => { + const allAttributes = get(this.attributes); + let index = allAttributes.length + 1; + while (!name && this.isAttributeNameTaken(name || 'attribute ' + index)) { + index++; + } + const attrib = new FabledAttribute({ name: name || 'attribute ' + index }); + allAttributes.push(attrib); + + this.attributes.set(allAttributes); + attrib.save(); + return attrib; + }; + + loadAttributes = (e: ProgressEvent) => { + const text: string = e.target?.result; + if (!text) return; + + this.loadAttributesText(text); + }; + + /** + * Loads attribute data from a file + * e - event details + */ + loadAttributesText = (text: string, location: 'local' | 'server' = 'local') => { + const yaml = parseYaml(text); + if (!yaml) return; + + // Get the current attributes + const currentAttributes = get(this.attributes); + // Create a map of current attributes for easy lookup + const currentAttributesMap = new Map(currentAttributes.map((attr) => [attr.name, attr])); + + // Merge the current attributes with the new ones + const mergedAttributes = [...currentAttributes]; + Object.keys(yaml).forEach((key: string) => { + // If the attribute already exists, ignore it + if (!currentAttributesMap.has(key)) { + // Otherwise, create a new attribute + const newAttribute = new FabledAttribute({ name: key, location }); + newAttribute.load(yaml[key]); + mergedAttributes.push(newAttribute); + } + }); + + this.attributes.set(mergedAttributes); + this.refreshAttributes(); + }; + + loadAttribute = async (data: FabledAttribute) => { + if (data.loaded) return; + + if (data.location === 'local') { + const yamlData = await getPersistedAttribute(data.name); + if (!yamlData) return; + data.load(yamlData); + } + }; + + cloneAttribute = async (data: FabledAttribute): Promise => { + if (!data.loaded) { + await this.loadAttribute(data); + } + + if (!data.loaded) { + throw new Error(`Cannot clone unloaded attribute "${data.name}". Load it before cloning.`); + } + + const attr: FabledAttribute[] = get(this.attributes); + let name = data.name + ' (Copy)'; + let i = 1; + while (this.isAttributeNameTaken(name)) { + name = data.name + ' (Copy ' + i + ')'; + i++; + } + const attribute = new FabledAttribute(); + const yamlData = data.serializeYaml(); + attribute.load(yamlData); + attribute.name = name; + attr.push(attribute); + + this.attributes.set(attr); + attribute.save(); + return attribute; + }; + + refreshAttributes = () => this.attributes.set(sort(get(this.attributes))); + + deleteAttribute = (data: FabledAttribute) => { + const filtered = get(this.attributes).filter((c) => c != data); + const act = get(active); + this.attributes.set(filtered); + this.saveAll(); + + if (!(act instanceof FabledAttribute)) return; + + if (filtered.length === 0) { + goto(`${base}/`).then(() => {}); + } else if (!filtered.find((attr) => attr === get(active))) { + goto(`${base}/attribute/${filtered[0].name}/edit`).then(() => {}); + } + }; + + saveAll = () => { + const attributeYaml: MultiAttributeYamlData = {}; + for (const attr of get(this.attributes)) { + attributeYaml[attr.name] = attr.serializeYaml(); + } + + void savePersistedAttributes( + Object.entries(attributeYaml).map(([name, data]) => ({ + name, + data + })) + ).then((result) => { + if (!result.ok) { + console.error('Attributes Save error', result.error); + saveError.set({ + name: 'Attributes', + message: getPersistenceFailureMessage(result) + }); + return; + } + + if (get(saveError)?.name === 'Attributes') { + saveError.set(undefined); + } + console.log('Saved attributes 😎'); + }); + }; } -export const attributeStore = new AttributeStore(); \ No newline at end of file +export const attributeStore = new AttributeStore(); diff --git a/src/data/class-store.svelte.ts b/src/data/class-store.svelte.ts index 31c7b63127..e91fbc7d1e 100644 --- a/src/data/class-store.svelte.ts +++ b/src/data/class-store.svelte.ts @@ -1,713 +1,734 @@ -import type { Writable } from 'svelte/store'; -import { get, writable } from 'svelte/store'; -import { active } from './store'; -import { parseBool, sort, toEditorCase, toProperCase } from '$api/api'; -import { parseYaml } from '$api/yaml'; -import { - browser -} from '$app/environment'; -import { - goto -} from '$app/navigation'; -import { base } from '$app/paths'; -import type { ClassYamlData, FabledClassData, IAttribute, Icon, MultiClassYamlData, Serializable } from '$api/types'; -import YAML from 'yaml'; -import { - socketService -} from '$api/socket/socket-connector'; -import { - notify -} from '$api/notification-service'; +import type { Writable } from 'svelte/store'; +import { get, writable } from 'svelte/store'; +import { active, saveError } from './store'; +import { parseBool, sort, toEditorCase, toProperCase } from '$api/api'; +import { parseYaml } from '$api/yaml'; +import { goto } from '$app/navigation'; +import { base } from '$app/paths'; import type { - SkillTree -} from '$api/SkillTree'; -import FabledSkill, { - skillStore -} from './skill-store.svelte'; + ClassYamlData, + FabledClassData, + IAttribute, + Icon, + MultiClassYamlData, + Serializable +} from '$api/types'; +import { socketService } from '$api/socket/socket-connector'; +import { notify } from '$api/notification-service'; +import type { SkillTree } from '$api/SkillTree'; +import FabledSkill, { skillStore } from './skill-store.svelte'; +import { FabledFolder, folderStore, type FolderProperties } from './folder-store.svelte'; +import { getPersistenceFailureMessage } from './persistence-state'; import { - FabledFolder, - folderStore -} from './folder-store.svelte'; + deletePersistedClass, + getPersistedClass, + getPersistedFolders, + listPersistedClassNames, + savePersistedClass, + savePersistedFolders +} from './editor-persistence'; export default class FabledClass implements Serializable { - dataType = 'class'; - location: 'local' | 'server' = 'local'; - loaded = $state(false); - - isClass = true; - public key = {}; - name: string = $state(''); - previousName: string = ''; - prefix = $state(''); - group = $state('class'); - manaName = $state('&2Mana'); - maxLevel = $state(40); - parent?: FabledClass = $state(); - parentStr = $state(this.parent?.name); - - permission = $state(false); - expSources = $state(273); - manaRegen = $state(1); - health: IAttribute = $state({ name: 'health', base: 20, scale: 1 }); - mana: IAttribute = $state({ name: 'mana', base: 20, scale: 1 }); - attributes: IAttribute[] = $state([]); - skillTree: SkillTree = $state('Requirement'); - skills: FabledSkill[] = $state([]); - icon: Icon = $state({ - material: 'Pumpkin', - customModelData: 0 - }); - unusableItems: string[] = $state([]); - actionBar = $state(''); - - lInverted = $state(true); - rInverted = $state(true); - lsInverted = $state(true); - rsInverted = $state(true); - sInverted = $state(true); - pInverted = $state(true); - qInverted = $state(true); - fInverted = $state(true); - - lWhitelist: string[] = $state([]); - rWhitelist: string[] = $state([]); - lsWhitelist: string[] = $state([]); - rsWhitelist: string[] = $state([]); - sWhitelist: string[] = $state([]); - pWhitelist: string[] = $state([]); - qWhitelist: string[] = $state([]); - fWhitelist: string[] = $state([]); - - constructor(data?: FabledClassData) { - this.name = data?.name || 'Class'; - this.prefix = data?.prefix || '&6' + this.name; - if (!data) return; - if (data?.location) this.location = data.location; - if (data?.group) this.group = data.group; - if (data?.manaName) this.manaName = data.manaName; - if (data?.maxLevel) this.maxLevel = data.maxLevel; - if (data?.parent) this.parent = data.parent; - if (data?.permission !== undefined) this.permission = data.permission; - if (data?.expSources) this.expSources = data.expSources; - if (data?.health) this.health = data.health; - if (data?.mana) this.mana = data.mana; - if (data?.manaRegen) this.manaRegen = data.manaRegen; - if (data?.attributes) this.attributes = data.attributes; - if (data?.skillTree) this.skillTree = data.skillTree; - if (data?.skills) this.skills = data.skills; - if (data?.icon) this.icon = data.icon; - if (data?.unusableItems) this.unusableItems = data.unusableItems; - if (data?.actionBar) this.actionBar = data.actionBar; - - // Combo starters - if (data?.lInverted !== undefined) this.lInverted = data.lInverted; - if (data?.rInverted !== undefined) this.rInverted = data.rInverted; - if (data?.lsInverted !== undefined) this.lsInverted = data.lsInverted; - if (data?.rsInverted !== undefined) this.rsInverted = data.rsInverted; - if (data?.pInverted !== undefined) this.pInverted = data.pInverted; - if (data?.qInverted !== undefined) this.qInverted = data.qInverted; - if (data?.fInverted !== undefined) this.fInverted = data.fInverted; - if (data?.lWhitelist) this.lWhitelist = data.lWhitelist; - if (data?.rWhitelist) this.rWhitelist = data.rWhitelist; - if (data?.lsWhitelist) this.lsWhitelist = data.lsWhitelist; - if (data?.rsWhitelist) this.rsWhitelist = data.rsWhitelist; - if (data?.pWhitelist) this.pWhitelist = data.pWhitelist; - if (data?.qWhitelist) this.qWhitelist = data.qWhitelist; - if (data?.fWhitelist) this.fWhitelist = data.fWhitelist; - } - - /** - * Reads all the reactive state elements to act as a chane detector - */ - public changed = () => { - return { - name: this.name, - prefix: this.prefix, - group: this.group, - manaName: this.manaName, - maxLevel: this.maxLevel, - parent: this.parent, - permission: this.permission, - expSources: this.expSources, - health: this.health, - mana: this.mana, - attributes: this.attributes, - skillTree: this.skillTree, - skills: this.skills, - icon: this.icon, - unusableItems: this.unusableItems, - actionBar: this.actionBar, - lInverted: this.lInverted, - rInverted: this.rInverted, - lsInverted: this.lsInverted, - rsInverted: this.rsInverted, - sInverted: this.sInverted, - pInverted: this.pInverted, - qInverted: this.qInverted, - fInverted: this.fInverted, - lWhitelist: this.lWhitelist, - rWhitelist: this.rWhitelist, - lsWhitelist: this.lsWhitelist, - rsWhitelist: this.rsWhitelist, - sWhitelist: this.sWhitelist, - pWhitelist: this.pWhitelist, - qWhitelist: this.qWhitelist, - fWhitelist: this.fWhitelist - }; - }; - - public updateAttributes = (attribs: string[]) => { - const included: string[] = []; - this.attributes = this.attributes.filter(a => { - if (attribs?.includes(a.name)) { - included.push(a.name); - return true; - } - return false; - }); - - attribs = attribs.filter(a => !included.includes(a)); - - for (const attrib of attribs) { - this.attributes.push({ name: attrib, base: 0, scale: 0 }); - } - }; - - public serializeYaml = (): ClassYamlData => { - const health = { - base: this.health.base, - scale: this.health.scale - }; - const mana = { - base: this.mana.base, - scale: this.mana.scale - }; - - // Attempt to convert health/mana base & scale to numbers, if applicable - if (typeof (health.base) === 'string') { - const base = parseFloat(health.base); - if (!isNaN(base)) { - health.base = base; - } - } - if (typeof (health.scale) === 'string') { - const scale = parseFloat(health.scale); - if (!isNaN(scale)) { - health.scale = scale; - } - } - if (typeof (mana.base) === 'string') { - const base = parseFloat(mana.base); - if (!isNaN(base)) { - mana.base = base; - } - } - if (typeof (mana.scale) === 'string') { - const scale = parseFloat(mana.scale); - if (!isNaN(scale)) { - mana.scale = scale; - } - } - - const yaml = { - name: this.name, - 'action-bar': this.actionBar, - prefix: this.prefix, - group: this.group, - mana: this.manaName, - 'max-level': this.maxLevel, - parent: this.parent?.name || '', - 'needs-permission': this.permission, - attributes: { - 'health-base': health.base ?? 20, - 'health-scale': health.scale ?? 0, - 'mana-base': mana.base ?? 20, - 'mana-scale': mana.scale ?? 0 - }, - 'mana-regen': this.manaRegen, - 'skill-tree': this.skillTree.toUpperCase().replace(/ /g, '_'), - blacklist: this.unusableItems, - skills: this.skills.map(s => s.name), - icon: this.icon.material, - 'icon-data': this.icon.customModelData, - 'icon-lore': this.icon.lore, - 'exp-source': this.expSources, - 'combo-starters': { - L: { inverted: this.lInverted, whitelist: this.lWhitelist }, - R: { inverted: this.rInverted, whitelist: this.rWhitelist }, - LS: { inverted: this.lsInverted, whitelist: this.lsWhitelist }, - RS: { inverted: this.rsInverted, whitelist: this.rsWhitelist }, - S: { inverted: this.sInverted, whitelist: this.sWhitelist }, - P: { inverted: this.pInverted, whitelist: this.pWhitelist }, - Q: { inverted: this.qInverted, whitelist: this.qWhitelist }, - F: { inverted: this.fInverted, whitelist: this.fWhitelist } - } - }; - - this.attributes.forEach(attr => { - if (typeof attr.base === 'string') { - const base = parseFloat(attr.base); - if (!isNaN(base)) { - attr.base = base; - } - } - if (typeof attr.scale === 'string') { - const scale = parseFloat(attr.scale); - if (!isNaN(scale)) { - attr.scale = scale; - } - } - - yaml.attributes[`${attr.name.toLowerCase()}-base`] = attr.base || 0; - yaml.attributes[`${attr.name.toLowerCase()}-scale`] = attr.scale || 0; - }); - - return yaml; - }; - - public updateParent = (classes: FabledClass[]) => { - if (!this.parentStr) return; - this.parent = classes.find(c => c.name === this.parentStr); - }; - - public load = (yaml: ClassYamlData) => { - if (yaml.name) this.name = yaml.name; - if (yaml['action-bar'] !== undefined) this.actionBar = yaml['action-bar']; - if (yaml.mana !== undefined) this.manaName = yaml.mana; - if (yaml.prefix !== undefined) this.prefix = yaml.prefix; - if (yaml.group) this.group = yaml.group; - if (yaml['max-level']) this.maxLevel = yaml['max-level']; - if (yaml.parent) this.parentStr = yaml.parent; - this.permission = parseBool(yaml['needs-permission']); - - if (yaml.attributes) { - const attributes = yaml.attributes; - this.health = { - name: 'health', - base: attributes['health-base'] ?? 20, - scale: attributes['health-scale'] ?? 1 - }; - this.mana = { name: 'mana', base: attributes['mana-base'] ?? 20, scale: attributes['mana-scale'] ?? 1 }; - - const map: { [key: string]: IAttribute } = {}; - for (const attrId of Object.keys(attributes)) { - const split = attrId.split('-'); - const name = split[0]; - if (map[name] || name === 'health' || name === 'mana') continue; - - map[name] = { name, base: attributes[`${name}-base`], scale: attributes[`${name}-scale`] }; - } - this.attributes = Object.values(map); - } - - if (yaml['mana-regen']) this.manaRegen = yaml['mana-regen']; - if (yaml['skill-tree']) this.skillTree = toProperCase(yaml['skill-tree']); - if (yaml.blacklist) this.unusableItems = yaml.blacklist; - if (yaml.skills) this.skills = yaml.skills.map(s => skillStore.getSkill(s)).filter(s => !!s); - if (yaml.icon) this.icon.material = toEditorCase(yaml.icon); - if (yaml['icon-data']) this.icon.customModelData = yaml['icon-data']; - if (yaml['icon-lore']) this.icon.lore = yaml['icon-lore']; - if (yaml['exp-source'] !== null) this.expSources = yaml['exp-source']; - - if (yaml['combo-starters']) { - // Combo starters - const combos = yaml['combo-starters']; - if (combos) { - this.lInverted = parseBool(combos.L?.inverted); - this.rInverted = parseBool(combos.R?.inverted); - this.lsInverted = parseBool(combos.LS?.inverted); - this.rsInverted = parseBool(combos.RS?.inverted); - this.sInverted = parseBool(combos.S?.inverted); - this.pInverted = parseBool(combos.P?.inverted); - this.qInverted = parseBool(combos.Q?.inverted); - this.fInverted = parseBool(combos.F?.inverted); - this.lWhitelist = combos.L?.whitelist || []; - this.rWhitelist = combos.R?.whitelist || []; - this.lsWhitelist = combos.LS?.whitelist || []; - this.rsWhitelist = combos.RS?.whitelist || []; - this.sWhitelist = combos.S?.whitelist || []; - this.pWhitelist = combos.P?.whitelist || []; - this.qWhitelist = combos.Q?.whitelist || []; - this.fWhitelist = combos.F?.whitelist || []; - } - } - - this.loaded = true; - this.save(); - }; - - public save = () => { - if (!this.name) return; - - if (this.location === 'server') { - return; - } - - this.changed(); - - const yaml = YAML.stringify({ [this.name]: this.serializeYaml() }, { lineWidth: 0, aliasDuplicateObjects: false }); - - if (this.previousName && this.previousName !== this.name) { - localStorage.removeItem('sapi.class.' + this.previousName); - } - this.previousName = this.name; - localStorage.setItem('sapi.class.' + this.name, yaml); - - console.log('Saved ' + this.name + ' 😎'); - }; + dataType = 'class'; + location: 'local' | 'server' = 'local'; + loaded = $state(false); + + isClass = true; + public key = {}; + name: string = $state(''); + previousName: string = ''; + prefix = $state(''); + group = $state('class'); + manaName = $state('&2Mana'); + maxLevel = $state(40); + parent?: FabledClass = $state(); + parentStr = $state(this.parent?.name); + + permission = $state(false); + expSources = $state(273); + manaRegen = $state(1); + health: IAttribute = $state({ name: 'health', base: 20, scale: 1 }); + mana: IAttribute = $state({ name: 'mana', base: 20, scale: 1 }); + attributes: IAttribute[] = $state([]); + skillTree: SkillTree = $state('Requirement'); + skills: FabledSkill[] = $state([]); + icon: Icon = $state({ + material: 'Pumpkin', + customModelData: 0 + }); + unusableItems: string[] = $state([]); + actionBar = $state(''); + + lInverted = $state(true); + rInverted = $state(true); + lsInverted = $state(true); + rsInverted = $state(true); + sInverted = $state(true); + pInverted = $state(true); + qInverted = $state(true); + fInverted = $state(true); + + lWhitelist: string[] = $state([]); + rWhitelist: string[] = $state([]); + lsWhitelist: string[] = $state([]); + rsWhitelist: string[] = $state([]); + sWhitelist: string[] = $state([]); + pWhitelist: string[] = $state([]); + qWhitelist: string[] = $state([]); + fWhitelist: string[] = $state([]); + + constructor(data?: FabledClassData) { + this.name = data?.name || 'Class'; + this.prefix = data?.prefix || '&6' + this.name; + if (!data) return; + if (data?.location) this.location = data.location; + if (data?.group) this.group = data.group; + if (data?.manaName) this.manaName = data.manaName; + if (data?.maxLevel) this.maxLevel = data.maxLevel; + if (data?.parent) this.parent = data.parent; + if (data?.permission !== undefined) this.permission = data.permission; + if (data?.expSources) this.expSources = data.expSources; + if (data?.health) this.health = data.health; + if (data?.mana) this.mana = data.mana; + if (data?.manaRegen) this.manaRegen = data.manaRegen; + if (data?.attributes) this.attributes = data.attributes; + if (data?.skillTree) this.skillTree = data.skillTree; + if (data?.skills) this.skills = data.skills; + if (data?.icon) this.icon = data.icon; + if (data?.unusableItems) this.unusableItems = data.unusableItems; + if (data?.actionBar) this.actionBar = data.actionBar; + + // Combo starters + if (data?.lInverted !== undefined) this.lInverted = data.lInverted; + if (data?.rInverted !== undefined) this.rInverted = data.rInverted; + if (data?.lsInverted !== undefined) this.lsInverted = data.lsInverted; + if (data?.rsInverted !== undefined) this.rsInverted = data.rsInverted; + if (data?.pInverted !== undefined) this.pInverted = data.pInverted; + if (data?.qInverted !== undefined) this.qInverted = data.qInverted; + if (data?.fInverted !== undefined) this.fInverted = data.fInverted; + if (data?.lWhitelist) this.lWhitelist = data.lWhitelist; + if (data?.rWhitelist) this.rWhitelist = data.rWhitelist; + if (data?.lsWhitelist) this.lsWhitelist = data.lsWhitelist; + if (data?.rsWhitelist) this.rsWhitelist = data.rsWhitelist; + if (data?.pWhitelist) this.pWhitelist = data.pWhitelist; + if (data?.qWhitelist) this.qWhitelist = data.qWhitelist; + if (data?.fWhitelist) this.fWhitelist = data.fWhitelist; + } + + /** + * Reads all the reactive state elements to act as a chane detector + */ + public changed = () => { + return { + name: this.name, + prefix: this.prefix, + group: this.group, + manaName: this.manaName, + maxLevel: this.maxLevel, + parent: this.parent, + permission: this.permission, + expSources: this.expSources, + health: this.health, + mana: this.mana, + attributes: this.attributes, + skillTree: this.skillTree, + skills: this.skills, + icon: this.icon, + unusableItems: this.unusableItems, + actionBar: this.actionBar, + lInverted: this.lInverted, + rInverted: this.rInverted, + lsInverted: this.lsInverted, + rsInverted: this.rsInverted, + sInverted: this.sInverted, + pInverted: this.pInverted, + qInverted: this.qInverted, + fInverted: this.fInverted, + lWhitelist: this.lWhitelist, + rWhitelist: this.rWhitelist, + lsWhitelist: this.lsWhitelist, + rsWhitelist: this.rsWhitelist, + sWhitelist: this.sWhitelist, + pWhitelist: this.pWhitelist, + qWhitelist: this.qWhitelist, + fWhitelist: this.fWhitelist + }; + }; + + public updateAttributes = (attribs: string[]) => { + const included: string[] = []; + this.attributes = this.attributes.filter((a) => { + if (attribs?.includes(a.name)) { + included.push(a.name); + return true; + } + return false; + }); + + attribs = attribs.filter((a) => !included.includes(a)); + + for (const attrib of attribs) { + this.attributes.push({ name: attrib, base: 0, scale: 0 }); + } + }; + + public serializeYaml = (): ClassYamlData => { + const health = { + base: this.health.base, + scale: this.health.scale + }; + const mana = { + base: this.mana.base, + scale: this.mana.scale + }; + + // Attempt to convert health/mana base & scale to numbers, if applicable + if (typeof health.base === 'string') { + const base = parseFloat(health.base); + if (!isNaN(base)) { + health.base = base; + } + } + if (typeof health.scale === 'string') { + const scale = parseFloat(health.scale); + if (!isNaN(scale)) { + health.scale = scale; + } + } + if (typeof mana.base === 'string') { + const base = parseFloat(mana.base); + if (!isNaN(base)) { + mana.base = base; + } + } + if (typeof mana.scale === 'string') { + const scale = parseFloat(mana.scale); + if (!isNaN(scale)) { + mana.scale = scale; + } + } + + const yaml = { + name: this.name, + 'action-bar': this.actionBar, + prefix: this.prefix, + group: this.group, + mana: this.manaName, + 'max-level': this.maxLevel, + parent: this.parent?.name || '', + 'needs-permission': this.permission, + attributes: { + 'health-base': health.base ?? 20, + 'health-scale': health.scale ?? 0, + 'mana-base': mana.base ?? 20, + 'mana-scale': mana.scale ?? 0 + }, + 'mana-regen': this.manaRegen, + 'skill-tree': this.skillTree.toUpperCase().replace(/ /g, '_'), + blacklist: this.unusableItems, + skills: this.skills.map((s) => s.name), + icon: this.icon.material, + 'icon-data': this.icon.customModelData, + 'icon-lore': this.icon.lore, + 'exp-source': this.expSources, + 'combo-starters': { + L: { inverted: this.lInverted, whitelist: this.lWhitelist }, + R: { inverted: this.rInverted, whitelist: this.rWhitelist }, + LS: { inverted: this.lsInverted, whitelist: this.lsWhitelist }, + RS: { inverted: this.rsInverted, whitelist: this.rsWhitelist }, + S: { inverted: this.sInverted, whitelist: this.sWhitelist }, + P: { inverted: this.pInverted, whitelist: this.pWhitelist }, + Q: { inverted: this.qInverted, whitelist: this.qWhitelist }, + F: { inverted: this.fInverted, whitelist: this.fWhitelist } + } + }; + + this.attributes.forEach((attr) => { + if (typeof attr.base === 'string') { + const base = parseFloat(attr.base); + if (!isNaN(base)) { + attr.base = base; + } + } + if (typeof attr.scale === 'string') { + const scale = parseFloat(attr.scale); + if (!isNaN(scale)) { + attr.scale = scale; + } + } + + yaml.attributes[`${attr.name.toLowerCase()}-base`] = attr.base || 0; + yaml.attributes[`${attr.name.toLowerCase()}-scale`] = attr.scale || 0; + }); + + return yaml; + }; + + public updateParent = (classes: FabledClass[]) => { + if (!this.parentStr) return; + this.parent = classes.find((c) => c.name === this.parentStr); + }; + + public load = (yaml: ClassYamlData) => { + if (yaml.name) this.name = yaml.name; + if (yaml['action-bar'] !== undefined) this.actionBar = yaml['action-bar']; + if (yaml.mana !== undefined) this.manaName = yaml.mana; + if (yaml.prefix !== undefined) this.prefix = yaml.prefix; + if (yaml.group) this.group = yaml.group; + if (yaml['max-level']) this.maxLevel = yaml['max-level']; + if (yaml.parent) this.parentStr = yaml.parent; + this.permission = parseBool(yaml['needs-permission']); + + if (yaml.attributes) { + const attributes = yaml.attributes; + this.health = { + name: 'health', + base: attributes['health-base'] ?? 20, + scale: attributes['health-scale'] ?? 1 + }; + this.mana = { + name: 'mana', + base: attributes['mana-base'] ?? 20, + scale: attributes['mana-scale'] ?? 1 + }; + + const map: { [key: string]: IAttribute } = {}; + for (const attrId of Object.keys(attributes)) { + const split = attrId.split('-'); + const name = split[0]; + if (map[name] || name === 'health' || name === 'mana') continue; + + map[name] = { name, base: attributes[`${name}-base`], scale: attributes[`${name}-scale`] }; + } + this.attributes = Object.values(map); + } + + if (yaml['mana-regen']) this.manaRegen = yaml['mana-regen']; + if (yaml['skill-tree']) this.skillTree = toProperCase(yaml['skill-tree']); + if (yaml.blacklist) this.unusableItems = yaml.blacklist; + if (yaml.skills) + this.skills = ( + yaml.skills.map((s) => skillStore.getSkill(s)).filter((s) => !!s) + ); + if (yaml.icon) this.icon.material = toEditorCase(yaml.icon); + if (yaml['icon-data']) this.icon.customModelData = yaml['icon-data']; + if (yaml['icon-lore']) this.icon.lore = yaml['icon-lore']; + if (yaml['exp-source'] !== null) this.expSources = yaml['exp-source']; + + if (yaml['combo-starters']) { + // Combo starters + const combos = yaml['combo-starters']; + if (combos) { + this.lInverted = parseBool(combos.L?.inverted); + this.rInverted = parseBool(combos.R?.inverted); + this.lsInverted = parseBool(combos.LS?.inverted); + this.rsInverted = parseBool(combos.RS?.inverted); + this.sInverted = parseBool(combos.S?.inverted); + this.pInverted = parseBool(combos.P?.inverted); + this.qInverted = parseBool(combos.Q?.inverted); + this.fInverted = parseBool(combos.F?.inverted); + this.lWhitelist = combos.L?.whitelist || []; + this.rWhitelist = combos.R?.whitelist || []; + this.lsWhitelist = combos.LS?.whitelist || []; + this.rsWhitelist = combos.RS?.whitelist || []; + this.sWhitelist = combos.S?.whitelist || []; + this.pWhitelist = combos.P?.whitelist || []; + this.qWhitelist = combos.Q?.whitelist || []; + this.fWhitelist = combos.F?.whitelist || []; + } + } + + this.loaded = true; + this.save(); + }; + + public save = () => { + if (!this.name) return; + + if (this.location === 'server') { + return; + } + + this.changed(); + + void savePersistedClass(this.name, this.serializeYaml(), this.previousName || undefined).then( + (result) => { + if (!result.ok) { + console.error(this.name + ' Save error', result.error); + saveError.set({ + name: this.name, + message: getPersistenceFailureMessage(result) + }); + return; + } + + this.previousName = this.name; + if (get(saveError)?.name === this.name) { + saveError.set(undefined); + } + + console.log('Saved ' + this.name + ' 😎'); + } + ); + }; } class ClassStoreSvelte { - isLegacy = false; - - private loadClassesFromServer = async () => { - let serverClasses: string[]; - try { - serverClasses = await socketService.getClasses(); - } catch (_) { - return; - } - - const tempFolders = get(this.classFolders); - const tempClasses = get(this.classes); - serverClasses.forEach(c => { - const parts = c.split('/'); - const name = parts.pop(); - if (!name) return; - - let previous: FabledFolder | undefined; - let folder: FabledFolder | undefined; - parts.forEach(part => { - folder = previous ? previous.getSubfolder(part) : tempFolders.find(f => f.name === part); - if (!folder) { - folder = new FabledFolder(); - folder.name = part; - folder.location = 'server'; - if (previous) { - previous.add(folder); - folder.updateParent(previous); - } - } - if (!previous && !tempFolders.includes(folder)) tempFolders.push(folder); - previous = folder; - }); - - // If we already have this class, don't add it - if (tempClasses.find(cl => cl.name === c)) return; - - const clazz = new FabledClass({ name, location: 'server' }); - if (folder) folder.add(clazz); - tempClasses.push(clazz); - }); - this.classes.set(tempClasses); - }; - - private removeServerClasses = () => { - const tempClasses = get(this.classes); - this.classes.set(tempClasses.filter(c => c.location !== 'server')); - - const tempFolders = get(this.classFolders); - tempFolders.filter(f => f.location === 'server').forEach(f => this.deleteClassFolder(f, (sb) => sb.location === 'server')); - }; - - constructor() { - socketService.onConnect(this.loadClassesFromServer); - socketService.onDisconnect(this.removeServerClasses); - - if (this.isLegacy) { - get(this.classes).forEach(clazz => { - if (clazz.location === 'local') clazz.save(); - }); - this.persistClasses(); - } - } - - private loadClassTextToArray = (text: string): FabledClass[] => { - const list: FabledClass[] = []; - // Load classes - const data = parseYaml(text); - const keys = Object.keys(data); - - let clazz: FabledClass; - // If we only have one class, and it is the current YAML, - // the structure is a bit different - if (keys.length == 1) { - const key = keys[0]; - if (key === 'loaded') return list; - clazz = new FabledClass({ name: key }); - clazz.load(data[key]); - list.push(clazz); - return list; - } - - for (const key of Object.keys(data)) { - if (key != 'loaded') { - clazz = new FabledClass({ name: key }); - clazz.load(data[key]); - list.push(clazz); - } - } - return list; - }; - - private setupClassStore = (key: string, - def: T, - mapper: (data: string) => T, - setAction: (data: T) => T, - postLoad?: (saved: T) => void): Writable => { - let saved: T = def; - if (browser) { - const stored = localStorage.getItem(key); - if (stored) { - saved = mapper(stored); - if (postLoad) postLoad(saved); - } - } - - const { - subscribe, - set, - update - } = writable(saved); - return { - subscribe, - set: (value: T) => { - if (setAction) value = setAction(value); - return set(value); - }, - update - }; - }; - - classes: Writable = this.setupClassStore( - browser && localStorage.getItem('classNames') ? 'classNames' : 'classData', [], - (data: string) => { - if (localStorage.getItem('classNames')) { - return data.split(', ').map(name => new FabledClass({ - name, - location: 'local' - })).filter(cl => localStorage.getItem('sapi.class.' + cl.name)); - } else { - localStorage.removeItem('classData'); - this.isLegacy = true; - return sort(this.loadClassTextToArray(data)); - } - }, - (value: FabledClass[]) => { - this.persistClasses(value); - value.forEach(c => c.updateParent(value)); - return sort(value); - }, - (saved: FabledClass[]) => saved.forEach(c => c.updateParent(saved))); // This will be the gotcha here - - getClass = (name: string): FabledClass | undefined => { - for (const c of get(this.classes)) { - if (c.name == name) return c; - } - - return undefined; - }; - - classFolders: Writable = this.setupClassStore('classFolders', [], - (data: string) => { - if (!data || data === 'null') return []; - - try { - return JSON.parse(data, (key: string, value) => { - if (!value) return; - if (/\d+/.test(key)) { - if (typeof (value) === 'string') { - return this.getClass(value); - } - - const folder = new FabledFolder(value.data); - folder.name = value.name; - return folder; - } - return value; - }); - } catch (e) { - console.error('Error loading class folders. Folder data: ' + data, e); - notify('Error loading class folders. ' + JSON.stringify(e) + '\nFolder data: ' + data); - return []; - } - }, - (value: FabledFolder[]) => { - const data = JSON.stringify(value, (key, value: FabledFolder | FabledClass | FabledSkill) => { - if (value instanceof FabledClass || value instanceof FabledSkill) return value.name; - else if (key === 'parent') return undefined; - return value; - }); - localStorage.setItem('classFolders', data); - return sort(value); - }); - - updateAllAttributes = (attributes: string[]) => - get(this.classes).forEach(c => c.updateAttributes(attributes)); - - isClassNameTaken = (name: string): boolean => !!this.getClass(name); - - addClass = (name?: string): FabledClass => { - const cl = get(this.classes); - let index = cl.length + 1; - while (!name && this.isClassNameTaken(name || 'Class ' + index)) { - index++; - } - const clazz = new FabledClass({ name: (name || 'Class ' + index) }); - cl.push(clazz); - - this.classes.set(cl); - clazz.save(); - return clazz; - }; - - loadClass = async (data: FabledClass) => { - if (data.loaded) return; - let yamlData: MultiClassYamlData; - - if (data.location === 'local') { - yamlData = parseYaml(localStorage.getItem(`sapi.class.${data.name}`) || ''); - } else { - const yaml = await socketService.getClassYaml(data.name); - if (!yaml) return; - yamlData = YAML.parse(yaml); - } - - if (yamlData === null || Object.values(yamlData).length == 0) { - console.warn(`Failed to parse yaml for class ${data.name}`, localStorage.getItem(`sapi.class.${data.name}`)); - return; - } - - const clazz = Object.values(yamlData)[0]; - data.load(clazz); - - data.updateParent(get(this.classes)); - data.loaded = true; - }; - - cloneClass = async (data: FabledClass): Promise => { - if (!data.loaded) await this.loadClass(data); - - const cl: FabledClass[] = get(this.classes); - let name = data.name + ' (Copy)'; - let i = 1; - while (this.isClassNameTaken(name)) { - name = data.name + ' (Copy ' + i + ')'; - i++; - } - const clazz = new FabledClass(); - const yamlData = data.serializeYaml(); - clazz.load(yamlData); - clazz.name = name; - cl.push(clazz); - - this.classes.set(cl); - clazz.save(); - return clazz; - }; - - addClassFolder = (folder: FabledFolder) => { - const folders = get(this.classFolders); - if (folders.includes(folder)) return; - - folderStore.rename(folder, folders); - - folders.push(folder); - folders.sort((a, b) => a.name.localeCompare(b.name)); - this.classFolders.set(folders); - }; - - deleteClassFolder = (folder: FabledFolder, deleteCheck?: (subfolder: FabledFolder) => boolean) => { - const folders = get(this.classFolders).filter(f => f != folder); - - folder.data.forEach(d => { - if (d instanceof FabledFolder) { - if (deleteCheck && deleteCheck(d)) { - this.deleteClassFolder(d, deleteCheck); - return; - } - if (folder.parent) folder.parent.add(d); - else { - d.updateParent(); - folders.push(d); - } - } else if (folder.parent) - folder.parent.add(d); // Add the class to the parent folder - }); - - this.classFolders.set(folders); - }; - - deleteClass = (data: FabledClass) => { - const filtered = get(this.classes).filter(c => c != data); - const act = get(active); - this.classes.set(filtered); - localStorage.removeItem('sapi.class.' + data.name); - - if (!(act instanceof FabledClass)) return; - - if (filtered.length === 0) goto(`${base}/`); - else if (!filtered.find(cl => cl === get(active))) goto(`${base}/class/${filtered[0].name}/edit`).then(() => { - }); - }; - - refreshClasses = () => this.classes.set(sort(get(this.classes))); - refreshClassFolders = () => { - this.classFolders.set(sort(get(this.classFolders))); - this.refreshClasses(); - }; - - - /** - * Loads class data from a string - */ - loadClassText = (text: string, fromServer: boolean = false) => { - // Load new classes - const data = parseYaml(text); - - if (!data || Object.keys(data).length === 0) { - // If there is no data or the object is empty... return - return; - } - - const keys = Object.keys(data); - - let clazz: FabledClass; - // If we only have one class, and it is the current YAML, - // the structure is a bit different - if (keys.length == 1) { - const key: string = keys[0]; - clazz = ((this.isClassNameTaken(key) - ? this.getClass(key) - : this.addClass(key))); - if (fromServer) clazz.location = 'server'; - clazz.load(data[key]); - this.refreshClasses(); - return; - } - - for (const key of Object.keys(data)) { - if (key != 'loaded' && !this.isClassNameTaken(key)) { - clazz = ((this.isClassNameTaken(key) - ? this.getClass(key) - : this.addClass(key))); - clazz.load(data[key]); - } - } - this.refreshClasses(); - }; - - loadClasses = (e: ProgressEvent) => { - const text: string = e.target?.result; - if (!text) return; - - this.loadClassText(text); - }; - - persistClasses = (list?: FabledClass[]) => { - const classList = (list || get(this.classes)).filter(c => c.location === 'local'); - localStorage.setItem('classNames', classList.map(c => c.name).join(', ')); - }; + isLegacy = false; + + private loadClassesFromServer = async () => { + let serverClasses: string[]; + try { + serverClasses = await socketService.getClasses(); + } catch (_) { + return; + } + + const tempFolders = get(this.classFolders); + const tempClasses = get(this.classes); + serverClasses.forEach((c) => { + const parts = c.split('/'); + const name = parts.pop(); + if (!name) return; + + let previous: FabledFolder | undefined; + let folder: FabledFolder | undefined; + parts.forEach((part) => { + folder = previous ? previous.getSubfolder(part) : tempFolders.find((f) => f.name === part); + if (!folder) { + folder = new FabledFolder(); + folder.name = part; + folder.location = 'server'; + if (previous) { + previous.add(folder); + folder.updateParent(previous); + } + } + if (!previous && !tempFolders.includes(folder)) tempFolders.push(folder); + previous = folder; + }); + + // If we already have this class, don't add it + if (tempClasses.find((cl) => cl.name === name)) return; + + const clazz = new FabledClass({ name, location: 'server' }); + if (folder) folder.add(clazz); + tempClasses.push(clazz); + }); + this.classes.set(tempClasses); + }; + + private removeServerClasses = () => { + const tempClasses = get(this.classes); + this.classes.set(tempClasses.filter((c) => c.location !== 'server')); + + const tempFolders = get(this.classFolders); + tempFolders + .filter((f) => f.location === 'server') + .forEach((f) => this.deleteClassFolder(f, (sb) => sb.location === 'server')); + }; + + constructor() { + socketService.onConnect(this.loadClassesFromServer); + socketService.onDisconnect(this.removeServerClasses); + } + + private loadClassTextToArray = (text: string): FabledClass[] => { + const list: FabledClass[] = []; + // Load classes + const data = parseYaml(text); + const keys = Object.keys(data); + + let clazz: FabledClass; + // If we only have one class, and it is the current YAML, + // the structure is a bit different + if (keys.length == 1) { + const key = keys[0]; + if (key === 'loaded') return list; + clazz = new FabledClass({ name: key }); + clazz.load(data[key]); + list.push(clazz); + return list; + } + + for (const key of Object.keys(data)) { + if (key != 'loaded') { + clazz = new FabledClass({ name: key }); + clazz.load(data[key]); + list.push(clazz); + } + } + return list; + }; + + private setupClassStore = ( + _key: string, + def: T, + mapper: (data: string) => T, + setAction: (data: T) => T, + postLoad?: (saved: T) => void + ): Writable => { + let saved: T = def; + if (postLoad) postLoad(saved); + + const { subscribe, set, update } = writable(saved); + return { + subscribe, + set: (value: T) => { + if (setAction) value = setAction(value); + return set(value); + }, + update + }; + }; + + private deserializeClassFolders = (data: string | FolderProperties[]): FabledFolder[] => { + const serialized = typeof data === 'string' ? data : JSON.stringify(data); + if (!serialized || serialized === 'null') return []; + + try { + return JSON.parse(serialized, (key: string, value) => { + if (value == null) return; + if (/\d+/.test(key)) { + if (typeof value === 'string') { + return this.getClass(value); + } + + const folder = new FabledFolder(value.data); + folder.name = value.name; + folder.location = value.location || 'local'; + folder.open = !!value.open; + return folder; + } + return value; + }); + } catch (e) { + console.error('Error loading class folders. Folder data: ' + serialized, e); + notify('Error loading class folders. ' + JSON.stringify(e) + '\nFolder data: ' + serialized); + return []; + } + }; + + hydratePersistedData = async () => { + const classes = listPersistedClassNames().map( + (name) => + new FabledClass({ + name, + location: 'local' + }) + ); + + this.classes.set(sort(classes)); + this.classFolders.set( + sort(this.deserializeClassFolders(getPersistedFolders('class'))) + ); + }; + + classes: Writable = this.setupClassStore( + 'classes', + [], + (_data: string) => [], + (value: FabledClass[]) => { + this.persistClasses(value); + value.forEach((c) => c.updateParent(value)); + return sort(value); + }, + (saved: FabledClass[]) => saved.forEach((c) => c.updateParent(saved)) + ); // This will be the gotcha here + + getClass = (name: string): FabledClass | undefined => { + for (const c of get(this.classes)) { + if (c.name == name) return c; + } + + return undefined; + }; + + classFolders: Writable = this.setupClassStore( + 'class-folders', + [], + (_data: string) => [], + (value: FabledFolder[]) => { + void savePersistedFolders( + 'class', + value.filter((folder) => folder.location === 'local').map((folder) => folder.toJSON()) + ).then((result) => { + if (result.ok) { + if (get(saveError)?.name === 'Classes') { + saveError.set(undefined); + } + return; + } + console.error('Class folder save error', result.error); + saveError.set({ + name: 'Classes', + message: getPersistenceFailureMessage(result) + }); + }); + return sort(value); + } + ); + + updateAllAttributes = (attributes: string[]) => + get(this.classes).forEach((c) => c.updateAttributes(attributes)); + + isClassNameTaken = (name: string): boolean => !!this.getClass(name); + + addClass = (name?: string): FabledClass => { + const cl = get(this.classes); + let index = cl.length + 1; + while (!name && this.isClassNameTaken(name || 'Class ' + index)) { + index++; + } + const clazz = new FabledClass({ name: name || 'Class ' + index }); + cl.push(clazz); + + this.classes.set(cl); + clazz.save(); + return clazz; + }; + + loadClass = async (data: FabledClass) => { + if (data.loaded) return; + + if (data.location === 'local') { + const yamlData = await getPersistedClass(data.name); + if (!yamlData) return; + data.load(yamlData); + } else { + const yaml = await socketService.getClassYaml(data.name); + if (!yaml) return; + const yamlData = parseYaml(yaml); + if (yamlData === null || Object.values(yamlData).length == 0) { + console.warn(`Failed to parse yaml for class ${data.name}`, yaml); + return; + } + + const clazz = Object.values(yamlData)[0]; + data.load(clazz); + } + + data.updateParent(get(this.classes)); + data.loaded = true; + }; + + cloneClass = async (data: FabledClass): Promise => { + if (!data.loaded) await this.loadClass(data); + + const cl: FabledClass[] = get(this.classes); + let name = data.name + ' (Copy)'; + let i = 1; + while (this.isClassNameTaken(name)) { + name = data.name + ' (Copy ' + i + ')'; + i++; + } + const clazz = new FabledClass(); + const yamlData = data.serializeYaml(); + clazz.load(yamlData); + clazz.name = name; + cl.push(clazz); + + this.classes.set(cl); + clazz.save(); + return clazz; + }; + + addClassFolder = (folder: FabledFolder) => { + const folders = get(this.classFolders); + if (folders.includes(folder)) return; + + folderStore.rename(folder, folders); + + folders.push(folder); + folders.sort((a, b) => a.name.localeCompare(b.name)); + this.classFolders.set(folders); + }; + + deleteClassFolder = ( + folder: FabledFolder, + deleteCheck?: (subfolder: FabledFolder) => boolean + ) => { + const folders = get(this.classFolders).filter((f) => f != folder); + + folder.data.forEach((d) => { + if (d instanceof FabledFolder) { + if (deleteCheck && deleteCheck(d)) { + this.deleteClassFolder(d, deleteCheck); + return; + } + if (folder.parent) folder.parent.add(d); + else { + d.updateParent(); + folders.push(d); + } + } else if (folder.parent) folder.parent.add(d); // Add the class to the parent folder + }); + + this.classFolders.set(folders); + }; + + deleteClass = (data: FabledClass) => { + const filtered = get(this.classes).filter((c) => c != data); + const act = get(active); + this.classes.set(filtered); + void deletePersistedClass(data.name); + + if (!(act instanceof FabledClass)) return; + + if (filtered.length === 0) goto(`${base}/`); + else if (!filtered.find((cl) => cl === get(active))) + goto(`${base}/class/${filtered[0].name}/edit`).then(() => {}); + }; + + refreshClasses = () => this.classes.set(sort(get(this.classes))); + refreshClassFolders = () => { + this.classFolders.set(sort(get(this.classFolders))); + this.refreshClasses(); + }; + + /** + * Loads class data from a string + */ + loadClassText = (text: string, fromServer: boolean = false) => { + // Load new classes + const data = parseYaml(text); + + if (!data || Object.keys(data).length === 0) { + // If there is no data or the object is empty... return + return; + } + + const keys = Object.keys(data); + + let clazz: FabledClass; + // If we only have one class, and it is the current YAML, + // the structure is a bit different + if (keys.length == 1) { + const key: string = keys[0]; + clazz = (this.isClassNameTaken(key) ? this.getClass(key) : this.addClass(key)); + if (fromServer) clazz.location = 'server'; + clazz.load(data[key]); + this.refreshClasses(); + return; + } + + for (const key of Object.keys(data)) { + if (key != 'loaded' && !this.isClassNameTaken(key)) { + clazz = (this.isClassNameTaken(key) ? this.getClass(key) : this.addClass(key)); + clazz.load(data[key]); + } + } + this.refreshClasses(); + }; + + loadClasses = (e: ProgressEvent) => { + const text: string = e.target?.result; + if (!text) return; + + this.loadClassText(text); + }; + + persistClasses = (_list?: FabledClass[]) => {}; } export const classStore = new ClassStoreSvelte(); diff --git a/src/data/editor-persistence-db.ts b/src/data/editor-persistence-db.ts new file mode 100644 index 0000000000..79e854b2e7 --- /dev/null +++ b/src/data/editor-persistence-db.ts @@ -0,0 +1,206 @@ +import { browser } from '$app/environment'; +import { deleteDB, openDB, type IDBPDatabase } from 'idb'; +import type { PersistenceWriteResult } from './persistence-state'; +import { isStorageQuotaError } from './persistence-state'; +import { + ATTRIBUTES_STORE, + CLASSES_STORE, + CLASS_FOLDERS_KEY, + DB_NAME, + DB_VERSION, + type EditorPersistenceSchema, + type EntityStoreName, + type MetaRecord, + META_STORE, + MIGRATION_KEY, + normalizeForPersistence, + type PersistedAttributeRecord, + type PersistedClassRecord, + type PersistedSkillRecord, + type ReplaceEditorDataInput, + SKILLS_STORE, + SKILL_FOLDERS_KEY, + type StoreName +} from './editor-persistence-shared'; + +export interface LoadedEditorData { + skills: PersistedSkillRecord[]; + classes: PersistedClassRecord[]; + attributes: PersistedAttributeRecord[]; + meta: MetaRecord[]; +} + +let databasePromise: Promise> | undefined; + +const createStorageResult = (error?: unknown): PersistenceWriteResult => ({ + ok: !error, + quotaExceeded: !!error && isStorageQuotaError(error), + error +}); + +export const openEditorDatabase = (): Promise> => { + if (!browser || typeof indexedDB === 'undefined') { + return Promise.reject(new Error('IndexedDB is unavailable.')); + } + + if (!databasePromise) { + databasePromise = openDB(DB_NAME, DB_VERSION, { + upgrade(db) { + if (!db.objectStoreNames.contains(SKILLS_STORE)) { + db.createObjectStore(SKILLS_STORE, { keyPath: 'name' }); + } + if (!db.objectStoreNames.contains(CLASSES_STORE)) { + db.createObjectStore(CLASSES_STORE, { keyPath: 'name' }); + } + if (!db.objectStoreNames.contains(ATTRIBUTES_STORE)) { + db.createObjectStore(ATTRIBUTES_STORE, { keyPath: 'name' }); + } + if (!db.objectStoreNames.contains(META_STORE)) { + db.createObjectStore(META_STORE, { keyPath: 'key' }); + } + } + }); + } + + return databasePromise; +}; + +const putAllIndexedDbRecords = async < + T extends MetaRecord | PersistedSkillRecord | PersistedClassRecord | PersistedAttributeRecord +>( + db: IDBPDatabase, + storeName: StoreName, + records: T[] +): Promise => { + const transaction = db.transaction(storeName, 'readwrite'); + const store = transaction.store; + records.forEach((record) => store.put(normalizeForPersistence(record))); + await transaction.done; +}; + +const deleteIndexedDbKeys = async ( + db: IDBPDatabase, + storeName: StoreName, + keys: string[] +): Promise => { + if (keys.length === 0) return; + + const transaction = db.transaction(storeName, 'readwrite'); + const store = transaction.store; + keys.forEach((key) => store.delete(key)); + await transaction.done; +}; + +const syncIndexedDbEntityStore = async < + T extends PersistedSkillRecord | PersistedClassRecord | PersistedAttributeRecord +>( + db: IDBPDatabase, + storeName: EntityStoreName, + records: T[], + existingNames: string[] +): Promise => { + const incomingNames = new Set(records.map((record) => record.name)); + await deleteIndexedDbKeys( + db, + storeName, + existingNames.filter((name) => !incomingNames.has(name)) + ); + await putAllIndexedDbRecords(db, storeName, records); +}; + +export const loadEditorDbData = async ( + db: IDBPDatabase +): Promise => { + const [skills, classes, attributes, meta] = await Promise.all([ + db.getAll(SKILLS_STORE), + db.getAll(CLASSES_STORE), + db.getAll(ATTRIBUTES_STORE), + db.getAll(META_STORE) + ]); + + return { + skills, + classes, + attributes, + meta + }; +}; + +export const replaceIndexedDbData = async ( + db: IDBPDatabase, + data: ReplaceEditorDataInput, + existing: { + skills: string[]; + classes: string[]; + attributes: string[]; + } +): Promise => { + await syncIndexedDbEntityStore(db, SKILLS_STORE, data.skills, existing.skills); + await syncIndexedDbEntityStore(db, CLASSES_STORE, data.classes, existing.classes); + await syncIndexedDbEntityStore(db, ATTRIBUTES_STORE, data.attributes, existing.attributes); + await putAllIndexedDbRecords(db, META_STORE, [ + { key: SKILL_FOLDERS_KEY, value: data.skillFolders }, + { key: CLASS_FOLDERS_KEY, value: data.classFolders }, + { key: MIGRATION_KEY, value: true } + ]); +}; + +export const writeIndexedDbRecord = async ( + storeName: EntityStoreName, + record: PersistedSkillRecord | PersistedClassRecord | PersistedAttributeRecord, + previousName?: string +): Promise => { + try { + const db = await openEditorDatabase(); + const transaction = db.transaction(storeName, 'readwrite'); + const store = transaction.store; + const cloneableRecord = normalizeForPersistence(record); + if (previousName && previousName !== record.name) { + store.delete(previousName); + } + store.put(cloneableRecord); + await transaction.done; + return createStorageResult(); + } catch (error) { + return createStorageResult(error); + } +}; + +export const writeIndexedDbMeta = async ( + key: string, + value: T +): Promise => { + try { + const db = await openEditorDatabase(); + const cloneableRecord = normalizeForPersistence({ key, value }); + await db.put(META_STORE, cloneableRecord); + return createStorageResult(); + } catch (error) { + return createStorageResult(error); + } +}; + +export const deleteIndexedDbRecord = async ( + storeName: EntityStoreName, + name: string +): Promise => { + const db = await openEditorDatabase(); + await db.delete(storeName, name); +}; + +export const resetEditorDatabaseForTests = async () => { + if (!browser || typeof indexedDB === 'undefined') { + databasePromise = undefined; + return; + } + + const db = await databasePromise?.catch(() => undefined); + db?.close(); + databasePromise = undefined; + + await deleteDB(DB_NAME, { + blocked() { + return; + } + }); +}; diff --git a/src/data/editor-persistence-legacy.ts b/src/data/editor-persistence-legacy.ts new file mode 100644 index 0000000000..102c496313 --- /dev/null +++ b/src/data/editor-persistence-legacy.ts @@ -0,0 +1,208 @@ +import { browser } from '$app/environment'; +import type { + AttributeYamlData, + ClassYamlData, + MultiAttributeYamlData, + MultiClassYamlData, + MultiSkillYamlData, + SkillYamlData +} from '$api/types'; +import { parseYaml } from '$api/yaml'; +import type { FolderProperties } from './folder-store.svelte'; +import type { + PersistedAttributeRecord, + PersistedClassRecord, + PersistedSkillRecord, + ReplaceEditorDataInput +} from './editor-persistence-shared'; +import { + CLASS_FOLDERS_KEY, + SKILL_FOLDERS_KEY +} from './editor-persistence-shared'; + +const SKILL_PREFIX = 'sapi.skill.'; +const CLASS_PREFIX = 'sapi.class.'; + +const defaultAttributeYaml = (name: string): AttributeYamlData => ({ + display: name, + max: 999, + cost_base: 1, + cost_modifier: 0, + icon: 'Ink sac', + 'icon-data': 0, + 'icon-lore': [], + global: { + target: {}, + condition: {}, + mechanic: {} + }, + stats: {} +}); + +const normalizeMultiYamlRecords = ( + data: MultiSkillYamlData | MultiClassYamlData | undefined +): Array<{ name: string; data: T }> => { + if (!data) return []; + + return Object.entries(data) + .filter(([name]) => name !== 'loaded') + .map(([name, value]) => ({ + name, + data: value as T + })); +}; + +const getLegacyNamedKeys = (prefix: string, metadataKey: string): string[] => { + if (!browser) return []; + + const names = localStorage.getItem(metadataKey); + if (names) { + return names + .split(', ') + .map((name) => name.trim()) + .filter((name) => name.length > 0); + } + + const fromKeys: string[] = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (!key?.startsWith(prefix)) continue; + fromKeys.push(key.substring(prefix.length)); + } + return fromKeys; +}; + +const readLegacySkillRecords = (): PersistedSkillRecord[] => { + if (!browser) return []; + + const names = getLegacyNamedKeys(SKILL_PREFIX, 'skillNames'); + if (names.length > 0) { + return names + .map((name) => { + const stored = localStorage.getItem(`${SKILL_PREFIX}${name}`); + if (!stored) return undefined; + const parsed = parseYaml(stored) as MultiSkillYamlData | undefined; + const record = normalizeMultiYamlRecords(parsed)[0]; + if (!record) return undefined; + return { name: record.name, data: record.data }; + }) + .filter((record): record is PersistedSkillRecord => !!record); + } + + const legacyData = localStorage.getItem('skillData'); + if (!legacyData) return []; + return normalizeMultiYamlRecords(parseYaml(legacyData) as MultiSkillYamlData).map( + (record) => ({ + name: record.name, + data: record.data + }) + ); +}; + +const readLegacyClassRecords = (): PersistedClassRecord[] => { + if (!browser) return []; + + const names = getLegacyNamedKeys(CLASS_PREFIX, 'classNames'); + if (names.length > 0) { + return names + .map((name) => { + const stored = localStorage.getItem(`${CLASS_PREFIX}${name}`); + if (!stored) return undefined; + const parsed = parseYaml(stored) as MultiClassYamlData | undefined; + const record = normalizeMultiYamlRecords(parsed)[0]; + if (!record) return undefined; + return { name: record.name, data: record.data }; + }) + .filter((record): record is PersistedClassRecord => !!record); + } + + const legacyData = localStorage.getItem('classData'); + if (!legacyData) return []; + return normalizeMultiYamlRecords(parseYaml(legacyData) as MultiClassYamlData).map( + (record) => ({ + name: record.name, + data: record.data + }) + ); +}; + +const readLegacyAttributeRecords = (): PersistedAttributeRecord[] => { + if (!browser) return []; + + const stored = localStorage.getItem('attribs'); + if (!stored) return []; + + if (stored.split('\n').length < 3 && stored.charAt(0) !== '{') { + return stored + .replace('\n', '') + .split(',') + .map((name) => name.trim()) + .filter((name) => name.length > 0) + .map((name) => ({ + name, + data: defaultAttributeYaml(name) + })); + } + + const parsed = parseYaml(stored) as MultiAttributeYamlData | undefined; + if (!parsed) return []; + + return Object.entries(parsed).map(([name, data]) => ({ + name, + data + })); +}; + +const parseFolderMeta = (key: string): FolderProperties[] => { + if (!browser) return []; + + const stored = localStorage.getItem(key); + if (!stored || stored === 'null') return []; + + try { + return JSON.parse(stored) as FolderProperties[]; + } catch (error) { + console.error(`Failed to parse ${key} from localStorage`, error); + return []; + } +}; + +export const hasLegacyEditorData = () => { + if (!browser) return false; + + return ( + getLegacyNamedKeys(SKILL_PREFIX, 'skillNames').length > 0 || + getLegacyNamedKeys(CLASS_PREFIX, 'classNames').length > 0 || + !!localStorage.getItem('skillData') || + !!localStorage.getItem('classData') || + !!localStorage.getItem('attribs') || + !!localStorage.getItem(SKILL_FOLDERS_KEY) || + !!localStorage.getItem(CLASS_FOLDERS_KEY) + ); +}; + +export const collectLegacyEditorData = (): ReplaceEditorDataInput => ({ + skills: readLegacySkillRecords(), + classes: readLegacyClassRecords(), + attributes: readLegacyAttributeRecords(), + skillFolders: parseFolderMeta(SKILL_FOLDERS_KEY), + classFolders: parseFolderMeta(CLASS_FOLDERS_KEY) +}); + +export const clearLegacyEditorStorage = () => { + if (!browser) return; + + const skillNames = getLegacyNamedKeys(SKILL_PREFIX, 'skillNames'); + const classNames = getLegacyNamedKeys(CLASS_PREFIX, 'classNames'); + + skillNames.forEach((name) => localStorage.removeItem(`${SKILL_PREFIX}${name}`)); + classNames.forEach((name) => localStorage.removeItem(`${CLASS_PREFIX}${name}`)); + + localStorage.removeItem('skillNames'); + localStorage.removeItem('classNames'); + localStorage.removeItem('skillData'); + localStorage.removeItem('classData'); + localStorage.removeItem('attribs'); + localStorage.removeItem(SKILL_FOLDERS_KEY); + localStorage.removeItem(CLASS_FOLDERS_KEY); +}; diff --git a/src/data/editor-persistence-shared.ts b/src/data/editor-persistence-shared.ts new file mode 100644 index 0000000000..8cf5b9bc07 --- /dev/null +++ b/src/data/editor-persistence-shared.ts @@ -0,0 +1,74 @@ +import type { + AttributeYamlData, + ClassYamlData, + SkillYamlData +} from '$api/types'; +import type { DBSchema } from 'idb'; +import type { FolderProperties } from './folder-store.svelte'; + +export const DB_NAME = 'fabled-editor'; +export const DB_VERSION = 1; + +export const SKILLS_STORE = 'skills'; +export const CLASSES_STORE = 'classes'; +export const ATTRIBUTES_STORE = 'attributes'; +export const META_STORE = 'meta'; + +export const MIGRATION_KEY = 'editor-storage-migrated'; +export const SKILL_FOLDERS_KEY = 'skillFolders'; +export const CLASS_FOLDERS_KEY = 'classFolders'; + +export type PersistenceMode = 'indexeddb' | 'unsupported'; + +export interface PersistedSkillRecord { + name: string; + data: SkillYamlData; +} + +export interface PersistedClassRecord { + name: string; + data: ClassYamlData; +} + +export interface PersistedAttributeRecord { + name: string; + data: AttributeYamlData; +} + +export interface MetaRecord { + key: string; + value: T; +} + +export interface ReplaceEditorDataInput { + skills: PersistedSkillRecord[]; + classes: PersistedClassRecord[]; + attributes: PersistedAttributeRecord[]; + skillFolders: FolderProperties[]; + classFolders: FolderProperties[]; +} + +export interface EditorPersistenceSchema extends DBSchema { + [SKILLS_STORE]: { + key: string; + value: PersistedSkillRecord; + }; + [CLASSES_STORE]: { + key: string; + value: PersistedClassRecord; + }; + [ATTRIBUTES_STORE]: { + key: string; + value: PersistedAttributeRecord; + }; + [META_STORE]: { + key: string; + value: MetaRecord; + }; +} + +export type EntityStoreName = typeof SKILLS_STORE | typeof CLASSES_STORE | typeof ATTRIBUTES_STORE; +export type StoreName = EntityStoreName | typeof META_STORE; + +export const normalizeForPersistence = (value: T): T => + JSON.parse(JSON.stringify(value)) as T; diff --git a/src/data/editor-persistence-unsupported.test.ts b/src/data/editor-persistence-unsupported.test.ts new file mode 100644 index 0000000000..9a5b4a3dbd --- /dev/null +++ b/src/data/editor-persistence-unsupported.test.ts @@ -0,0 +1,60 @@ +import 'fake-indexeddb/auto'; + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { SkillYamlData } from '$api/types'; + +vi.mock('$app/environment', () => ({ + browser: true +})); + +const skillData: SkillYamlData = { + name: 'Meteor', + type: 'Dynamic', + 'max-level': 5, + 'skill-req': '', + 'skill-req-lvl': 0, + 'needs-permission': false, + 'cooldown-message': true, + msg: 'cast', + combo: '', + icon: 'stone', + 'icon-data': 0, + 'icon-lore': [], + attributes: { + 'level-base': 1, + 'level-scale': 0, + 'cost-base': 1, + 'cost-scale': 0, + 'cooldown-base': 1, + 'cooldown-scale': 0, + 'mana-base': 0, + 'mana-scale': 0, + 'points-spent-req-base': 0, + 'points-spent-req-scale': 0 + }, + incompatible: [], + components: {} +}; + +describe('editor persistence unsupported browser handling', () => { + beforeEach(() => { + vi.resetModules(); + vi.stubGlobal('indexedDB', undefined); + localStorage.clear(); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('does not fall back to localStorage when IndexedDB is unavailable', async () => { + const persistence = await import('./editor-persistence'); + const result = await persistence.savePersistedSkill('Meteor', skillData); + + expect(result.ok).toBe(false); + expect(persistence.getEditorPersistenceMode()).toBe('unsupported'); + expect(localStorage.getItem('skillNames')).toBeNull(); + expect(localStorage.getItem('sapi.skill.Meteor')).toBeNull(); + expect(persistence.listPersistedSkillNames()).toEqual([]); + }); +}); diff --git a/src/data/editor-persistence.test.ts b/src/data/editor-persistence.test.ts new file mode 100644 index 0000000000..06683eebf7 --- /dev/null +++ b/src/data/editor-persistence.test.ts @@ -0,0 +1,223 @@ +import 'fake-indexeddb/auto'; + +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import YAML from 'yaml'; +import type { AttributeYamlData, ClassYamlData, SkillYamlData } from '$api/types'; +import { ATTRIBUTES_STORE, SKILLS_STORE } from './editor-persistence-shared'; + +vi.mock('$app/environment', () => ({ + browser: true +})); + +const skillData: SkillYamlData = { + name: 'Meteor', + type: 'Dynamic', + 'max-level': 5, + 'skill-req': '', + 'skill-req-lvl': 0, + 'needs-permission': false, + 'cooldown-message': true, + msg: 'cast', + combo: '', + icon: 'stone', + 'icon-data': 0, + 'icon-lore': [], + attributes: { + 'level-base': 1, + 'level-scale': 0, + 'cost-base': 1, + 'cost-scale': 0, + 'cooldown-base': 1, + 'cooldown-scale': 0, + 'mana-base': 0, + 'mana-scale': 0, + 'points-spent-req-base': 0, + 'points-spent-req-scale': 0 + }, + incompatible: [], + components: {} +}; + +const classData: ClassYamlData = { + name: 'Mage', + 'action-bar': '', + prefix: '&6Mage', + group: 'class', + mana: '&2Mana', + 'max-level': 40, + parent: '', + 'needs-permission': false, + attributes: { + 'health-base': 20, + 'health-scale': 1, + 'mana-base': 20, + 'mana-scale': 1 + }, + 'mana-regen': 1, + 'skill-tree': 'REQUIREMENT', + blacklist: [], + skills: ['Meteor'], + icon: 'stone', + 'icon-data': 0, + 'icon-lore': [], + 'exp-source': 273, + 'combo-starters': {} +}; + +const attributeData: AttributeYamlData = { + display: 'Spirit', + max: 999, + cost_base: 1, + cost_modifier: 0, + icon: 'Ink sac', + 'icon-data': 0, + 'icon-lore': [], + global: { + target: {}, + condition: {}, + mechanic: {} + }, + stats: {} +}; + +describe('editor persistence', () => { + beforeEach(async () => { + localStorage.clear(); + const persistence = await import('./editor-persistence'); + await persistence.resetEditorPersistenceForTests(); + }); + + it('migrates legacy localStorage editor data into IndexedDB-backed cache', async () => { + localStorage.setItem('skillNames', 'Meteor'); + localStorage.setItem( + 'sapi.skill.Meteor', + YAML.stringify({ Meteor: skillData }, { lineWidth: 0, aliasDuplicateObjects: false }) + ); + localStorage.setItem('classNames', 'Mage'); + localStorage.setItem( + 'sapi.class.Mage', + YAML.stringify({ Mage: classData }, { lineWidth: 0, aliasDuplicateObjects: false }) + ); + localStorage.setItem( + 'attribs', + YAML.stringify({ Spirit: attributeData }, { lineWidth: 0, aliasDuplicateObjects: false }) + ); + localStorage.setItem( + 'skillFolders', + JSON.stringify([ + { + location: 'local', + dataType: 'folder', + name: 'Magic', + data: ['Meteor'], + open: false + } + ]) + ); + + const persistence = await import('./editor-persistence'); + await persistence.ensureEditorPersistence(); + + expect(persistence.getEditorPersistenceMode()).toBe('indexeddb'); + expect(persistence.listPersistedSkillNames()).toEqual(['Meteor']); + expect(persistence.listPersistedClassNames()).toEqual(['Mage']); + expect(persistence.listPersistedAttributeRecords()).toEqual([ + { name: 'Spirit', data: attributeData } + ]); + expect(persistence.getPersistedFolders('skill')).toEqual([ + { + location: 'local', + dataType: 'folder', + name: 'Magic', + data: ['Meteor'], + open: false + } + ]); + expect(localStorage.getItem('skillNames')).toBeNull(); + expect(localStorage.getItem('sapi.skill.Meteor')).toBeNull(); + }); + + it('normalizes proxy-backed class data into structured-clone-safe values', async () => { + const persistence = await import('./editor-persistence'); + const proxiedClassData: ClassYamlData = { + ...classData, + blacklist: new Proxy(['stick'], {}), + 'icon-lore': new Proxy(['Line 1'], {}), + 'combo-starters': { + L: { + inverted: true, + whitelist: new Proxy(['wand'], {}) + } + } + }; + + expect(() => structuredClone({ name: 'Mage', data: proxiedClassData })).toThrow(); + + const normalized = persistence.normalizeForPersistence({ + name: 'Mage', + data: proxiedClassData + }); + + expect(() => structuredClone(normalized)).not.toThrow(); + expect(normalized).toEqual({ + name: 'Mage', + data: { + ...classData, + blacklist: ['stick'], + 'icon-lore': ['Line 1'], + 'combo-starters': { + L: { + inverted: true, + whitelist: ['wand'] + } + } + } + }); + expect(Array.isArray(normalized.data.blacklist)).toBe(true); + expect(Array.isArray(normalized.data['icon-lore'])).toBe(true); + expect(normalized.data['combo-starters']).toEqual({ + L: { + inverted: true, + whitelist: ['wand'] + } + }); + expect(normalized.data).toEqual({ + ...classData, + blacklist: ['stick'], + 'icon-lore': ['Line 1'], + 'combo-starters': { + L: { + inverted: true, + whitelist: ['wand'] + } + } + }); + }); + + it('does not overwrite newer persisted skills when saving attributes', async () => { + const persistence = await import('./editor-persistence'); + const updatedSkillData: SkillYamlData = { + ...skillData, + msg: 'updated' + }; + const { openEditorDatabase, writeIndexedDbRecord } = await import('./editor-persistence-db'); + + await persistence.savePersistedSkill('Meteor', skillData); + await writeIndexedDbRecord(SKILLS_STORE, { + name: 'Meteor', + data: updatedSkillData + }); + + await persistence.savePersistedAttributes([{ name: 'Spirit', data: attributeData }]); + + const db = await openEditorDatabase(); + expect(await db.get(SKILLS_STORE, 'Meteor')).toEqual({ + name: 'Meteor', + data: updatedSkillData + }); + expect(await db.get(ATTRIBUTES_STORE, 'Spirit')).toEqual({ + name: 'Spirit', + data: attributeData + }); + }); +}); diff --git a/src/data/editor-persistence.ts b/src/data/editor-persistence.ts new file mode 100644 index 0000000000..0fa68bdb77 --- /dev/null +++ b/src/data/editor-persistence.ts @@ -0,0 +1,369 @@ +import { browser } from '$app/environment'; +import { writable } from 'svelte/store'; +import { + clearLegacyEditorStorage, + collectLegacyEditorData, + hasLegacyEditorData +} from './editor-persistence-legacy'; +import { + deleteIndexedDbRecord, + loadEditorDbData, + openEditorDatabase, + replaceIndexedDbData, + resetEditorDatabaseForTests, + writeIndexedDbMeta, + writeIndexedDbRecord +} from './editor-persistence-db'; +import { + ATTRIBUTES_STORE, + CLASS_FOLDERS_KEY, + CLASSES_STORE, + MIGRATION_KEY, + normalizeForPersistence, + type PersistedAttributeRecord, + type PersistenceMode, + type ReplaceEditorDataInput, + SKILL_FOLDERS_KEY, + SKILLS_STORE +} from './editor-persistence-shared'; +import type { AttributeYamlData, ClassYamlData, SkillYamlData } from '$api/types'; +import type { FolderProperties } from './folder-store.svelte'; +import type { PersistenceWriteResult } from './persistence-state'; +import { isStorageQuotaError } from './persistence-state'; + +const cache = { + skills: new Map(), + classes: new Map(), + attributes: new Map(), + meta: new Map() +}; + +export const editorPersistenceUnsupported = writable(null); + +let persistenceMode: PersistenceMode = 'indexeddb'; +let initializationPromise: Promise | undefined; + +const resetCache = () => { + cache.skills.clear(); + cache.classes.clear(); + cache.attributes.clear(); + cache.meta.clear(); +}; + +const unsupportedPersistenceError = (cause?: unknown) => + new Error( + cause instanceof Error && cause.message + ? `IndexedDB is unavailable in this browser: ${cause.message}` + : 'IndexedDB is unavailable in this browser.' + ); + +const loadCache = async () => { + const db = await openEditorDatabase(); + const data = await loadEditorDbData(db); + + resetCache(); + data.skills.forEach((record) => cache.skills.set(record.name, record.data)); + data.classes.forEach((record) => cache.classes.set(record.name, record.data)); + data.attributes.forEach((record) => cache.attributes.set(record.name, record.data)); + data.meta.forEach((record) => cache.meta.set(record.key, record.value)); +}; + +const replacePersistedAttributeCache = (records: PersistedAttributeRecord[]) => { + cache.attributes.clear(); + records.forEach((record) => cache.attributes.set(record.name, record.data)); +}; + +const migrateLegacyLocalStorage = async (): Promise => { + if (!browser || persistenceMode !== 'indexeddb') return; + if (cache.meta.get(MIGRATION_KEY)) return; + + if (!hasLegacyEditorData()) { + await writeIndexedDbMeta(MIGRATION_KEY, true); + cache.meta.set(MIGRATION_KEY, true); + return; + } + + const data = collectLegacyEditorData(); + const db = await openEditorDatabase(); + await replaceIndexedDbData(db, data, { + skills: [...cache.skills.keys()], + classes: [...cache.classes.keys()], + attributes: [...cache.attributes.keys()] + }); + clearLegacyEditorStorage(); +}; + +export const ensureEditorPersistence = async (): Promise => { + if (!browser) return 'unsupported'; + if (initializationPromise) { + await initializationPromise; + return persistenceMode; + } + + initializationPromise = (async () => { + editorPersistenceUnsupported.set(null); + + if (typeof indexedDB === 'undefined') { + persistenceMode = 'unsupported'; + resetCache(); + editorPersistenceUnsupported.set('This browser does not support IndexedDB persistence.'); + return; + } + + try { + await loadCache(); + await migrateLegacyLocalStorage(); + await loadCache(); + } catch (error) { + console.error('IndexedDB unavailable for editor persistence.', error); + persistenceMode = 'unsupported'; + resetCache(); + editorPersistenceUnsupported.set( + error instanceof Error && error.message + ? `IndexedDB persistence is unavailable: ${error.message}` + : 'IndexedDB persistence is unavailable in this browser.' + ); + } + })(); + + await initializationPromise; + return persistenceMode; +}; + +export const getEditorPersistenceMode = (): PersistenceMode => persistenceMode; + +export const listPersistedSkillNames = (): string[] => + [...cache.skills.keys()].sort((left, right) => left.localeCompare(right)); + +export const listPersistedClassNames = (): string[] => + [...cache.classes.keys()].sort((left, right) => left.localeCompare(right)); + +export const listPersistedAttributeRecords = (): PersistedAttributeRecord[] => + [...cache.attributes.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .map(([name, data]) => ({ name, data })); + +export const getPersistedSkill = async (name: string): Promise => { + await ensureEditorPersistence(); + return cache.skills.get(name); +}; + +export const getPersistedClass = async (name: string): Promise => { + await ensureEditorPersistence(); + return cache.classes.get(name); +}; + +export const getPersistedAttribute = async ( + name: string +): Promise => { + await ensureEditorPersistence(); + return cache.attributes.get(name); +}; + +export const getPersistedFolders = (type: 'skill' | 'class'): FolderProperties[] => + ( + (cache.meta.get( + type === 'skill' ? SKILL_FOLDERS_KEY : CLASS_FOLDERS_KEY + ) as FolderProperties[]) || [] + ).map((folder) => structuredClone(folder)); + +const unsupportedResult = (): PersistenceWriteResult => ({ + ok: false, + quotaExceeded: false, + error: unsupportedPersistenceError() +}); + +export const savePersistedSkill = async ( + name: string, + data: SkillYamlData, + previousName?: string +): Promise => { + await ensureEditorPersistence(); + if (persistenceMode !== 'indexeddb') { + return unsupportedResult(); + } + + const result = await writeIndexedDbRecord(SKILLS_STORE, { name, data }, previousName); + if (result.ok) { + if (previousName && previousName !== name) cache.skills.delete(previousName); + cache.skills.set(name, normalizeForPersistence(data)); + } + return result; +}; + +export const savePersistedClass = async ( + name: string, + data: ClassYamlData, + previousName?: string +): Promise => { + await ensureEditorPersistence(); + if (persistenceMode !== 'indexeddb') { + return unsupportedResult(); + } + + const result = await writeIndexedDbRecord(CLASSES_STORE, { name, data }, previousName); + if (result.ok) { + if (previousName && previousName !== name) cache.classes.delete(previousName); + cache.classes.set(name, normalizeForPersistence(data)); + } + return result; +}; + +export const savePersistedAttributes = async ( + records: PersistedAttributeRecord[] +): Promise => { + await ensureEditorPersistence(); + if (persistenceMode !== 'indexeddb') { + return unsupportedResult(); + } + + try { + const db = await openEditorDatabase(); + const normalizedRecords = normalizeForPersistence(records); + const transaction = db.transaction(ATTRIBUTES_STORE, 'readwrite'); + const store = transaction.store; + const incomingNames = new Set(normalizedRecords.map((record) => record.name)); + + [...cache.attributes.keys()] + .filter((name) => !incomingNames.has(name)) + .forEach((name) => store.delete(name)); + normalizedRecords.forEach((record) => store.put(record)); + + await transaction.done; + replacePersistedAttributeCache(normalizedRecords); + return { ok: true, quotaExceeded: false }; + } catch (error) { + return { ok: false, quotaExceeded: isStorageQuotaError(error), error }; + } +}; + +export const savePersistedFolders = async ( + type: 'skill' | 'class', + folders: FolderProperties[] +): Promise => { + await ensureEditorPersistence(); + if (persistenceMode !== 'indexeddb') { + return unsupportedResult(); + } + + const key = type === 'skill' ? SKILL_FOLDERS_KEY : CLASS_FOLDERS_KEY; + const result = await writeIndexedDbMeta(key, folders); + if (result.ok) { + cache.meta.set(key, normalizeForPersistence(folders)); + } + return result; +}; + +export const deletePersistedSkill = async (name: string): Promise => { + await ensureEditorPersistence(); + cache.skills.delete(name); + if (persistenceMode !== 'indexeddb') return; + await deleteIndexedDbRecord(SKILLS_STORE, name); +}; + +export const deletePersistedClass = async (name: string): Promise => { + await ensureEditorPersistence(); + cache.classes.delete(name); + if (persistenceMode !== 'indexeddb') return; + await deleteIndexedDbRecord(CLASSES_STORE, name); +}; + +export const deletePersistedAttribute = async (name: string): Promise => { + await ensureEditorPersistence(); + const previousAttribute = cache.attributes.get(name); + cache.attributes.delete(name); + if (persistenceMode !== 'indexeddb') return; + + try { + const result = await savePersistedAttributes(listPersistedAttributeRecords()); + if (!result.ok) { + if (previousAttribute !== undefined) { + cache.attributes.set(name, previousAttribute); + } + throw new Error(`Failed to persist deletion of attribute "${name}"`); + } + } catch (error) { + if (previousAttribute !== undefined && !cache.attributes.has(name)) { + cache.attributes.set(name, previousAttribute); + } + throw error; + } +}; + +export const replacePersistedEditorData = async (data: ReplaceEditorDataInput): Promise => { + await ensureEditorPersistence(); + if (persistenceMode !== 'indexeddb') { + throw unsupportedPersistenceError(); + } + + const db = await openEditorDatabase(); + await replaceIndexedDbData(db, data, { + skills: [...cache.skills.keys()], + classes: [...cache.classes.keys()], + attributes: [...cache.attributes.keys()] + }); + await loadCache(); +}; + +export const importLegacyMigrationData = async (input: { + skillData: string; + classData: string; + attributes: string; + skillFolders: string; + classFolders: string; +}): Promise => { + const { parseYaml } = await import('$api/yaml'); + const skills = Object.entries((parseYaml(input.skillData) as Record) || {}) + .filter(([name]) => name !== 'loaded') + .map(([name, data]) => ({ + name, + data + })); + const classes = Object.entries( + (parseYaml(input.classData) as Record) || {} + ) + .filter(([name]) => name !== 'loaded') + .map(([name, data]) => ({ + name, + data + })); + const attributes = Object.entries( + (parseYaml(input.attributes) as Record) || {} + ).map(([name, data]) => ({ + name, + data + })); + + let skillFolders: FolderProperties[] = []; + let classFolders: FolderProperties[] = []; + try { + skillFolders = input.skillFolders ? (JSON.parse(input.skillFolders) as FolderProperties[]) : []; + } catch (_) { + skillFolders = []; + } + + try { + classFolders = input.classFolders ? (JSON.parse(input.classFolders) as FolderProperties[]) : []; + } catch (_) { + classFolders = []; + } + + await replacePersistedEditorData({ + skills, + classes, + attributes, + skillFolders, + classFolders + }); +}; + +export { normalizeForPersistence }; + +export const resetEditorPersistenceForTests = async () => { + resetCache(); + initializationPromise = undefined; + persistenceMode = 'indexeddb'; + editorPersistenceUnsupported.set(null); + + await resetEditorDatabaseForTests(); +}; diff --git a/src/data/editor-session.ts b/src/data/editor-session.ts new file mode 100644 index 0000000000..dbd3617696 --- /dev/null +++ b/src/data/editor-session.ts @@ -0,0 +1,26 @@ +import { browser } from '$app/environment'; +import { attributeStore } from './attribute-store'; +import { classStore } from './class-store.svelte'; +import { ensureEditorPersistence } from './editor-persistence'; +import { skillStore } from './skill-store.svelte'; + +let hydrationPromise: Promise | undefined; + +export const hydrateEditorData = async (): Promise => { + if (!browser) return; + + if (!hydrationPromise) { + hydrationPromise = (async () => { + await ensureEditorPersistence(); + await skillStore.hydratePersistedData(); + await classStore.hydratePersistedData(); + await attributeStore.hydratePersistedData(); + })(); + } + + await hydrationPromise; +}; + +export const resetEditorHydrationForTests = () => { + hydrationPromise = undefined; +}; diff --git a/src/data/persistence-state.test.ts b/src/data/persistence-state.test.ts new file mode 100644 index 0000000000..90953e34d1 --- /dev/null +++ b/src/data/persistence-state.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; +import { getPersistenceFailureMessage, isStorageQuotaError } from './persistence-state'; + +describe('storage helpers', () => { + it('recognizes quota exceeded errors by name and message', () => { + expect(isStorageQuotaError(new DOMException('Quota exceeded', 'QuotaExceededError'))).toBe( + true + ); + expect(isStorageQuotaError({ message: 'The quota has been exceeded.' })).toBe(true); + expect(isStorageQuotaError(new Error('disk full'))).toBe(false); + }); +}); + +describe('persistence failure messaging', () => { + it('describes quota failures in user-facing terms', () => { + expect( + getPersistenceFailureMessage({ + ok: false, + quotaExceeded: true + }) + ).toBe('Browser storage is full. Export before refreshing or closing this page.'); + }); + + it('describes non-quota failures generically', () => { + expect( + getPersistenceFailureMessage({ + ok: false, + quotaExceeded: false + }) + ).toBe( + "The editor couldn't persist this change to browser storage. Your latest edits remain only in memory until you refresh or close this page." + ); + }); +}); diff --git a/src/data/persistence-state.ts b/src/data/persistence-state.ts new file mode 100644 index 0000000000..df2f1764d4 --- /dev/null +++ b/src/data/persistence-state.ts @@ -0,0 +1,37 @@ +// Shared helpers for browser-persistence failures. IndexedDB owns editor storage, but +// the UI still needs a consistent way to classify failures and explain them to users. +export interface PersistenceSaveError { + name: string; + message: string; +} + +export interface PersistenceWriteResult { + ok: boolean; + quotaExceeded: boolean; + error?: unknown; +} + +const storageQuotaNames = new Set(['QuotaExceededError', 'NS_ERROR_DOM_QUOTA_REACHED']); +const storageQuotaCodes = new Set([22, 1014]); + +export const isStorageQuotaError = (error: unknown): boolean => { + if (!error || typeof error !== 'object') return false; + + const maybeDomException = error as { name?: string; code?: number; message?: string }; + if (maybeDomException.name && storageQuotaNames.has(maybeDomException.name)) { + return true; + } + + if (typeof maybeDomException.code === 'number' && storageQuotaCodes.has(maybeDomException.code)) { + return true; + } + + if (typeof maybeDomException.message !== 'string') return false; + + return maybeDomException.message.toLowerCase().includes('quota'); +}; + +export const getPersistenceFailureMessage = (result: PersistenceWriteResult): string => + result.quotaExceeded + ? 'Browser storage is full. Export before refreshing or closing this page.' + : "The editor couldn't persist this change to browser storage. Your latest edits remain only in memory until you refresh or close this page."; diff --git a/src/data/skill-store.svelte.ts b/src/data/skill-store.svelte.ts index 0d2988a3e7..cefb4d1e5c 100644 --- a/src/data/skill-store.svelte.ts +++ b/src/data/skill-store.svelte.ts @@ -1,710 +1,704 @@ import type { Unsubscriber, Writable } from 'svelte/store'; -import { get, writable } from 'svelte/store'; -import { sort, toEditorCase } from '$api/api'; -import { parseYaml } from '$api/yaml'; -import { browser } from '$app/environment'; -import { active, saveError } from './store'; -import { goto } from '$app/navigation'; -import { base } from '$app/paths'; -import Registry, { initialized } from '$api/components/registry'; +import { get, writable } from 'svelte/store'; +import { sort, toEditorCase } from '$api/api'; +import { parseYaml } from '$api/yaml'; +import { active, saveError } from './store'; +import { goto } from '$app/navigation'; +import { base } from '$app/paths'; +import Registry, { initialized } from '$api/components/registry'; import type { - FabledSkillData, - IAttribute, - Icon, - MultiSkillYamlData, - Serializable, - SkillYamlData, - YamlComponentData -} from '$api/types'; -import { socketService } from '$api/socket/socket-connector'; -import { notify } from '$api/notification-service'; -import FabledTrigger from '$api/components/triggers.svelte'; -import type FabledComponent from '$api/components/fabled-component.svelte'; -import { FabledFolder, folderStore } from './folder-store.svelte'; -import YAML from 'yaml'; + FabledSkillData, + IAttribute, + Icon, + MultiSkillYamlData, + Serializable, + SkillYamlData, + YamlComponentData +} from '$api/types'; +import { socketService } from '$api/socket/socket-connector'; +import { notify } from '$api/notification-service'; +import FabledTrigger from '$api/components/triggers.svelte'; +import type FabledComponent from '$api/components/fabled-component.svelte'; +import { FabledFolder, folderStore, type FolderProperties } from './folder-store.svelte'; +import { getPersistenceFailureMessage } from './persistence-state'; +import { + deletePersistedSkill, + getPersistedFolders, + getPersistedSkill, + listPersistedSkillNames, + savePersistedFolders, + savePersistedSkill +} from './editor-persistence'; export default class FabledSkill implements Serializable { - dataType = 'skill'; - location: 'local' | 'server' = 'local'; - loaded = false; - tooBig = false; - acknowledged = false; - - isSkill = true; - public key = {}; - name: string = $state(''); - previousName: string = ''; - type = $state('Dynamic'); - maxLevel = $state(5); - skillReq?: FabledSkill = $state(); - skillReqLevel = $state(0); - attributeRequirements: IAttribute[] = $state([]); - permission: boolean = $state(false); - levelReq: IAttribute = $state({ name: 'level', base: 1, scale: 0 }); - cost: IAttribute = $state({ name: 'cost', base: 1, scale: 0 }); - cooldown: IAttribute = $state({ name: 'cooldown', base: 1, scale: 0 }); - cooldownMessage: boolean = $state(true); - mana: IAttribute = $state({ name: 'mana', base: 0, scale: 0 }); - minSpent: IAttribute = $state({ name: 'points-spent-req', base: 0, scale: 0 }); - castMessage = $state('&6{player} &2has cast &6{skill}'); - combo = $state(''); - icon: Icon = $state({ - material: 'Pumpkin', - customModelData: 0, - lore: [ - '&d{name} &7({level}/{max})', - '&2Type: &6{type}', - '', - '{req:level}Level: {attr:level}', - '{req:cost}Cost: {attr:cost}', - '', - '&2Mana: {attr:mana}', - '&2Cooldown: {attr:cooldown}' - ] - }); - incompatible: FabledSkill[] = $state([]); - triggers: FabledTrigger[] = $state([]); - - private skillReqStr = ''; - private incompStr: string[] = []; - - constructor(data?: FabledSkillData) { - this.name = data?.name || 'Skill'; - if (!data) return; - if (data.location) this.location = data.location; - if (data.type) this.type = data.type; - if (data.maxLevel) this.maxLevel = data.maxLevel; - if (data.skillReq) this.skillReq = data.skillReq; - if (data.skillReqLevel) this.skillReqLevel = data.skillReqLevel; - if (data.attributeRequirements) this.attributeRequirements = data.attributeRequirements.map(a => ({ - name: a.name, - base: a.base, - scale: a.scale - })); - if (data.permission !== undefined) this.permission = data.permission; - if (data.levelReq) this.levelReq = data.levelReq; - if (data.cost) this.cost = data.cost; - if (data.cooldown) this.cooldown = data.cooldown; - if (data.cooldownMessage !== undefined) this.cooldownMessage = data.cooldownMessage; - if (data.mana) this.mana = data.mana; - if (data.minSpent) this.minSpent = data.minSpent; - if (data.castMessage) this.castMessage = data.castMessage; - if (data.combo) this.combo = data.combo; - if (data.icon) this.icon = data.icon; - if (data.incompatible) this.incompatible = data.incompatible; - if (data.triggers) this.triggers = data.triggers; - } - - /** - * Reads all the reactive state elements to act as a chane detector - */ - public changed = () => { - return { - name: this.name, - type: this.type, - 'max-level': this.maxLevel, - 'skill-req': this.skillReq?.name, - 'skill-req-lvl': this.skillReqLevel, - 'needs-permission': this.permission, - 'cooldown-message': this.cooldownMessage, - msg: this.castMessage, - combo: this.combo, - icon: this.icon.material, - 'icon-data': this.icon.customModelData, - 'icon-lore': this.icon.lore, - attributes: { - 'level-base': this.levelReq.base, - 'level-scale': this.levelReq.scale, - 'cost-base': this.cost.base, - 'cost-scale': this.cost.scale, - 'cooldown-base': this.cooldown.base, - 'cooldown-scale': this.cooldown.scale, - 'mana-base': this.mana.base, - 'mana-scale': this.mana.scale, - 'points-spent-req-base': this.minSpent.base, - 'points-spent-req-scale': this.minSpent.scale - }, - incompatible: this.incompatible, - components: this.triggers - }; - }; - - public addComponent = (comp: FabledComponent) => { - if (comp instanceof FabledTrigger) { - this.triggers = [...this.triggers, comp]; - return; - } - - if (this.triggers.length === 0) { - this.triggers.push(Registry.getTriggerByName('cast')?.new()); - } - - this.triggers[0].addComponent(comp); - this.triggers = [...this.triggers]; - }; - - public removeComponent = (comp: FabledComponent) => { - if (comp instanceof FabledTrigger && this.triggers.includes(comp)) { - this.triggers.splice(this.triggers.indexOf(comp), 1); - return; - } - - for (const trigger of this.triggers) { - if (trigger.contains(comp)) - trigger.removeComponent(comp); - } - - this.triggers = [...this.triggers]; - }; - - private nextChar = (c: string) => { - if (/z$/.test(c)) { - return c.replaceAll(/z$/g, 'a') + 'a'; - } - return c.substring(0, c.length - 1) + String.fromCharCode(c.charCodeAt(c.length - 1) + 1); - }; - - public serializeYaml = (): SkillYamlData => { - const compData = {}; - - for (const comp of this.triggers) { - const yamlData = comp.toYamlObj(); - let name = comp.name; - let suffix = 'a'; - while (compData[name]) { - suffix = this.nextChar(suffix); - name = comp.name + '-' + suffix; - } - compData[name] = yamlData; - } - const data = { - name: this.name, - type: this.type, - 'max-level': this.maxLevel, - 'skill-req': this.skillReq?.name, - 'skill-req-lvl': this.skillReqLevel, - 'needs-permission': this.permission, - 'cooldown-message': this.cooldownMessage, - msg: this.castMessage, - combo: this.combo, - icon: this.icon.material, - 'icon-data': this.icon.customModelData, - 'icon-lore': this.icon.lore, - attributes: { - 'level-base': this.levelReq.base, - 'level-scale': this.levelReq.scale, - 'cost-base': this.cost.base, - 'cost-scale': this.cost.scale, - 'cooldown-base': this.cooldown.base, - 'cooldown-scale': this.cooldown.scale, - 'mana-base': this.mana.base, - 'mana-scale': this.mana.scale, - 'points-spent-req-base': this.minSpent.base, - 'points-spent-req-scale': this.minSpent.scale - }, - incompatible: this.incompatible.map(s => s.name), - components: compData - }; - - this.attributeRequirements.forEach(attr => { - data.attributes[`${attr.name.toLowerCase()}-base`] = attr.base; - data.attributes[`${attr.name.toLowerCase()}-scale`] = attr.scale; - }); - - return data; - }; - - public load = async (yaml: SkillYamlData) => { - if (yaml.name) this.name = yaml.name; - if (yaml.type) this.type = yaml.type; - if (yaml['max-level']) this.maxLevel = yaml['max-level']; - if (yaml['skill-req']) this.skillReqStr = yaml['skill-req']; - if (yaml['skill-req-lvl']) this.skillReqLevel = yaml['skill-req-lvl']; - if (yaml['needs-permission'] !== undefined) this.permission = yaml['needs-permission']; - if (yaml['cooldown-message'] !== undefined) this.cooldownMessage = yaml['cooldown-message']; - if (yaml.msg) this.castMessage = yaml.msg; - if (yaml.combo) this.combo = yaml.combo; - - if (yaml.attributes) { - const attributes = yaml.attributes; - this.levelReq = { name: 'level', base: attributes['level-base'], scale: attributes['level-scale'] }; - this.cost = { name: 'cost', base: attributes['cost-base'], scale: attributes['cost-scale'] }; - this.cooldown = { name: 'cooldown', base: attributes['cooldown-base'], scale: attributes['cooldown-scale'] }; - this.mana = { name: 'mana', base: attributes['mana-base'], scale: attributes['mana-scale'] }; - this.minSpent = { - name: 'points-spent-req', - base: attributes['points-spent-req-base'], - scale: attributes['points-spent-req-scale'] - }; - - const reserved = ['level', 'cost', 'cooldown', 'mana', 'points-spent-req', 'incompatible']; - const names = new Set(Object.keys(attributes).map(k => k.replace(/-(base|scale)/i, '')).filter(name => !reserved.includes(name))); - this.attributeRequirements = [...names].map(name => ({ - name, - base: attributes[`${name}-base`], - scale: attributes[`${name}-scale`] - })); - } - - if (yaml.incompatible) this.incompStr = yaml.incompatible; - - if (yaml.icon) this.icon.material = toEditorCase(yaml.icon); - if (yaml['icon-data']) this.icon.customModelData = yaml['icon-data']; - if (yaml['icon-lore']) this.icon.lore = yaml['icon-lore']; - - let unsub: Unsubscriber | undefined = undefined; - - return new Promise((resolve) => { - unsub = initialized.subscribe(init => { - if (!init) return; - if (yaml.components) this.triggers = Registry.deserializeComponents(yaml.components); - - if (unsub) { - unsub(); - } - - this.loaded = true; - resolve(); - }); - }); - }; - - public postLoad = () => { - this.skillReq = skillStore.getSkill(this.skillReqStr); - this.incompatible = this.incompStr.map(s => skillStore.getSkill(s)).filter(s => !!s); - }; - - private saveDebounceTimeout: number | undefined; - public save = () => { - if (!this.name || this.tooBig) return; - - if (this.tooBig && !this.acknowledged) { - saveError.set(this); - return; - } - - if (this.location === 'server') { - return; - } - - if (this.saveDebounceTimeout) { - window.clearTimeout(this.saveDebounceTimeout); - } - - this.changed(); - this.saveDebounceTimeout = window.setTimeout(() => { - skillStore.isSaving.set(true); - - if (this.previousName && this.previousName !== this.name) { - localStorage.removeItem('sapi.skill.' + this.previousName); - } - this.previousName = this.name; - - try { - const yaml = YAML.stringify({ [this.name]: this.serializeYaml() }, { - lineWidth: 0, - aliasDuplicateObjects: false - }); - localStorage.setItem('sapi.skill.' + this.name, yaml); - this.tooBig = false; - } catch (e: any) { - // If the data is too big - if (!e?.message?.includes('quota')) { - console.error(this.name + ' Save error', e); - } else { - localStorage.removeItem('sapi.skill.' + this.name); - this.tooBig = true; - saveError.set(this); - } - } - - this.saveDebounceTimeout = undefined; - skillStore.isSaving.set(false); - console.log('Saved ' + this.name + ' 😎'); - }, 600); // Adjust the debounce delay as needed - }; + dataType = 'skill'; + location: 'local' | 'server' = 'local'; + loaded = false; + + isSkill = true; + public key = {}; + name: string = $state(''); + previousName: string = ''; + type = $state('Dynamic'); + maxLevel = $state(5); + skillReq?: FabledSkill = $state(); + skillReqLevel = $state(0); + attributeRequirements: IAttribute[] = $state([]); + permission: boolean = $state(false); + levelReq: IAttribute = $state({ name: 'level', base: 1, scale: 0 }); + cost: IAttribute = $state({ name: 'cost', base: 1, scale: 0 }); + cooldown: IAttribute = $state({ name: 'cooldown', base: 1, scale: 0 }); + cooldownMessage: boolean = $state(true); + mana: IAttribute = $state({ name: 'mana', base: 0, scale: 0 }); + minSpent: IAttribute = $state({ name: 'points-spent-req', base: 0, scale: 0 }); + castMessage = $state('&6{player} &2has cast &6{skill}'); + combo = $state(''); + icon: Icon = $state({ + material: 'Pumpkin', + customModelData: 0, + lore: [ + '&d{name} &7({level}/{max})', + '&2Type: &6{type}', + '', + '{req:level}Level: {attr:level}', + '{req:cost}Cost: {attr:cost}', + '', + '&2Mana: {attr:mana}', + '&2Cooldown: {attr:cooldown}' + ] + }); + incompatible: FabledSkill[] = $state([]); + triggers: FabledTrigger[] = $state([]); + + private skillReqStr = ''; + private incompStr: string[] = []; + + constructor(data?: FabledSkillData) { + this.name = data?.name || 'Skill'; + if (!data) return; + if (data.location) this.location = data.location; + if (data.type) this.type = data.type; + if (data.maxLevel) this.maxLevel = data.maxLevel; + if (data.skillReq) this.skillReq = data.skillReq; + if (data.skillReqLevel) this.skillReqLevel = data.skillReqLevel; + if (data.attributeRequirements) + this.attributeRequirements = data.attributeRequirements.map((a) => ({ + name: a.name, + base: a.base, + scale: a.scale + })); + if (data.permission !== undefined) this.permission = data.permission; + if (data.levelReq) this.levelReq = data.levelReq; + if (data.cost) this.cost = data.cost; + if (data.cooldown) this.cooldown = data.cooldown; + if (data.cooldownMessage !== undefined) this.cooldownMessage = data.cooldownMessage; + if (data.mana) this.mana = data.mana; + if (data.minSpent) this.minSpent = data.minSpent; + if (data.castMessage) this.castMessage = data.castMessage; + if (data.combo) this.combo = data.combo; + if (data.icon) this.icon = data.icon; + if (data.incompatible) this.incompatible = data.incompatible; + if (data.triggers) this.triggers = data.triggers; + } + + /** + * Reads all the reactive state elements to act as a chane detector + */ + public changed = () => { + return { + name: this.name, + type: this.type, + 'max-level': this.maxLevel, + 'skill-req': this.skillReq?.name, + 'skill-req-lvl': this.skillReqLevel, + 'needs-permission': this.permission, + 'cooldown-message': this.cooldownMessage, + msg: this.castMessage, + combo: this.combo, + icon: this.icon.material, + 'icon-data': this.icon.customModelData, + 'icon-lore': this.icon.lore, + attributes: { + 'level-base': this.levelReq.base, + 'level-scale': this.levelReq.scale, + 'cost-base': this.cost.base, + 'cost-scale': this.cost.scale, + 'cooldown-base': this.cooldown.base, + 'cooldown-scale': this.cooldown.scale, + 'mana-base': this.mana.base, + 'mana-scale': this.mana.scale, + 'points-spent-req-base': this.minSpent.base, + 'points-spent-req-scale': this.minSpent.scale + }, + incompatible: this.incompatible, + components: this.triggers + }; + }; + + public addComponent = (comp: FabledComponent) => { + if (comp instanceof FabledTrigger) { + this.triggers = [...this.triggers, comp]; + return; + } + + if (this.triggers.length === 0) { + this.triggers.push(Registry.getTriggerByName('cast')?.new()); + } + + this.triggers[0].addComponent(comp); + this.triggers = [...this.triggers]; + }; + + public removeComponent = (comp: FabledComponent) => { + if (comp instanceof FabledTrigger && this.triggers.includes(comp)) { + this.triggers.splice(this.triggers.indexOf(comp), 1); + return; + } + + for (const trigger of this.triggers) { + if (trigger.contains(comp)) trigger.removeComponent(comp); + } + + this.triggers = [...this.triggers]; + }; + + private nextChar = (c: string) => { + if (/z$/.test(c)) { + return c.replaceAll(/z$/g, 'a') + 'a'; + } + return c.substring(0, c.length - 1) + String.fromCharCode(c.charCodeAt(c.length - 1) + 1); + }; + + public serializeYaml = (): SkillYamlData => { + const compData = {}; + + for (const comp of this.triggers) { + const yamlData = comp.toYamlObj(); + let name = comp.name; + let suffix = 'a'; + while (compData[name]) { + suffix = this.nextChar(suffix); + name = comp.name + '-' + suffix; + } + compData[name] = yamlData; + } + const data = { + name: this.name, + type: this.type, + 'max-level': this.maxLevel, + 'skill-req': this.skillReq?.name, + 'skill-req-lvl': this.skillReqLevel, + 'needs-permission': this.permission, + 'cooldown-message': this.cooldownMessage, + msg: this.castMessage, + combo: this.combo, + icon: this.icon.material, + 'icon-data': this.icon.customModelData, + 'icon-lore': this.icon.lore, + attributes: { + 'level-base': this.levelReq.base, + 'level-scale': this.levelReq.scale, + 'cost-base': this.cost.base, + 'cost-scale': this.cost.scale, + 'cooldown-base': this.cooldown.base, + 'cooldown-scale': this.cooldown.scale, + 'mana-base': this.mana.base, + 'mana-scale': this.mana.scale, + 'points-spent-req-base': this.minSpent.base, + 'points-spent-req-scale': this.minSpent.scale + }, + incompatible: this.incompatible.map((s) => s.name), + components: compData + }; + + this.attributeRequirements.forEach((attr) => { + data.attributes[`${attr.name.toLowerCase()}-base`] = attr.base; + data.attributes[`${attr.name.toLowerCase()}-scale`] = attr.scale; + }); + + return data; + }; + + public load = async (yaml: SkillYamlData) => { + if (yaml.name) this.name = yaml.name; + if (yaml.type) this.type = yaml.type; + if (yaml['max-level']) this.maxLevel = yaml['max-level']; + if (yaml['skill-req']) this.skillReqStr = yaml['skill-req']; + if (yaml['skill-req-lvl']) this.skillReqLevel = yaml['skill-req-lvl']; + if (yaml['needs-permission'] !== undefined) this.permission = yaml['needs-permission']; + if (yaml['cooldown-message'] !== undefined) this.cooldownMessage = yaml['cooldown-message']; + if (yaml.msg) this.castMessage = yaml.msg; + if (yaml.combo) this.combo = yaml.combo; + + if (yaml.attributes) { + const attributes = yaml.attributes; + this.levelReq = { + name: 'level', + base: attributes['level-base'], + scale: attributes['level-scale'] + }; + this.cost = { name: 'cost', base: attributes['cost-base'], scale: attributes['cost-scale'] }; + this.cooldown = { + name: 'cooldown', + base: attributes['cooldown-base'], + scale: attributes['cooldown-scale'] + }; + this.mana = { name: 'mana', base: attributes['mana-base'], scale: attributes['mana-scale'] }; + this.minSpent = { + name: 'points-spent-req', + base: attributes['points-spent-req-base'], + scale: attributes['points-spent-req-scale'] + }; + + const reserved = ['level', 'cost', 'cooldown', 'mana', 'points-spent-req', 'incompatible']; + const names = new Set( + Object.keys(attributes) + .map((k) => k.replace(/-(base|scale)/i, '')) + .filter((name) => !reserved.includes(name)) + ); + this.attributeRequirements = [...names].map((name) => ({ + name, + base: attributes[`${name}-base`], + scale: attributes[`${name}-scale`] + })); + } + + if (yaml.incompatible) this.incompStr = yaml.incompatible; + + if (yaml.icon) this.icon.material = toEditorCase(yaml.icon); + if (yaml['icon-data']) this.icon.customModelData = yaml['icon-data']; + if (yaml['icon-lore']) this.icon.lore = yaml['icon-lore']; + + let unsub: Unsubscriber | undefined = undefined; + + return new Promise((resolve) => { + unsub = initialized.subscribe((init) => { + if (!init) return; + if (yaml.components) + this.triggers = Registry.deserializeComponents(yaml.components); + + if (unsub) { + unsub(); + } + + this.loaded = true; + resolve(); + }); + }); + }; + + public postLoad = () => { + this.skillReq = skillStore.getSkill(this.skillReqStr); + this.incompatible = ( + this.incompStr.map((s) => skillStore.getSkill(s)).filter((s) => !!s) + ); + }; + + private saveDebounceTimeout: number | undefined; + public save = () => { + if (!this.name) return; + + if (this.location === 'server') { + return; + } + + if (this.saveDebounceTimeout) { + window.clearTimeout(this.saveDebounceTimeout); + } + + this.changed(); + this.saveDebounceTimeout = window.setTimeout(async () => { + skillStore.isSaving.set(true); + const result = await savePersistedSkill( + this.name, + this.serializeYaml(), + this.previousName || undefined + ); + if (!result.ok) { + console.error(this.name + ' Save error', result.error); + saveError.set({ + name: this.name, + message: getPersistenceFailureMessage(result) + }); + } else { + this.previousName = this.name; + if (get(saveError)?.name === this.name) { + saveError.set(undefined); + } + } + + this.saveDebounceTimeout = undefined; + skillStore.isSaving.set(false); + console.log('Saved ' + this.name + ' 😎'); + }, 600); // Adjust the debounce delay as needed + }; } class SkillStore { - isLegacy = false; - private loadSkillsFromServer = async () => { - let serverSkills: string[]; - try { - serverSkills = await socketService.getSkills(); - } catch (_) { - return; - } - - const tempFolders = get(this.skillFolders); - const tempSkills = get(this.skills); - // Skills come through with some sort of path before their name A/B/C/Skill - // We need to create folders for each of these - serverSkills.forEach(sk => { - const parts = sk.split('/'); - const name = parts.pop(); - if (!name) return; - - let previous: FabledFolder | undefined; - let folder: FabledFolder | undefined; - parts.forEach(part => { - folder = previous ? previous.getSubfolder(part) : tempFolders.find(f => f.name === part); - if (!folder) { - folder = new FabledFolder(); - folder.name = part; - folder.location = 'server'; - if (previous) { - previous.add(folder); - folder.updateParent(previous); - } - } - if (!previous && !tempFolders.includes(folder)) tempFolders.push(folder); - previous = folder; - }); - - // If we already have this skill, don't add it - if (tempSkills.find(sk => sk.name === name)) return; - - const skill = new FabledSkill({ name, location: 'server' }); - if (folder) folder.add(skill); - tempSkills.push(skill); - }); - - this.skills.set(tempSkills); - this.skillFolders.set(tempFolders); - }; - - private removeServerSkills = () => { - const tempSkills = get(this.skills); - this.skills.set(tempSkills.filter(c => c.location !== 'server')); - - const tempFolders = get(this.skillFolders); - tempFolders.filter(f => f.location === 'server').forEach(f => this.deleteSkillFolder(f, (sb) => sb.location === 'server')); - }; - - constructor() { - socketService.onConnect(this.loadSkillsFromServer); - socketService.onDisconnect(this.removeServerSkills); - - get(this.skills).forEach(sk => { - if (sk.loaded) { - sk.postLoad(); - } - }); - - if (this.isLegacy) { - const sub = initialized.subscribe(init => { - if (!init) return; - get(this.skills).forEach(sk => { - if (sk.location === 'local') sk.save(); - }); - this.persistSkills(); - if (sub) sub(); - }); - } - } - - private loadSkillTextToArray = (text: string): FabledSkill[] => { - const list: FabledSkill[] = []; - // Load skills - const data = parseYaml(text); - if (!data || Object.keys(data).length === 0) { - // If there is no data or the object is empty... return - return list; - } - - const keys = Object.keys(data); - - let skill: FabledSkill; - // If we only have one skill, and it is the current YAML, - // the structure is a bit different - if (keys.length == 1) { - const key = keys[0]; - if (key === 'loaded') return list; - skill = new FabledSkill({ name: key }); - skill.load(data[key]).then(() => { - }); - list.push(skill); - return list; - } - - for (const key of Object.keys(data)) { - if (key != 'loaded') { - skill = new FabledSkill({ name: key }); - skill.load(data[key]).then(() => { - }); - list.push(skill); - } - } - return list; - }; - - private setupSkillStore = (key: string, - def: T, - mapper: (data: string) => T, - setAction: (data: T) => T, - postLoad?: (saved: T) => void): Writable => { - let saved: T = def; - if (browser) { - const stored = localStorage.getItem(key); - if (stored) { - saved = mapper(stored); - if (postLoad) postLoad(saved); - } - } - - const { - subscribe, - set, - update - } = writable(saved); - return { - subscribe, - set: (value: T) => { - if (setAction) value = setAction(value); - return set(value); - }, - update - }; - }; - - skills: Writable = this.setupSkillStore( - browser && localStorage.getItem('skillNames') ? 'skillNames' : 'skillData', - [], - (data: string) => { - if (localStorage.getItem('skillNames')) { - return data.split(', ').map(name => new FabledSkill({ - name, - location: 'local' - })).filter(sk => localStorage.getItem('sapi.skill.' + sk.name)); - } else { - localStorage.removeItem('skillData'); - this.isLegacy = true; - return sort(this.loadSkillTextToArray(data)); - } - }, - (value: FabledSkill[]) => { - this.persistSkills(); - return sort(value); - }); - - getSkill = (name: string): FabledSkill | undefined => { - for (const c of get(this.skills)) { - if (c.name == name) return c; - } - - return undefined; - }; - - skillFolders: Writable = this.setupSkillStore('skillFolders', [], - (data: string) => { - if (!data || data === 'null') return []; - - try { - return JSON.parse(data, (key: string, value) => { - if (!value) return; - if (/\d+/.test(key)) { - if (typeof (value) === 'string') { - return this.getSkill(value); - } - - const folder = new FabledFolder(value.data); - folder.name = value.name; - return folder; - } - return value; - }); - } catch (e) { - console.error('Error loading skill folders. Folder data: ' + data, e); - notify('Error loading skill folders. ' + JSON.stringify(e) + '\nFolder data: ' + data); - return []; - } - }, - (value: FabledFolder[]) => { - const data = JSON.stringify(value, (key, value: FabledFolder | FabledSkill) => { - if (value instanceof FabledSkill) return value.name; - else if (key === 'parent') return undefined; - return value; - }); - localStorage.setItem('skillFolders', data); - return sort(value); - }); - - isSkillNameTaken = (name: string): boolean => !!this.getSkill(name); - - addSkill = (name?: string): FabledSkill => { - const allSkills = get(this.skills); - let index = allSkills.length + 1; - while (!name && this.isSkillNameTaken(name || 'Skill ' + index)) { - index++; - } - const skill = new FabledSkill({ name: (name || 'Skill ' + index) }); - allSkills.push(skill); - - this.skills.set(allSkills); - skill.save(); - return skill; - }; - - loadSkill = async (data: FabledSkill) => { - if (data.loaded) return; - let yamlData: MultiSkillYamlData; - - if (data.location === 'local') { - yamlData = parseYaml(localStorage.getItem(`sapi.skill.${data.name}`) || ''); - } else { - const yaml = await socketService.getSkillYaml(data.name); - if (!yaml) return; - - yamlData = parseYaml(yaml); - } - - // Get the first entry in the object - const skill = Object.values(yamlData)[0]; - await data.load(skill); - - data.postLoad(); - }; - - cloneSkill = async (data: FabledSkill): Promise => { - if (!data.loaded) await this.loadSkill(data); - - const sk: FabledSkill[] = get(this.skills); - let name = data.name + ' (Copy)'; - let i = 1; - while (this.isSkillNameTaken(name)) { - name = data.name + ' (Copy ' + i + ')'; - i++; - } - const skill = new FabledSkill(); - const yamlData = data.serializeYaml(); - await skill.load(yamlData); - skill.name = name; - sk.push(skill); - - this.skills.set(sk); - skill.save(); - return skill; - }; - - addSkillFolder = (folder: FabledFolder) => { - const folders = get(this.skillFolders); - if (folders.includes(folder)) return; - - folderStore.rename(folder, folders); - - folders.push(folder); - folders.sort((a, b) => a.name.localeCompare(b.name)); - this.skillFolders.set(folders); - }; - - - deleteSkillFolder = (folder: FabledFolder, deleteCheck?: (subfolder: FabledFolder) => boolean) => { - const folders = get(this.skillFolders).filter(f => f != folder); - - // If there are any subfolders or skills, move them to the parent or root - folder.data.forEach(d => { - if (d instanceof FabledFolder) { - if (deleteCheck && deleteCheck(d)) { - this.deleteSkillFolder(d, deleteCheck); - return; - } - if (folder.parent) folder.parent.add(d); - else { - d.updateParent(); - folders.push(d); - } - } else if (folder.parent) - folder.parent.add(d); // Add the skill to the parent folder - }); - - this.skillFolders.set(folders); - }; - - deleteSkill = (data: FabledSkill) => { - const filtered = get(this.skills).filter(c => c != data); - const act = get(active); - this.skills.set(filtered); - localStorage.removeItem('sapi.skill.' + data.name); - - if (!(act instanceof FabledSkill)) return; - - if (filtered.length === 0) goto(`${base}/`).then(() => { - }); - else if (!filtered.find(sk => sk === get(active))) goto(`${base}/skill/${filtered[0].name}`).then(() => { - }); - }; - - refreshSkills = () => this.skills.set(sort(get(this.skills))); - refreshSkillFolders = () => { - this.skillFolders.set(sort(get(this.skillFolders))); - this.refreshSkills(); - }; - - - /** - * Loads skill data from a string - */ - loadSkillText = async (text: string, fromServer: boolean = false) => { - // Load new skills - const data = parseYaml(text); - - if (!data || Object.keys(data).length === 0) { - // If there is no data or the object is empty... return - return; - } - - const keys = Object.keys(data); - - let skill: FabledSkill; - // If we only have one skill, and it is the current YAML, - // the structure is a bit different - if (keys.length == 1) { - const key: string = keys[0]; - skill = ((this.isSkillNameTaken(key) - ? this.getSkill(key) - : this.addSkill(key))); - if (fromServer) skill.location = 'server'; - await skill.load(data[key]); - skill.save(); - this.refreshSkills(); - return; - } - - for (const key of Object.keys(data)) { - if (key != 'loaded' && !this.isSkillNameTaken(key)) { - skill = ((this.isSkillNameTaken(key) - ? this.getSkill(key) - : this.addSkill(key))); - await skill.load(data[key]); - skill.save(); - } - } - this.refreshSkills(); - }; - - loadSkills = async (e: ProgressEvent) => { - const text: string = e.target?.result; - if (!text) return; - - await this.loadSkillText(text); - }; - - isSaving: Writable = writable(false); - saveTask: number = 0; - - persistSkills = (list?: FabledSkill[]) => { - if (get(this.isSaving) && this.saveTask) { - clearTimeout(this.saveTask); - } - - this.isSaving.set(true); - - this.saveTask = window.setTimeout(() => { - const skillList = (list || get(this.skills)).filter(sk => sk.location === 'local'); - localStorage.setItem('skillNames', skillList.map(sk => sk.name).join(', ')); - this.isSaving.set(false); - }); - }; + isLegacy = false; + private loadSkillsFromServer = async () => { + let serverSkills: string[]; + try { + serverSkills = await socketService.getSkills(); + } catch (_) { + return; + } + + const tempFolders = get(this.skillFolders); + const tempSkills = get(this.skills); + // Skills come through with some sort of path before their name A/B/C/Skill + // We need to create folders for each of these + serverSkills.forEach((sk) => { + const parts = sk.split('/'); + const name = parts.pop(); + if (!name) return; + + let previous: FabledFolder | undefined; + let folder: FabledFolder | undefined; + parts.forEach((part) => { + folder = previous ? previous.getSubfolder(part) : tempFolders.find((f) => f.name === part); + if (!folder) { + folder = new FabledFolder(); + folder.name = part; + folder.location = 'server'; + if (previous) { + previous.add(folder); + folder.updateParent(previous); + } + } + if (!previous && !tempFolders.includes(folder)) tempFolders.push(folder); + previous = folder; + }); + + // If we already have this skill, don't add it + if (tempSkills.find((sk) => sk.name === name)) return; + + const skill = new FabledSkill({ name, location: 'server' }); + if (folder) folder.add(skill); + tempSkills.push(skill); + }); + + this.skills.set(tempSkills); + this.skillFolders.set(tempFolders); + }; + + private removeServerSkills = () => { + const tempSkills = get(this.skills); + this.skills.set(tempSkills.filter((c) => c.location !== 'server')); + + const tempFolders = get(this.skillFolders); + tempFolders + .filter((f) => f.location === 'server') + .forEach((f) => this.deleteSkillFolder(f, (sb) => sb.location === 'server')); + }; + + constructor() { + socketService.onConnect(this.loadSkillsFromServer); + socketService.onDisconnect(this.removeServerSkills); + + get(this.skills).forEach((sk) => { + if (sk.loaded) { + sk.postLoad(); + } + }); + } + + private loadSkillTextToArray = (text: string): FabledSkill[] => { + const list: FabledSkill[] = []; + // Load skills + const data = parseYaml(text); + if (!data || Object.keys(data).length === 0) { + // If there is no data or the object is empty... return + return list; + } + + const keys = Object.keys(data); + + let skill: FabledSkill; + // If we only have one skill, and it is the current YAML, + // the structure is a bit different + if (keys.length == 1) { + const key = keys[0]; + if (key === 'loaded') return list; + skill = new FabledSkill({ name: key }); + skill.load(data[key]).then(() => {}); + list.push(skill); + return list; + } + + for (const key of Object.keys(data)) { + if (key != 'loaded') { + skill = new FabledSkill({ name: key }); + skill.load(data[key]).then(() => {}); + list.push(skill); + } + } + return list; + }; + + private setupSkillStore = ( + _key: string, + def: T, + mapper: (data: string) => T, + setAction: (data: T) => T, + postLoad?: (saved: T) => void + ): Writable => { + let saved: T = def; + if (postLoad) postLoad(saved); + + const { subscribe, set, update } = writable(saved); + return { + subscribe, + set: (value: T) => { + if (setAction) value = setAction(value); + return set(value); + }, + update + }; + }; + + private deserializeSkillFolders = (data: string | FolderProperties[]): FabledFolder[] => { + const serialized = typeof data === 'string' ? data : JSON.stringify(data); + if (!serialized || serialized === 'null') return []; + + try { + return JSON.parse(serialized, (key: string, value) => { + if (value == null) return; + if (/\d+/.test(key)) { + if (typeof value === 'string') { + return this.getSkill(value); + } + + const folder = new FabledFolder(value.data); + folder.name = value.name; + folder.location = value.location || 'local'; + folder.open = !!value.open; + return folder; + } + return value; + }); + } catch (e) { + console.error('Error loading skill folders. Folder data: ' + serialized, e); + notify('Error loading skill folders. ' + JSON.stringify(e) + '\nFolder data: ' + serialized); + return []; + } + }; + + hydratePersistedData = async () => { + const skills = listPersistedSkillNames().map( + (name) => + new FabledSkill({ + name, + location: 'local' + }) + ); + + this.skills.set(sort(skills)); + this.skillFolders.set( + sort(this.deserializeSkillFolders(getPersistedFolders('skill'))) + ); + }; + + skills: Writable = this.setupSkillStore( + 'skills', + [], + (_data: string) => [], + (value: FabledSkill[]) => { + this.persistSkills(); + return sort(value); + } + ); + + getSkill = (name: string): FabledSkill | undefined => { + for (const c of get(this.skills)) { + if (c.name == name) return c; + } + + return undefined; + }; + + skillFolders: Writable = this.setupSkillStore( + 'skill-folders', + [], + (_data: string) => [], + (value: FabledFolder[]) => { + void savePersistedFolders( + 'skill', + value.filter((folder) => folder.location === 'local').map((folder) => folder.toJSON()) + ).then((result) => { + if (result.ok) { + if (get(saveError)?.name === 'Skills') { + saveError.set(undefined); + } + return; + } + console.error('Skill folder save error', result.error); + saveError.set({ + name: 'Skills', + message: getPersistenceFailureMessage(result) + }); + }); + return sort(value); + } + ); + + isSkillNameTaken = (name: string): boolean => !!this.getSkill(name); + + addSkill = (name?: string): FabledSkill => { + const allSkills = get(this.skills); + let index = allSkills.length + 1; + while (!name && this.isSkillNameTaken(name || 'Skill ' + index)) { + index++; + } + const skill = new FabledSkill({ name: name || 'Skill ' + index }); + allSkills.push(skill); + + this.skills.set(allSkills); + skill.save(); + return skill; + }; + + loadSkill = async (data: FabledSkill) => { + if (data.loaded) return; + + if (data.location === 'local') { + const yamlData = await getPersistedSkill(data.name); + if (!yamlData) return; + await data.load(yamlData); + } else { + const yaml = await socketService.getSkillYaml(data.name); + if (!yaml) return; + const yamlData = parseYaml(yaml); + const skill = Object.values(yamlData)[0]; + await data.load(skill); + } + + data.postLoad(); + }; + + cloneSkill = async (data: FabledSkill): Promise => { + if (!data.loaded) await this.loadSkill(data); + + const sk: FabledSkill[] = get(this.skills); + let name = data.name + ' (Copy)'; + let i = 1; + while (this.isSkillNameTaken(name)) { + name = data.name + ' (Copy ' + i + ')'; + i++; + } + const skill = new FabledSkill(); + const yamlData = data.serializeYaml(); + await skill.load(yamlData); + skill.name = name; + sk.push(skill); + + this.skills.set(sk); + skill.save(); + return skill; + }; + + addSkillFolder = (folder: FabledFolder) => { + const folders = get(this.skillFolders); + if (folders.includes(folder)) return; + + folderStore.rename(folder, folders); + + folders.push(folder); + folders.sort((a, b) => a.name.localeCompare(b.name)); + this.skillFolders.set(folders); + }; + + deleteSkillFolder = ( + folder: FabledFolder, + deleteCheck?: (subfolder: FabledFolder) => boolean + ) => { + const folders = get(this.skillFolders).filter((f) => f != folder); + + // If there are any subfolders or skills, move them to the parent or root + folder.data.forEach((d) => { + if (d instanceof FabledFolder) { + if (deleteCheck && deleteCheck(d)) { + this.deleteSkillFolder(d, deleteCheck); + return; + } + if (folder.parent) folder.parent.add(d); + else { + d.updateParent(); + folders.push(d); + } + } else if (folder.parent) folder.parent.add(d); // Add the skill to the parent folder + }); + + this.skillFolders.set(folders); + }; + + deleteSkill = (data: FabledSkill) => { + const filtered = get(this.skills).filter((c) => c != data); + const act = get(active); + this.skills.set(filtered); + void deletePersistedSkill(data.name); + + if (!(act instanceof FabledSkill)) return; + + if (filtered.length === 0) goto(`${base}/`).then(() => {}); + else if (!filtered.find((sk) => sk === get(active))) + goto(`${base}/skill/${filtered[0].name}`).then(() => {}); + }; + + refreshSkills = () => this.skills.set(sort(get(this.skills))); + refreshSkillFolders = () => { + this.skillFolders.set(sort(get(this.skillFolders))); + this.refreshSkills(); + }; + + /** + * Loads skill data from a string + */ + loadSkillText = async (text: string, fromServer: boolean = false) => { + // Load new skills + const data = parseYaml(text); + + if (!data || Object.keys(data).length === 0) { + // If there is no data or the object is empty... return + return; + } + + const keys = Object.keys(data); + + let skill: FabledSkill; + // If we only have one skill, and it is the current YAML, + // the structure is a bit different + if (keys.length == 1) { + const key: string = keys[0]; + skill = (this.isSkillNameTaken(key) ? this.getSkill(key) : this.addSkill(key)); + if (fromServer) skill.location = 'server'; + await skill.load(data[key]); + skill.save(); + this.refreshSkills(); + return; + } + + for (const key of Object.keys(data)) { + if (key != 'loaded' && !this.isSkillNameTaken(key)) { + skill = (this.isSkillNameTaken(key) ? this.getSkill(key) : this.addSkill(key)); + await skill.load(data[key]); + skill.save(); + } + } + this.refreshSkills(); + }; + + loadSkills = async (e: ProgressEvent) => { + const text: string = e.target?.result; + if (!text) return; + + await this.loadSkillText(text); + }; + + isSaving: Writable = writable(false); + saveTask: number = 0; + + persistSkills = (_list?: FabledSkill[]) => {}; } -export const skillStore = new SkillStore(); \ No newline at end of file +export const skillStore = new SkillStore(); diff --git a/src/data/store.ts b/src/data/store.ts index 4822c13f17..129bf9156c 100644 --- a/src/data/store.ts +++ b/src/data/store.ts @@ -1,298 +1,329 @@ -import type { Readable, Writable } from 'svelte/store'; -import { derived, get, writable } from 'svelte/store'; -import { localStore } from '$api/api'; -import { attributeStore } from './attribute-store'; -import type FabledComponent - from '$api/components/fabled-component.svelte'; +import type { Readable, Writable } from 'svelte/store'; +import { derived, get, writable } from 'svelte/store'; +import { localStore } from '$api/api'; +import { attributeStore } from './attribute-store'; +import type FabledComponent from '$api/components/fabled-component.svelte'; import type { MultiAttributeYamlData, MultiClassYamlData, MultiSkillYamlData } from '$api/types'; -import { socketService } from '$api/socket/socket-connector'; -import YAML from 'yaml'; -import FabledAttribute from '$api/fabled-attribute.svelte'; -import { Tab } from '$api/tab'; -import FabledSkill, { skillStore } from './skill-store.svelte'; -import FabledClass, { classStore } from './class-store.svelte'; -import { FabledFolder, folderStore } from './folder-store.svelte'; - -export const active: Writable = writable(undefined); -export const activeType: Readable<'class' | 'skill' | 'attribute' | ''> = derived( - active, - $active => { - if ($active?.dataType === 'class') { - return 'class'; - } else if ($active?.dataType === 'skill') { - return 'skill'; - } else if ($active?.dataType === 'attribute') { - return 'attribute'; - } else { - return ''; - } - } +import { socketService } from '$api/socket/socket-connector'; +import YAML from 'yaml'; +import FabledAttribute from '$api/fabled-attribute.svelte'; +import { Tab } from '$api/tab'; +import FabledSkill, { skillStore } from './skill-store.svelte'; +import FabledClass, { classStore } from './class-store.svelte'; +import { FabledFolder, folderStore } from './folder-store.svelte'; + +export const active: Writable = + writable(undefined); +export const activeType: Readable<'class' | 'skill' | 'attribute' | ''> = derived( + active, + ($active) => { + if ($active?.dataType === 'class') { + return 'class'; + } else if ($active?.dataType === 'skill') { + return 'skill'; + } else if ($active?.dataType === 'attribute') { + return 'attribute'; + } else { + return ''; + } + } ); -export const dragging: Writable = writable(); -export const draggingComponent: Writable = writable(); -export const showSidebar: Writable = localStore('sidebarOpen', true); -export const sidebarOpen: Writable = writable(true); -export const shownTab: Writable = writable(Tab.CLASSES); -export const importing: Writable = writable(false); -export const localSyncList: Writable> = writable(new Map()); +export const dragging: Writable = + writable(); +export const draggingComponent: Writable = writable(); +export const showSidebar: Writable = localStore('sidebarOpen', true); +export const sidebarOpen: Writable = writable(true); +export const shownTab: Writable = writable(Tab.CLASSES); +export const importing: Writable = writable(false); +export const localSyncList: Writable< + Map +> = writable(new Map()); export const toggleSidebar = (e: Event) => { - e.stopPropagation(); - showSidebar.set(!get(showSidebar)); + e.stopPropagation(); + showSidebar.set(!get(showSidebar)); }; -export const closeSidebar = () => showSidebar.set(false); -export const setImporting = (bool: boolean) => importing.set(bool); +export const closeSidebar = () => showSidebar.set(false); +export const setImporting = (bool: boolean) => importing.set(bool); export const deleteProData = (data: FabledClass | FabledSkill | FabledAttribute | undefined) => { - if (!data) return; + if (!data) return; - folderStore.getFolder(data)?.remove(data); - if (data instanceof FabledClass) classStore.deleteClass(data); - else if (data instanceof FabledSkill) skillStore.deleteSkill(data); - else if (data instanceof FabledAttribute) attributeStore.deleteAttribute(data); - folderStore.updateFolders(); + folderStore.getFolder(data)?.remove(data); + if (data instanceof FabledClass) classStore.deleteClass(data); + else if (data instanceof FabledSkill) skillStore.deleteSkill(data); + else if (data instanceof FabledAttribute) attributeStore.deleteAttribute(data); + folderStore.updateFolders(); }; -const skillFileRegex = /['"]?(components|combo)['"]?:/; +const skillFileRegex = /['"]?(components|combo)['"]?:/; /** * Loads an individual skill or class file * @param e ProgressEvent */ export const loadIndividual = async (e: ProgressEvent) => { - const text: string = e.target?.result; - if (!text) return; - - if (skillFileRegex.test(text)) { - await skillStore.loadSkillText(text); - } else { - classStore.loadClassText(text); - } - (document.activeElement).blur(); + const text: string = e.target?.result; + if (!text) return; + + if (skillFileRegex.test(text)) { + await skillStore.loadSkillText(text); + } else { + classStore.loadClassText(text); + } + (document.activeElement).blur(); }; export const loadRaw = async (text: string, fromServer: boolean = false) => { - if (!text) return; - - if (text.indexOf('global:') >= 0) { - attributeStore.loadAttributesText(text); - } else if (skillFileRegex.test(text)) { - await skillStore.loadSkillText(text.replace('loaded: false\n', ''), fromServer); - } else { - classStore.loadClassText(text.replace('loaded: false\n', ''), fromServer); - } + if (!text) return; + + if (text.indexOf('global:') >= 0) { + attributeStore.loadAttributesText(text); + } else if (skillFileRegex.test(text)) { + await skillStore.loadSkillText(text.replace('loaded: false\n', ''), fromServer); + } else { + classStore.loadClassText(text.replace('loaded: false\n', ''), fromServer); + } }; export const loadFile = (file: File) => { - const reader = new FileReader(); - if (file.name.indexOf('skills') == 0) { - reader.onload = skillStore.loadSkills; - } else if (file.name.indexOf('classes') == 0) { - reader.onload = classStore.loadClasses; - } else if (file.name.indexOf('attributes') == 0) { - reader.onload = attributeStore.loadAttributes; - } else { - reader.onload = loadIndividual; - } - reader.readAsText(file); + const reader = new FileReader(); + if (file.name.indexOf('skills') == 0) { + reader.onload = skillStore.loadSkills; + } else if (file.name.indexOf('classes') == 0) { + reader.onload = classStore.loadClasses; + } else if (file.name.indexOf('attributes') == 0) { + reader.onload = attributeStore.loadAttributes; + } else { + reader.onload = loadIndividual; + } + reader.readAsText(file); }; export const saveData = (data?: FabledSkill | FabledClass | FabledAttribute, e?: Event) => { - e?.preventDefault(); - e?.stopPropagation(); - - const act = data || get(active); - if (!act) return; - if (act instanceof FabledAttribute) { - // If it's an attribute, we should export the whole attributes.yml - saveAttributes().then(() => { - }); - return; - } - - saveToFile(act.name + '.yml', YAML.stringify({ [act.name]: act.serializeYaml() }, { - lineWidth: 0, - aliasDuplicateObjects: false - })); + e?.preventDefault(); + e?.stopPropagation(); + + const act = data || get(active); + if (!act) return; + if (act instanceof FabledAttribute) { + // If it's an attribute, we should export the whole attributes.yml + saveAttributes().then(() => {}); + return; + } + + saveToFile( + act.name + '.yml', + YAML.stringify( + { [act.name]: act.serializeYaml() }, + { + lineWidth: 0, + aliasDuplicateObjects: false + } + ) + ); }; export const getAttributeYaml = async () => { - let text = ''; - for (const line of (await fetch('https://raw.githubusercontent.com/magemonkeystudio/fabled/dev/src/main/resources/attributes.yml').then(r => r.text())).split('\n')) { - if (line.startsWith('#') || line.length === 0) { - text = text + line + '\n'; - } else { - break; - } - } - - const attributeYaml: MultiAttributeYamlData = {}; - for (const attr of get(attributeStore.attributes)) { - attributeYaml[attr.name] = attr.serializeYaml(); - } - const yaml = YAML.stringify(attributeYaml, { lineWidth: 0, aliasDuplicateObjects: false }); - - text += yaml; - return text; + let text = ''; + for (const line of ( + await fetch( + 'https://raw.githubusercontent.com/magemonkeystudio/fabled/dev/src/main/resources/attributes.yml' + ).then((r) => r.text()) + ).split('\n')) { + if (line.startsWith('#') || line.length === 0) { + text = text + line + '\n'; + } else { + break; + } + } + + const attributeYaml: MultiAttributeYamlData = {}; + for (const attr of get(attributeStore.attributes)) { + attributeYaml[attr.name] = attr.serializeYaml(); + } + const yaml = YAML.stringify(attributeYaml, { lineWidth: 0, aliasDuplicateObjects: false }); + + text += yaml; + return text; }; export const saveAttributes = async () => { - saveToFile('attributes.yml', await getAttributeYaml()); + saveToFile('attributes.yml', await getAttributeYaml()); }; export const saveDataToServer = async (data?: FabledSkill | FabledClass | FabledAttribute) => { - const act = data || get(active); - if (!act) return false; - - if (act instanceof FabledAttribute) { - return await socketService.saveAttributesToServer(await getAttributeYaml()); - } - - const yaml = YAML.stringify({ [act.name]: act.serializeYaml() }, { lineWidth: 0, aliasDuplicateObjects: false }); - - const folder = folderStore.getFolder(act); - let path = ''; - if (folder) { - path = folder.name + '/'; - } - - if (act instanceof FabledSkill) { - return await socketService.saveSkillToServer(path + act.name, yaml); - } else if (act instanceof FabledClass) { - return await socketService.saveClassToServer(path + act.name, yaml); - } + const act = data || get(active); + if (!act) return false; + + if (act instanceof FabledAttribute) { + return await socketService.saveAttributesToServer(await getAttributeYaml()); + } + + const yaml = YAML.stringify( + { [act.name]: act.serializeYaml() }, + { lineWidth: 0, aliasDuplicateObjects: false } + ); + + const folder = folderStore.getFolder(act); + let path = ''; + if (folder) { + path = folder.name + '/'; + } + + if (act instanceof FabledSkill) { + return await socketService.saveSkillToServer(path + act.name, yaml); + } else if (act instanceof FabledClass) { + return await socketService.saveClassToServer(path + act.name, yaml); + } }; export const getAllSkillYaml = async (): Promise => { - const allSkills: FabledSkill[] = get(skillStore.skills); - allSkills.sort((a, b) => { - if (a.name > b.name) return 1; - if (a.name < b.name) return -1; - return 0; - }); - - const skillYaml: MultiSkillYamlData = {}; - skillYaml.loaded = false; - - const loadedPromise = allSkills.map(async skill => { - if (!skill.loaded) await skillStore.loadSkill(skill); - skillYaml[skill.name] = skill.serializeYaml(); - }); - await Promise.all(loadedPromise); - - return skillYaml; + const allSkills: FabledSkill[] = get(skillStore.skills); + allSkills.sort((a, b) => { + if (a.name > b.name) return 1; + if (a.name < b.name) return -1; + return 0; + }); + + const skillYaml: MultiSkillYamlData = {}; + skillYaml.loaded = false; + + const loadedPromise = allSkills.map(async (skill) => { + if (!skill.loaded) await skillStore.loadSkill(skill); + skillYaml[skill.name] = skill.serializeYaml(); + }); + await Promise.all(loadedPromise); + + return skillYaml; }; export const getAllClassYaml = async (): Promise => { - const allClasses: FabledClass[] = get(classStore.classes); - allClasses.sort((a, b) => { - if (a.name > b.name) return 1; - if (a.name < b.name) return -1; - return 0; - }); - - const classYaml: MultiClassYamlData = {}; - classYaml.loaded = false; - for (const cls of allClasses) { - if (!cls.loaded) await classStore.loadClass(cls); - classYaml[cls.name] = cls.serializeYaml(); - } - - return classYaml; + const allClasses: FabledClass[] = get(classStore.classes); + allClasses.sort((a, b) => { + if (a.name > b.name) return 1; + if (a.name < b.name) return -1; + return 0; + }); + + const classYaml: MultiClassYamlData = {}; + classYaml.loaded = false; + for (const cls of allClasses) { + if (!cls.loaded) await classStore.loadClass(cls); + classYaml[cls.name] = cls.serializeYaml(); + } + + return classYaml; }; export const saveAllToServer = async () => { - const skillYaml = await getAllSkillYaml(); - const classYaml = await getAllClassYaml(); - const attributeYaml = await getAttributeYaml(); - - return await socketService.exportAll( - YAML.stringify(classYaml, { lineWidth: 0, aliasDuplicateObjects: false }), - YAML.stringify(skillYaml, { lineWidth: 0, aliasDuplicateObjects: false }), - attributeYaml.toString()); + const skillYaml = await getAllSkillYaml(); + const classYaml = await getAllClassYaml(); + const attributeYaml = await getAttributeYaml(); + + return await socketService.exportAll( + YAML.stringify(classYaml, { lineWidth: 0, aliasDuplicateObjects: false }), + YAML.stringify(skillYaml, { lineWidth: 0, aliasDuplicateObjects: false }), + attributeYaml.toString() + ); }; export const saveAll = async () => { - skillStore.isSaving.set(true); - const skillYaml = await getAllSkillYaml(); - const classYaml = await getAllClassYaml(); - - saveToFile('skills.yml', YAML.stringify(skillYaml, { lineWidth: 0, aliasDuplicateObjects: false })); - saveToFile('classes.yml', YAML.stringify(classYaml, { lineWidth: 0, aliasDuplicateObjects: false })); - await saveAttributes(); - skillStore.isSaving.set(false); + skillStore.isSaving.set(true); + const skillYaml = await getAllSkillYaml(); + const classYaml = await getAllClassYaml(); + + saveToFile( + 'skills.yml', + YAML.stringify(skillYaml, { lineWidth: 0, aliasDuplicateObjects: false }) + ); + saveToFile( + 'classes.yml', + YAML.stringify(classYaml, { lineWidth: 0, aliasDuplicateObjects: false }) + ); + await saveAttributes(); + skillStore.isSaving.set(false); }; /** * Saves text data to a file locally */ const saveToFile = (file: string, data: string) => { - const textFileAsBlob = new Blob([data], { type: 'text/plain;charset=utf-8' }); + const textFileAsBlob = new Blob([data], { type: 'text/plain;charset=utf-8' }); - const element = document.createElement('a'); - element.href = URL.createObjectURL(textFileAsBlob); - element.download = file; - element.style.display = 'none'; + const element = document.createElement('a'); + element.href = URL.createObjectURL(textFileAsBlob); + element.download = file; + element.style.display = 'none'; - document.body.appendChild(element); - element.click(); - document.body.removeChild(element); + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); }; export const isSyncLocal = (data: FabledSkill | FabledClass | FabledAttribute) => { - return findFileHandle(data) !== undefined; + return findFileHandle(data) !== undefined; }; export const toggleSyncLocal = async (data: FabledSkill | FabledClass | FabledAttribute) => { - if (isSyncLocal(data)) { - removeSyncLocal(data); - } else { - await addSyncLocal(data); - } + if (isSyncLocal(data)) { + removeSyncLocal(data); + } else { + await addSyncLocal(data); + } }; export const triggerAutoSync = async (data: FabledSkill | FabledClass | FabledAttribute) => { - console.log('triggerAutoSync'); - const fileHandle = findFileHandle(data); - if (fileHandle) { - const writable = await fileHandle.createWritable(); - await writable.write(YAML.stringify({ [data.name]: data.serializeYaml() }, { lineWidth: 0, aliasDuplicateObjects: false })); - await writable.close(); - - console.log(fileHandle.name); - } + console.log('triggerAutoSync'); + const fileHandle = findFileHandle(data); + if (fileHandle) { + const writable = await fileHandle.createWritable(); + await writable.write( + YAML.stringify( + { [data.name]: data.serializeYaml() }, + { lineWidth: 0, aliasDuplicateObjects: false } + ) + ); + await writable.close(); + + console.log(fileHandle.name); + } }; const addSyncLocal = async (data: FabledSkill | FabledClass | FabledAttribute) => { - if (!('showOpenFilePicker' in window)) return; - - // @ts-ignore - const file = await window.showOpenFilePicker({ - types: [{ description: 'Choose a yaml file to sync', accept: { 'text/yaml': ['.yml', '.yaml'] } }], - multiple: false, - excludeAcceptAllOption: true - }); - const fileHandle = await file[0] as FileSystemFileHandle; - const writable = await fileHandle.createWritable(); - if (!writable || writable.locked) return; - localSyncList.update(list => { - list.set(fileHandle, data); - triggerAutoSync(data) - return list; - }); + if (!('showOpenFilePicker' in window)) return; + + // @ts-ignore + const file = await window.showOpenFilePicker({ + types: [ + { description: 'Choose a yaml file to sync', accept: { 'text/yaml': ['.yml', '.yaml'] } } + ], + multiple: false, + excludeAcceptAllOption: true + }); + const fileHandle = (await file[0]) as FileSystemFileHandle; + const writable = await fileHandle.createWritable(); + if (!writable || writable.locked) return; + localSyncList.update((list) => { + list.set(fileHandle, data); + triggerAutoSync(data); + return list; + }); }; const removeSyncLocal = (data: FabledSkill | FabledClass | FabledAttribute) => { - localSyncList.update(list => { - const fileHandle = findFileHandle(data); - if (fileHandle) { - list.delete(fileHandle); - } - return list; - }); + localSyncList.update((list) => { + const fileHandle = findFileHandle(data); + if (fileHandle) { + list.delete(fileHandle); + } + return list; + }); }; const findFileHandle = (data: FabledSkill | FabledClass | FabledAttribute) => { - return get(localSyncList).entries().find(([_, value]) => value === data)?.[0]; + return get(localSyncList) + .entries() + .find(([_, value]) => value === data)?.[0]; }; -export const saveError: Writable<{ name: string, acknowledged: boolean } | undefined> = writable(); \ No newline at end of file +export const saveError: Writable<{ name: string; message: string } | undefined> = writable(); diff --git a/src/routes/(app)/[type=istype]/[id]/+page.ts b/src/routes/(app)/[type=istype]/[id]/+page.ts index 9515f4d685..9db617d9be 100644 --- a/src/routes/(app)/[type=istype]/[id]/+page.ts +++ b/src/routes/(app)/[type=istype]/[id]/+page.ts @@ -1,19 +1,18 @@ -import { active, shownTab } from '../../../../data/store'; -import { get } from 'svelte/store'; -import { redirect } from '@sveltejs/kit'; -import type { MultiSkillYamlData } from '$api/types'; -import { socketService } from '$api/socket/socket-connector'; -import { base } from '$app/paths'; -import { parseYaml } from '$api/yaml'; -import { Tab } from '$api/tab'; +import { active, shownTab } from '../../../../data/store'; +import { get } from 'svelte/store'; +import { redirect } from '@sveltejs/kit'; +import { base } from '$app/paths'; +import { Tab } from '$api/tab'; import FabledSkill, { skillStore } from '../../../../data/skill-store.svelte'; +import { hydrateEditorData } from '../../../../data/editor-session'; export const ssr = false; // noinspection JSUnusedGlobalSymbols /** @type {import('../../../../../.svelte-kit/types/src/routes').PageLoad} */ export async function load({ params }) { - const name = params.id; + await hydrateEditorData(); + const name = params.id; const isSkill = params.type === 'skill'; let data: FabledSkill | undefined; let fallback: FabledSkill | undefined; @@ -29,19 +28,7 @@ export async function load({ params }) { if (data) { if (!data.loaded) { - let yamlData: MultiSkillYamlData; - if (data.location === 'local') { - yamlData = parseYaml(localStorage.getItem(`sapi.skill.${data.name}`) || ''); - } else { - const yaml: string = await socketService.getSkillYaml(data.name); - - yamlData = parseYaml(yaml); - } - - if (yamlData && Object.keys(yamlData).length > 0) { - await (data).load(Object.values(yamlData)[0]); - } - (data).postLoad(); + await skillStore.loadSkill(data); } active.set(data); @@ -50,4 +37,4 @@ export async function load({ params }) { } } redirect(302, `${base}/${params.type}/${params.id}/edit`); -} \ No newline at end of file +} diff --git a/src/routes/(app)/[type=istype]/[id]/edit/+page.ts b/src/routes/(app)/[type=istype]/[id]/edit/+page.ts index 6dbb2b7579..680b67c433 100644 --- a/src/routes/(app)/[type=istype]/[id]/edit/+page.ts +++ b/src/routes/(app)/[type=istype]/[id]/edit/+page.ts @@ -9,12 +9,14 @@ import { parseYaml } from '$api/yaml'; import FabledSkill, { skillStore } from '../../../../../data/skill-store.svelte'; import FabledClass, { classStore } from '../../../../../data/class-store.svelte'; import { attributeStore } from '../../../../../data/attribute-store'; +import { hydrateEditorData } from '../../../../../data/editor-session'; export const ssr = false; // noinspection JSUnusedGlobalSymbols /** @type {import('../../../../../../.svelte-kit/types/src/routes').PageLoad} */ export async function load({ params }) { + await hydrateEditorData(); const name = params.id; const isSkill = params.type === 'skill'; @@ -55,23 +57,15 @@ export async function load({ params }) { if (data) { let classOrSkill = false; if (!data.loaded) { - let yamlData: MultiSkillYamlData | MultiClassYamlData | MultiAttributeYamlData; if (data.location === 'local') { if (data instanceof FabledAttribute) { - const text = localStorage.getItem('attribs') || ''; - if (text.split('\n').length > 2 || text.charAt(0) == '{') { - // New format - yamlData = parseYaml(text); - } else { - yamlData = {}; - } - } else { + await attributeStore.loadAttribute(data); + } else if (data instanceof FabledSkill) { + classOrSkill = true; + await skillStore.loadSkill(data); + } else if (data instanceof FabledClass) { classOrSkill = true; - yamlData = ( - parseYaml( - localStorage.getItem(`sapi.${isSkill ? 'skill' : 'class'}.${data.name}`) || '' - ) - ); + await classStore.loadClass(data); } } else { let yaml: string; @@ -79,15 +73,15 @@ export async function load({ params }) { else if (params.type === 'skill') yaml = await socketService.getSkillYaml(data.name); else yaml = await socketService.getAttributeYaml(); - yamlData = parseYaml(yaml); - } - - - if (yamlData && Object.keys(yamlData).length > 0) { - if (data instanceof FabledAttribute) { - data.load((yamlData)[data.name]); - } else { - data.load(Object.values(yamlData)[0]); + const yamlData = ( + parseYaml(yaml) + ); + if (yamlData && Object.keys(yamlData).length > 0) { + if (data instanceof FabledAttribute) { + data.load((yamlData)[data.name]); + } else { + data.load(Object.values(yamlData)[0]); + } } } diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 4c27831ec2..49b6043abb 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,619 +1,645 @@ {#if !page.url.pathname.endsWith('/migration') && !page.url.host.includes('fabled.travja.dev')} -
- We're moving! Expect changes! New URL: fabled.travja.dev. - Learn more about the migration -
+
+ We're moving! Expect changes! New URL: fabled.travja.dev. + Learn more about the migration +
{/if}
- {#if $showSidebar} - - {/if} -
- {@render children?.()} -
+ {#if $showSidebar} + + {/if} +
+ {@render children?.()} +
-
e.key === 'Enter' && saveAll()} - role="button" - style:--distance="{$distance}rem" - style:--rotation="{$rotation}deg" - tabindex="0" - title="Backup All Data" - > - cloud_download -
-
saveData()} - onkeypress={(e) => { - if (e.key === 'Enter') saveData(); - }} - role="button" - style:--distance="{$distance}rem" - style:--rotation="{$rotation * 3}deg" - tabindex="0" - title="Save" - > - save -
- {#if $socketConnected} - -
saveServerInfo()} - onkeypress={(e) => { - if (e.key === 'Enter') saveServerInfo(); - }} - > - {#if button === 'save' && serverSaveStatus !== 'NONE'} - {statusMap[serverSaveStatus]} - {:else} - upload_file - {/if} -
-
exportAllToServer()} - onkeypress={(e) => { - if (e.key === 'Enter') exportAllToServer(); - }} - > - {#if button === 'export' && serverSaveStatus !== 'NONE'} - {statusMap[serverSaveStatus]} - {:else} - cloud_upload - {/if} -
-
reload()} - onkeypress={(e) => { - if (e.key === 'Enter') reload(); - }} - > - {#if button === 'reload' && serverSaveStatus !== 'NONE'} - {statusMap[serverSaveStatus]} - {:else} - sync - {/if} -
- {/if} -
openModal(SettingsModal)} - onkeypress={(e) => e.key === 'Enter' && openModal(SettingsModal)} - role="button" - style:--distance="1rem" - style:--rotation="60deg" - tabindex="0" - title="Change Settings" - > - settings -
+
e.key === 'Enter' && saveAll()} + role="button" + style:--distance="{$distance}rem" + style:--rotation="{$rotation}deg" + tabindex="0" + title="Backup All Data" + > + cloud_download +
+
saveData()} + onkeypress={(e) => { + if (e.key === 'Enter') saveData(); + }} + role="button" + style:--distance="{$distance}rem" + style:--rotation="{$rotation * 3}deg" + tabindex="0" + title="Save" + > + save +
+ {#if $socketConnected} + +
saveServerInfo()} + onkeypress={(e) => { + if (e.key === 'Enter') saveServerInfo(); + }} + > + {#if button === 'save' && serverSaveStatus !== 'NONE'} + {statusMap[serverSaveStatus]} + {:else} + upload_file + {/if} +
+
exportAllToServer()} + onkeypress={(e) => { + if (e.key === 'Enter') exportAllToServer(); + }} + > + {#if button === 'export' && serverSaveStatus !== 'NONE'} + {statusMap[serverSaveStatus]} + {:else} + cloud_upload + {/if} +
+
reload()} + onkeypress={(e) => { + if (e.key === 'Enter') reload(); + }} + > + {#if button === 'reload' && serverSaveStatus !== 'NONE'} + {statusMap[serverSaveStatus]} + {:else} + sync + {/if} +
+ {/if} +
openModal(SettingsModal)} + onkeypress={(e) => e.key === 'Enter' && openModal(SettingsModal)} + role="button" + style:--distance="1rem" + style:--rotation="60deg" + tabindex="0" + title="Change Settings" + > + settings +
{#if $importing} - + {/if} {#if $saveError} -
- Failed to save {$saveError.name} - Data is too large. -
- We can keep it in memory for you to use, but will be unable to persist it to your browser's - storage. -
-
Closing/Refreshing the page will cause you to lose this data.
-
You'll need to export it and re-import later if you want to keep working with this.
-
{ - acknowledgeSaveError(); - }} - onkeypress={(e) => { - if (e.key === 'Enter') { - acknowledgeSaveError(); - } - }} - > - I Understand -
-
+
+ Failed to save {$saveError.name}. +
{$saveError.message}
+
Export before refreshing or closing if you need to keep working with this data.
+
{ + dismissSaveError(); + }} + onkeypress={(e) => { + if (e.key === 'Enter') { + dismissSaveError(); + } + }} + > + Dismiss +
+
+{/if} + +{#if $editorPersistenceUnsupported} + {/if} {#if ModalService.activeModal} - + {/if} {#if displaySave} -
{$isSaving ? 'Saving...' : 'Saved!'}
+
{$isSaving ? 'Saving...' : 'Saved!'}
{/if} {#if dragging} -
Drop to Import
+
Drop to Import
{/if} {#if !!$passphrase && !$socketTrusted} - -

Untrusted Connection to Server

-
- Server is not trusted. Please run -
{ - if (e.key === 'Enter') copyText(); - }} - role="button" - tabindex="0" - > - /synth trust {$passphrase} -
- from the server -
-
+ +

Untrusted Connection to Server

+
+ Server is not trusted. Please run +
{ + if (e.key === 'Enter') copyText(); + }} + role="button" + tabindex="0" + > + /synth trust {$passphrase} +
+ from the server +
+
{/if} {#if $dcWarning > 0} -
- You will lose connection in {$dcTime} seconds -
socketService.ping()} - onkeypress={(e) => { - if (e.key === 'Enter') socketService.ping(); - }} - > - Click to remain connected -
-
+
+ You will lose connection in {$dcTime} seconds +
socketService.ping()} + onkeypress={(e) => { + if (e.key === 'Enter') socketService.ping(); + }} + > + Click to remain connected +
+
{/if} diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index e1c88734c1..5dcd3da5f4 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -1,20 +1,15 @@ -import type { LayoutLoad } from './$types'; -import { getHaste } from '$api/hastebin'; -import { base } from '$app/paths'; -import { socketService } from '$api/socket/socket-connector'; -import { initComponents } from '$api/components/components.svelte'; -import YAML from 'yaml'; -import { parseYaml } from '$api/yaml'; -import type { MultiSkillYamlData } from '$api/types'; -import { synthesisEnabled } from '../data/settings'; +import type { LayoutLoad } from './$types'; +import { socketService } from '$api/socket/socket-connector'; +import { initComponents } from '$api/components/components.svelte'; +import { synthesisEnabled } from '../data/settings'; +import { hydrateEditorData } from '../data/editor-session'; export const ssr = false; -const expectedHost = ['fabled.travja.dev', 'synthesis.travja.dev']; -const separator = '\n\n\n~~~~~\n\n\n'; - export const load: LayoutLoad = async ({ url }) => { initComponents(); + await hydrateEditorData(); + if (synthesisEnabled && url.searchParams.has('session')) { // Attempt to connect to the socket.io server const sessionId = url.searchParams.get('session'); @@ -22,60 +17,4 @@ export const load: LayoutLoad = async ({ url }) => { socketService.connect(sessionId); } } - - if (url.host.includes('localhost')) return; - - if (url.searchParams.has('migrationData')) { - // Load the skills into the editor. - // This should be from migrations. - - getHaste({ url: url.searchParams.get('migrationData') || undefined }) - .then(data => { - const skillData = data.split(separator)[0]; - const classData = data.split(separator)[1]; - const skillFolders = data.split(separator)[2]; - const classFolders = data.split(separator)[3]; - const attributes = data.split(separator)[4]; - - parseYaml(skillData).forEach((skill: MultiSkillYamlData) => { - localStorage.setItem('sapi.skill.' + skill.name, YAML.stringify(skill, { - lineWidth: 0, - aliasDuplicateObjects: false - })); - }); - parseYaml(classData).forEach((cls: MultiSkillYamlData) => { - localStorage.setItem('sapi.class.' + cls.name, YAML.stringify(cls, { - lineWidth: 0, - aliasDuplicateObjects: false - })); - }); - localStorage.setItem('skillFolders', skillFolders); - localStorage.setItem('classFolders', classFolders); - localStorage.setItem('attribs', attributes); - - window.location.href = `https://${expectedHost}${base}`; - }) - .catch(console.error); - - return; - } - - // if (expectedHost.includes(url.host) || get(skills).length == 0) return; - // - // alert('We\'re migrating to a new URL. You\'re now going to be redirected. Your skills/classes should remain in tact.'); - // - // const skillYaml = YAML.stringify(await getAllSkillYaml(), { lineWidth: 0, aliasDuplicateObjects: false }); - // const classYaml = YAML.stringify(getAllClassYaml(), { lineWidth: 0, aliasDuplicateObjects: false }); - // const skillFolders = localStorage.getItem('skillFolders'); - // const classFolders = localStorage.getItem('classFolders'); - // const attributes = localStorage.getItem('attribs'); - // - // const qualifiedData = skillYaml + separator - // + classYaml + separator - // + skillFolders + separator - // + classFolders + separator - // + attributes; - // - // createPaste(qualifiedData) - // .then((url: string) => window.location.href = `https://${expectedHost}?migrationData=${url}`); -}; \ No newline at end of file +}; diff --git a/svelte.config.js b/svelte.config.js index 8d3958c5bf..9f499b91d2 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,15 +1,10 @@ import adapter from '@sveltejs/adapter-static'; -import preprocess from 'svelte-preprocess'; import { resolve } from 'path'; // const dev = process.argv.includes('dev'); /** @type {import('@sveltejs/kit').Config} */ const config = { - // Consult https://github.com/sveltejs/svelte-preprocess - // for more information about preprocessors - preprocess: preprocess(), - kit: { adapter: adapter({ // default options are shown. On some platforms diff --git a/vite.config.ts b/vite.config.ts index 7979f0ea7a..2bfbf96ecc 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,3 +1,4 @@ +/// import { sveltekit } from '@sveltejs/kit/vite'; import { webSocketServer } from './src/api/socket/socket-io-server'; @@ -8,6 +9,9 @@ const config = { supported: { 'top-level-await': true } + }, + test: { + environment: 'jsdom' } };