From 3ae32f4b0be4251e0733b4d5356bd330017d3ae5 Mon Sep 17 00:00:00 2001 From: Abhishiv Saxena Date: Mon, 24 Jun 2024 05:59:54 +0300 Subject: [PATCH] initial commit --- .github/workflows/ci.yml | 38 + .gitignore | 4 + Makefile | 9 + README.md | 400 ++++++ examples/kitchen-sink/index.html | 66 + examples/kitchen-sink/package.json | 21 + examples/kitchen-sink/src/index.tsx | 240 ++++ examples/kitchen-sink/src/main.tsx | 22 + examples/kitchen-sink/src/vite-env.d.ts | 12 + examples/kitchen-sink/tsconfig.json | 18 + minify.js | 29 + package-lock.json | 1662 +++++++++++++++++++++++ package-publish.json | 15 + package.json | 50 + src/core/constants.ts | 4 + src/core/index.ts | 1 + src/core/state.test.ts | 73 + src/core/state.ts | 1 + src/core/state/experimental.ts | 370 +++++ src/dom/api.ts | 281 ++++ src/dom/constants.ts | 10 + src/dom/dom.ts | 127 ++ src/dom/index.ts | 120 ++ src/dom/jsx.ts | 818 +++++++++++ src/dom/resolver.ts | 21 + src/dom/traverser.ts | 176 +++ src/dom/types.ts | 151 ++ src/dom/utils.ts | 143 ++ src/index.ts | 45 + src/object-observer.d.ts | 7 + src/stdlib/Each/index.tsx | 125 ++ src/stdlib/Portal/index.tsx | 29 + src/stdlib/When/index.tsx | 58 + src/stdlib/index.tsx | 3 + src/utils/crawl.ts | 45 + src/utils/index.ts | 4 + src/utils/observer.ts | 40 + src/utils/ts-object-path/index.ts | 82 ++ tsconfig.json | 18 + 39 files changed, 5338 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 examples/kitchen-sink/index.html create mode 100644 examples/kitchen-sink/package.json create mode 100644 examples/kitchen-sink/src/index.tsx create mode 100644 examples/kitchen-sink/src/main.tsx create mode 100644 examples/kitchen-sink/src/vite-env.d.ts create mode 100644 examples/kitchen-sink/tsconfig.json create mode 100644 minify.js create mode 100644 package-lock.json create mode 100644 package-publish.json create mode 100644 package.json create mode 100644 src/core/constants.ts create mode 100644 src/core/index.ts create mode 100644 src/core/state.test.ts create mode 100644 src/core/state.ts create mode 100644 src/core/state/experimental.ts create mode 100644 src/dom/api.ts create mode 100644 src/dom/constants.ts create mode 100644 src/dom/dom.ts create mode 100644 src/dom/index.ts create mode 100644 src/dom/jsx.ts create mode 100644 src/dom/resolver.ts create mode 100644 src/dom/traverser.ts create mode 100644 src/dom/types.ts create mode 100644 src/dom/utils.ts create mode 100644 src/index.ts create mode 100644 src/object-observer.d.ts create mode 100644 src/stdlib/Each/index.tsx create mode 100644 src/stdlib/Portal/index.tsx create mode 100644 src/stdlib/When/index.tsx create mode 100644 src/stdlib/index.tsx create mode 100644 src/utils/crawl.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/observer.ts create mode 100644 src/utils/ts-object-path/index.ts create mode 100644 tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a8aec94 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI +on: + push: + branches: + - master +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout 🛎️ + uses: actions/checkout@v2.3.1 + with: + persist-credentials: false + - uses: actions/setup-node@v2 + with: + node-version: "18.x" + cache: "npm" + + - name: Install + run: env NODE_ENV=development npm install --prefer-offline --no-audit + - name: Test + run: npm run typecheck && npm run test + + - name: Build + run: env NODE_ENV=production npm run build + + - run: jq -s 'add' package.json package-publish.json > package-final.json && rm -rf package.json && mv package-final.json package.json + + - id: Publish + uses: JS-DevTools/npm-publish@v1 + with: + registry: "https://registry.npmjs.org/" + token: ${{ secrets.NPM_TOKEN }} + access: "public" + check-version: true + - if: steps.publish.outputs.type != 'none' + run: | + echo "Version changed: ${{ steps.publish.outputs.old-version }} => ${{ steps.publish.outputs.version }}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b197b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +.vscode +.DS_Store +dist \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..13f3cea --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ + +.SHELL := /usr/bin/bash + +.PHONY: build +build: + npx tsc --resolveJsonModule -p ./tsconfig.json --outDir ./dist --emitDeclarationOnly --declaration + + npx esbuild ./src/index.ts --bundle --format=esm --outfile=dist/index.js --packages=external --target=es2022 --sourcemap --minify + npx esbuild ./src/core/index.ts --bundle --format=esm --outfile=dist/core/index.js --packages=external --target=es2022 --sourcemap --minify \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6675bd2 --- /dev/null +++ b/README.md @@ -0,0 +1,400 @@ +# rocky7 + +Fine grained reactive UI Library. + +[![Version](https://img.shields.io/npm/v/rocky7.svg?color=success&style=flat-square)](https://www.npmjs.com/package/rocky7) +[![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT) +[![Build Status](https://github.com/abhishiv/rocky7/actions/workflows/ci.yml/badge.svg)](https://github.com/abhishiv/rocky7/actions/workflows/ci.yml) +![Badge size](https://deno.bundlejs.com/?q=rocky7&config={%22analysis%22:undefined}&badge=) + +**npm**: `npm i rocky7` + +**cdn**: https://cdn.jsdelivr.net/npm/rocky7/+esm + +--- + +#### Features + +- **Small.** Fully featured at `~7kB` gzip. +- **Truly reactive and fine grained.** Unlike react and other VDOM libraries which use diffing to compute changes, it use fine grained updates to target only the DOM which needs to update. +- **No Magic** Explicit subscriptions obviate the need of [`sample`](https://github.com/luwes/sinuous/blob/8d1aa0cdb8a25e6bfcdf34f022523564a9adb533/src/observable.js#L34-L49)/[`untrack`](https://github.com/vobyjs/voby#untrack) methods found in other fine grained reactive libraries like solid/sinuous. _Importantly, many feel that this also makes your code easy to reason about._ +- **Signals and Stores.** Signals for primitives and Stores for deeply nested objects/arrays. +- **First class HMR** Preserves Signals/Stores across HMR loads for a truly stable HMR experience. +- **DevEx.** no compile step needed if you want: choose your view syntax: `h` for plain javascript or `` for babel/typescript. +- **Rich and Complete.** From support for `SVG` to popular patterns like `dangerouslySetInnerHTML`, `ref` to `` and `` rocky has you covered. + +#### Ecosystem + + + + + + + + + + +
+ rocky7-router + + Router with a familiar react-router like API +
+ rocky7-yjs + + Bidirectional sync between Rocky7 stores and Yjs documents +
+ +#### Sponsors + + + + + + +
You might want to try out grati.co, a no-code programming environment with emacs like extensibility.
+ +#### Example + +[Counter - Codesandbox](https://codesandbox.io/s/counter-demo-rocky7-t7ift3?file=/src/index.tsx) + +```tsx +/** @jsx h **/ + +import { component, h, render } from "rocky7"; + +type Props = { name: string }; +const Page = component("HomePage", (props, { signal, wire }) => { + const $count = signal("count", 0); + const $doubleCount = wire(($) => $count($) * 2); // explicit subscription + return ( +
+

Hey, {props.name}

+ +

Double count = {$doubleCount}

+
+ ); +}); + +render(, document.body); +``` + +## Motivation + +This library is at its core inspired by [haptic](https://github.com/heyheyhello/haptic) that in particular it also favours manual subscription model instead of automatic subscriptions model. This oblivates the need of [`sample`](https://github.com/luwes/sinuous/blob/8d1aa0cdb8a25e6bfcdf34f022523564a9adb533/src/observable.js#L34-L49)/[`untrack`](https://github.com/vobyjs/voby#untrack) found in almost all other reactive libraries. + +Also it borrows the nomenclature of aptly named Signal and Wire from haptic. + +It's also influenced by Sinuous, Solid, & S.js + +## API + +### Core + +
+ signal: create a signal + +```tsx +export const HomePage = component<{ name: string }>( + "HomePage", + (props, { signal, wire }) => { + const $count = signal("count", 0); + //.. rest of component + } +); +``` + +
+
+ wire: create a wire + +```tsx +
+ +
+``` + +
+
+ store: create a store to hold object/arrays + +```tsx +export const Todos = component("Todos", (props, { signal, wire, store }) => { + const $todos = store("todos", { + items: [{ task: "Do Something" }, { task: "Do Something else" }], + }); + return ( +
    + { + return
  • {item.task}
  • ; + }} + >
    +
+ ); +}); +``` + +
+ +
+ defineContext: define context value + +```tsx +export const RouterContext = defineContext("RouterObject"); +``` + +
+ +
+ setContext: set context value + +```tsx +const BrowserRouter = component("Router", (props, { setContext, signal }) => { + setContext( + RouterContext, + signal("router", createRouter(window.history, window.location)) + ); + return props.children; +}); +``` + +
+ +
+ getContext: get context value + +```tsx +const Link = component("Link", (props: any, { signal, wire, getContext }) => { + const router = getContext(RouterContext); + //... rest of component +}); +``` + +
+ +
+ onMount: triggered on mount + +```tsx +export const Prosemirror = component("Prosemirror", (props, { onMount }) => { + onMount(() => { + console.log("component mounted"); + }); + // ... +}); +``` + +
+ +
+ onUnmount: triggered on unmount + +```tsx +export const Prosemirror = component("Prosemirror", (props, { onUnmount }) => { + onUnmount(() => { + console.log("component unmounted"); + }); + // ... +}); +``` + +
+ +### Helper Components + +
+ When: reactive if + +```tsx + $count($) > 5} + views={{ + true: () => { + return
"TRUE"
; + }, + false: () => { + return
"FALSE"
; + }, + }} +>
+``` + +
+ +
+ Each: reactive map + +```tsx + { + return
  • {wire(item.task)}
  • ; + }} +>
    +``` + +
    + +
    + Portal: mount outside of render tree + +```tsx +export const PortalExample = component("PortalExample", (props, utils) => { + const $active = utils.signal("active", false); + return ( +
    + + $active($)} + views={{ + true: () => { + return ( + +
    +

    Portal

    +
    +
    + ); + }, + false: () => { + return ""; + }, + }} + >
    +
    + ); +}); +``` + +
    + +## Reciepes + +
    + HMR + +```tsx +/** @jsx h **/ +import { h, render } from "rocky7"; +import { Layout } from "./index"; + +const renderApp = ({ Layout }: { Layout: typeof Layout }) => + render(, document.getElementById("app")!); + +window.addEventListener("load", () => renderApp({ Layout })); + +if (import.meta.hot) { + import.meta.hot.accept("./index", (newModule) => { + if (newModule) renderApp(newModule as unknown as { Layout: typeof Layout }); + }); +} +``` + +
    + +
    + Refs + +```tsx +/** @jsx h **/ + +export const Prosemirror = component("Prosemirror", (props, { onUnmount }) => { + let container: Element | undefined = undefined; + let prosemirror: EditorView | undefined = undefined; + onUnmount(() => { + if (prosemirror) { + prosemirror.destroy(); + } + }); + return ( +
    { + container = el; + if (container) { + prosemirror = setupProsemirror(container); + } + }} + >
    + ); +}); +``` + +
    + +
    + dangerouslySetInnerHTML + +```tsx +/** @jsx h **/ +
    ` }} /> +``` + +
    + +## Concepts + +### Signals + +These are reactive read/write variables who notify subscribers when they've been written to. They act as dispatchers in the reactive system. + +```tsx +const $count = signal("count", 0); + +$count(); // Passive read (read-pass) +$count(1); // Write +``` + +The subscribers to signals are wires, which will be introduced later. They subscribe by read-subscribing the signal. + +### Stores + +Stores are for storing nested arrays/objects and also act as dispatchers in the reactive system. And like signals, stores can also be read subsribed by wires. Outside of wires, they can be read via `reify` function. Writes can be done via `produce` function immer style. + +```tsx +const val = { name: "Jane", friends: [{ id: "1", name: "John" }] }; +const $profile = store("profile", val); + +// Passive read (read-pass) +const friends = reify($profile.friends); +console.log(friends.length); +// Write +produce($profile.friends, (friends) => { + friends.push({ id: "2", name: "John Doe 2" }); +}); +``` + +### Wires + +These are task runners who subscribe to signals/stores and react to writes. They hold a function (the task) and manage its subscriptions, nested wires, run count, and other metadata. The wire provides a `$` token to the function call that, at your discretion as the developer, can use to read-subscribe to signals. + +```tsx +wire(($) => { + // Explicitly subscribe to count signal using the subtoken "$" + const count = $(count); + + // also possible to subscribe to a stores using "$" subtoken + const friendsCount = $($profile.friends); + return count + friendsCount; +}); +``` diff --git a/examples/kitchen-sink/index.html b/examples/kitchen-sink/index.html new file mode 100644 index 0000000..3da6907 --- /dev/null +++ b/examples/kitchen-sink/index.html @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + +
    + + + diff --git a/examples/kitchen-sink/package.json b/examples/kitchen-sink/package.json new file mode 100644 index 0000000..150a215 --- /dev/null +++ b/examples/kitchen-sink/package.json @@ -0,0 +1,21 @@ +{ + "name": "rocky7-kitchen-sink", + "version": "1.0.0", + "description": "", + "type": "module", + "main": "src/main.tsx", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "author": "", + "license": "ISC", + "dependencies": { + "rocky7": ">=0.1.90" + }, + "devDependencies": { + "typescript": "^5.2.2", + "vite": "^5.1.5" + } +} diff --git a/examples/kitchen-sink/src/index.tsx b/examples/kitchen-sink/src/index.tsx new file mode 100644 index 0000000..925f965 --- /dev/null +++ b/examples/kitchen-sink/src/index.tsx @@ -0,0 +1,240 @@ +/** @jsx h **/ +/** @jsxFrag Fragment */ +import { + component, + h, + render, + When, + Each, + SubToken, + Portal, + Fragment, + reify, + produce, +} from "rocky7"; + +export const PortalExample = component("PortalExample", (props, utils) => { + const $active = utils.signal("active", false); + return ( +
    + + $active($)} + views={{ + true: () => { + return ( + +
    +

    Portal

    +
    +
    + ); + }, + false: () => { + return ""; + }, + }} + >
    +
    + ); +}); + +type Store = { items: { task: string; stats: { count: number } }[] }; +export const Todos = component("Todos", (props, { signal, wire, store }) => { + const $todos = store("todos", { + items: [ + { task: "Do Something", stats: { count: 0 } }, + { task: "Do Something else", stats: { count: 1 } }, + ], + }); + const b = $todos.items; + + return ( +
    +

    Todos

    +
    { + e.preventDefault(); + const formData = new FormData(e.target as HTMLFormElement); + const json = Object.fromEntries(formData) as { + task: string; + stats: any; + }; + json.stats = { count: 0 }; + console.log(json); + produce($todos.items, (items) => { + //items.splice(0, 0, json); + items.push(json); + }); + (e.target as HTMLFormElement).reset(); + }} + > + +
    +
      + { + const v = reify(cursor); + return ( +
    • + + + + {wire(($: SubToken) => $(cursor.task))} +
    • + ); + }} + >
      +
    +
    + ); +}); + +const Test = component("Test", (props, utils) => { + return
    hey
    ; +}); + +export const Layout = component<{}>( + "Layout", + (props, { signal, wire, api }) => { + const $count = signal("count", 0); + const $doubleCount = wire(($) => $count($) * 2); // explicit subscription + + // setTimeout(() => { + // //$count(6); + // api.insert(["div"],
    text
    , "div/1"); + // }, 1000); + // setTimeout(() => { + // //$count(6); + // api.insert(["div"],
    text2
    , "span"); + // }, 2000); + // setTimeout(() => { + // //$count(6); + // //api.remove(["div"], "div/3"); + // }, 4000); + // return ( + //
    + // + // span + //
    { + // e.preventDefault(); + // $count($count() + 1); + // }} + // key="2" + // > + // {wire($count)} + //
    + //
    + // ); + // return ; + return ( +
    +
    +

    + Rocky7 Kitchen Sink +

    +
    +

    + + docs + +

    +
    +
    +
    +

    Signal Example with <When>

    +

    + + {wire($count)} + +

    +

    + Greater than 5:? + $count($) > 5} + views={{ + true: () => { + return

    "TRUE"
    ; + }, + false: () => { + return
    "FALSE"
    ; + }, + }} + > +

    +

    Double count = {$doubleCount}

    +
    +
    +
    +

    Store Example with <Each>

    +
    + +
    +
    +
    +
    +

    Modal with <Portal>

    +
    + +
    +
    +
    +
    +

    SVG Example

    +
    + + + + + +
    +
    +
    + ); + } +); diff --git a/examples/kitchen-sink/src/main.tsx b/examples/kitchen-sink/src/main.tsx new file mode 100644 index 0000000..1573cad --- /dev/null +++ b/examples/kitchen-sink/src/main.tsx @@ -0,0 +1,22 @@ +/** @jsx h **/ + +import { h, render } from "rocky7"; + +import { Layout } from "./index"; + +const renderApp = (props: { Layout: typeof Layout }) => { + const { Layout } = props; + const el = ; + console.time("render"); + const renderContext = render(el, document.getElementById("app")!); + console.timeEnd("render"); + console.log(renderContext); +}; + +window.addEventListener("load", () => renderApp({ Layout })); + +if (import.meta.hot) { + import.meta.hot.accept("./index", (newModule) => { + if (newModule) renderApp(newModule as unknown as { Layout: typeof Layout }); + }); +} diff --git a/examples/kitchen-sink/src/vite-env.d.ts b/examples/kitchen-sink/src/vite-env.d.ts new file mode 100644 index 0000000..596caff --- /dev/null +++ b/examples/kitchen-sink/src/vite-env.d.ts @@ -0,0 +1,12 @@ +/// +interface ImportMetaEnv { + readonly VITE_SUPABASE_ANON_KEY: string; + readonly VITE_SUPABASE_URL: string; + readonly VITE_VERCEL_GIT_PULL_REQUEST_ID: string; + readonly VITE_VERCEL_GIT_COMMIT_REF: string; + readonly VITE_VERCEL_GIT_COMMIT_SHA: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/examples/kitchen-sink/tsconfig.json b/examples/kitchen-sink/tsconfig.json new file mode 100644 index 0000000..61e21ca --- /dev/null +++ b/examples/kitchen-sink/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "Node", + "strict": true, + "jsx": "react", + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "lib": ["ESNext", "DOM", "dom.iterable"], + "skipLibCheck": true, + "noEmit": true, + "paths": {} + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx"] +} diff --git a/minify.js b/minify.js new file mode 100644 index 0000000..a85b4eb --- /dev/null +++ b/minify.js @@ -0,0 +1,29 @@ +const Terser = require("terser"); +const { globSync } = require("glob"); +const fs = require("fs"); +const path = require("path"); + +// Use a glob pattern to select all .js files in the 'src' directory and its subdirectories +console.log("minfiuing", __dirname + "/dist/**/*.js"); +const files = globSync(__dirname + "/dist/esm/**/*.js", (err, files) => {}); + +console.log("f"); +console.log(files); + +files.forEach((file) => { + console.log(file); + fs.readFile(file, "utf8", async (err, data) => { + if (err) throw err; + + // Minify the file contents + const result = await Terser.minify(data); + if (result.error) throw result.error; + + // Write the minified contents to a new file in the 'dist' directory + const distFilePath = file; + console.log(result); + fs.writeFile(distFilePath, result.code, (err) => { + if (err) throw err; + }); + }); +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..72d823c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1662 @@ +{ + "name": "rocky7", + "version": "0.3.15", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rocky7", + "version": "0.3.15", + "license": "MIT", + "dependencies": { + "@gullerya/object-observer": "^6.1.1" + }, + "devDependencies": { + "esbuild": "^0.18.11", + "esbuild-postcss": "^0.0.4", + "esbuild-sass-plugin": "^2.10.0", + "postcss": "^8.4.25", + "postcss-modules": "^6.0.0", + "sass": "^1.63.6", + "typescript": "^5.2.2", + "vite-tsconfig-paths": "^4.2.0", + "vitest": "^0.28.5" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.11.tgz", + "integrity": "sha512-q4qlUf5ucwbUJZXF5tEQ8LF7y0Nk4P58hOsGk3ucY0oCwgQqAnqXVbUuahCddVHfrxmpyewRpiTHwVHIETYu7Q==", + "cpu": ["arm"], + "dev": true, + "optional": true, + "os": ["android"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.11.tgz", + "integrity": "sha512-snieiq75Z1z5LJX9cduSAjUr7vEI1OdlzFPMw0HH5YI7qQHDd3qs+WZoMrWYDsfRJSq36lIA6mfZBkvL46KoIw==", + "cpu": ["arm64"], + "dev": true, + "optional": true, + "os": ["android"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.11.tgz", + "integrity": "sha512-iPuoxQEV34+hTF6FT7om+Qwziv1U519lEOvekXO9zaMMlT9+XneAhKL32DW3H7okrCOBQ44BMihE8dclbZtTuw==", + "cpu": ["x64"], + "dev": true, + "optional": true, + "os": ["android"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.11.tgz", + "integrity": "sha512-Gm0QkI3k402OpfMKyQEEMG0RuW2LQsSmI6OeO4El2ojJMoF5NLYb3qMIjvbG/lbMeLOGiW6ooU8xqc+S0fgz2w==", + "cpu": ["arm64"], + "dev": true, + "optional": true, + "os": ["darwin"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.11.tgz", + "integrity": "sha512-N15Vzy0YNHu6cfyDOjiyfJlRJCB/ngKOAvoBf1qybG3eOq0SL2Lutzz9N7DYUbb7Q23XtHPn6lMDF6uWbGv9Fw==", + "cpu": ["x64"], + "dev": true, + "optional": true, + "os": ["darwin"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.11.tgz", + "integrity": "sha512-atEyuq6a3omEY5qAh5jIORWk8MzFnCpSTUruBgeyN9jZq1K/QI9uke0ATi3MHu4L8c59CnIi4+1jDKMuqmR71A==", + "cpu": ["arm64"], + "dev": true, + "optional": true, + "os": ["freebsd"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.11.tgz", + "integrity": "sha512-XtuPrEfBj/YYYnAAB7KcorzzpGTvOr/dTtXPGesRfmflqhA4LMF0Gh/n5+a9JBzPuJ+CGk17CA++Hmr1F/gI0Q==", + "cpu": ["x64"], + "dev": true, + "optional": true, + "os": ["freebsd"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.11.tgz", + "integrity": "sha512-Idipz+Taso/toi2ETugShXjQ3S59b6m62KmLHkJlSq/cBejixmIydqrtM2XTvNCywFl3VC7SreSf6NV0i6sRyg==", + "cpu": ["arm"], + "dev": true, + "optional": true, + "os": ["linux"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.11.tgz", + "integrity": "sha512-c6Vh2WS9VFKxKZ2TvJdA7gdy0n6eSy+yunBvv4aqNCEhSWVor1TU43wNRp2YLO9Vng2G+W94aRz+ILDSwAiYog==", + "cpu": ["arm64"], + "dev": true, + "optional": true, + "os": ["linux"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.11.tgz", + "integrity": "sha512-S3hkIF6KUqRh9n1Q0dSyYcWmcVa9Cg+mSoZEfFuzoYXXsk6196qndrM+ZiHNwpZKi3XOXpShZZ+9dfN5ykqjjw==", + "cpu": ["ia32"], + "dev": true, + "optional": true, + "os": ["linux"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.11.tgz", + "integrity": "sha512-MRESANOoObQINBA+RMZW+Z0TJWpibtE7cPFnahzyQHDCA9X9LOmGh68MVimZlM9J8n5Ia8lU773te6O3ILW8kw==", + "cpu": ["loong64"], + "dev": true, + "optional": true, + "os": ["linux"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.11.tgz", + "integrity": "sha512-qVyPIZrXNMOLYegtD1u8EBccCrBVshxMrn5MkuFc3mEVsw7CCQHaqZ4jm9hbn4gWY95XFnb7i4SsT3eflxZsUg==", + "cpu": ["mips64el"], + "dev": true, + "optional": true, + "os": ["linux"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.11.tgz", + "integrity": "sha512-T3yd8vJXfPirZaUOoA9D2ZjxZX4Gr3QuC3GztBJA6PklLotc/7sXTOuuRkhE9W/5JvJP/K9b99ayPNAD+R+4qQ==", + "cpu": ["ppc64"], + "dev": true, + "optional": true, + "os": ["linux"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.11.tgz", + "integrity": "sha512-evUoRPWiwuFk++snjH9e2cAjF5VVSTj+Dnf+rkO/Q20tRqv+644279TZlPK8nUGunjPAtQRCj1jQkDAvL6rm2w==", + "cpu": ["riscv64"], + "dev": true, + "optional": true, + "os": ["linux"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.11.tgz", + "integrity": "sha512-/SlRJ15XR6i93gRWquRxYCfhTeC5PdqEapKoLbX63PLCmAkXZHY2uQm2l9bN0oPHBsOw2IswRZctMYS0MijFcg==", + "cpu": ["s390x"], + "dev": true, + "optional": true, + "os": ["linux"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.11.tgz", + "integrity": "sha512-xcncej+wF16WEmIwPtCHi0qmx1FweBqgsRtEL1mSHLFR6/mb3GEZfLQnx+pUDfRDEM4DQF8dpXIW7eDOZl1IbA==", + "cpu": ["x64"], + "dev": true, + "optional": true, + "os": ["linux"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.11.tgz", + "integrity": "sha512-aSjMHj/F7BuS1CptSXNg6S3M4F3bLp5wfFPIJM+Km2NfIVfFKhdmfHF9frhiCLIGVzDziggqWll0B+9AUbud/Q==", + "cpu": ["x64"], + "dev": true, + "optional": true, + "os": ["netbsd"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.11.tgz", + "integrity": "sha512-tNBq+6XIBZtht0xJGv7IBB5XaSyvYPCm1PxJ33zLQONdZoLVM0bgGqUrXnJyiEguD9LU4AHiu+GCXy/Hm9LsdQ==", + "cpu": ["x64"], + "dev": true, + "optional": true, + "os": ["openbsd"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.11.tgz", + "integrity": "sha512-kxfbDOrH4dHuAAOhr7D7EqaYf+W45LsAOOhAet99EyuxxQmjbk8M9N4ezHcEiCYPaiW8Dj3K26Z2V17Gt6p3ng==", + "cpu": ["x64"], + "dev": true, + "optional": true, + "os": ["sunos"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.11.tgz", + "integrity": "sha512-Sh0dDRyk1Xi348idbal7lZyfSkjhJsdFeuC13zqdipsvMetlGiFQNdO+Yfp6f6B4FbyQm7qsk16yaZk25LChzg==", + "cpu": ["arm64"], + "dev": true, + "optional": true, + "os": ["win32"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.11.tgz", + "integrity": "sha512-o9JUIKF1j0rqJTFbIoF4bXj6rvrTZYOrfRcGyL0Vm5uJ/j5CkBD/51tpdxe9lXEDouhRgdr/BYzUrDOvrWwJpg==", + "cpu": ["ia32"], + "dev": true, + "optional": true, + "os": ["win32"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.11.tgz", + "integrity": "sha512-rQI4cjLHd2hGsM1LqgDI7oOCYbQ6IBOVsX9ejuRMSze0GqXUG2ekwiKkiBU1pRGSeCqFFHxTrcEydB2Hyoz9CA==", + "cpu": ["x64"], + "dev": true, + "optional": true, + "os": ["win32"], + "engines": { + "node": ">=12" + } + }, + "node_modules/@gullerya/object-observer": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@gullerya/object-observer/-/object-observer-6.1.1.tgz", + "integrity": "sha512-bEfCYXWHbv3p10gnxTUGNOoTh96yQHl/uRCp0zXT+BgYJyV3Rte/bpFvBNp7SXYZvbsLjCi6jx6+/w7buNP0aw==", + "funding": [ + { + "url": "https://paypal.me/gullerya?locale.x=en_US" + }, + { + "url": "https://tidelift.com/funding/github/npm/object-observer" + } + ] + }, + "node_modules/@types/chai": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", + "dev": true + }, + "node_modules/@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/node": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.1.tgz", + "integrity": "sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg==", + "dev": true + }, + "node_modules/@vitest/expect": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.28.5.tgz", + "integrity": "sha512-gqTZwoUTwepwGIatnw4UKpQfnoyV0Z9Czn9+Lo2/jLIt4/AXLTn+oVZxlQ7Ng8bzcNkR+3DqLJ08kNr8jRmdNQ==", + "dev": true, + "dependencies": { + "@vitest/spy": "0.28.5", + "@vitest/utils": "0.28.5", + "chai": "^4.3.7" + } + }, + "node_modules/@vitest/runner": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.28.5.tgz", + "integrity": "sha512-NKkHtLB+FGjpp5KmneQjTcPLWPTDfB7ie+MmF1PnUBf/tGe2OjGxWyB62ySYZ25EYp9krR5Bw0YPLS/VWh1QiA==", + "dev": true, + "dependencies": { + "@vitest/utils": "0.28.5", + "p-limit": "^4.0.0", + "pathe": "^1.1.0" + } + }, + "node_modules/@vitest/spy": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.28.5.tgz", + "integrity": "sha512-7if6rsHQr9zbmvxN7h+gGh2L9eIIErgf8nSKYDlg07HHimCxp4H6I/X/DPXktVPPLQfiZ1Cw2cbDIx9fSqDjGw==", + "dev": true, + "dependencies": { + "tinyspy": "^1.0.2" + } + }, + "node_modules/@vitest/utils": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.28.5.tgz", + "integrity": "sha512-UyZdYwdULlOa4LTUSwZ+Paz7nBHGTT72jKwdFSV4IjHF1xsokp+CabMdhjvVhYwkLfO88ylJT46YMilnkSARZA==", + "dev": true, + "dependencies": { + "cli-truncate": "^3.1.0", + "diff": "^5.1.0", + "loupe": "^2.3.6", + "picocolors": "^1.0.0", + "pretty-format": "^27.5.1" + } + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.11.tgz", + "integrity": "sha512-i8u6mQF0JKJUlGR3OdFLKldJQMMs8OqM9Cc3UCi9XXziJ9WERM5bfkHaEAy0YAvPRMgqSW55W7xYn84XtEFTtA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.11", + "@esbuild/android-arm64": "0.18.11", + "@esbuild/android-x64": "0.18.11", + "@esbuild/darwin-arm64": "0.18.11", + "@esbuild/darwin-x64": "0.18.11", + "@esbuild/freebsd-arm64": "0.18.11", + "@esbuild/freebsd-x64": "0.18.11", + "@esbuild/linux-arm": "0.18.11", + "@esbuild/linux-arm64": "0.18.11", + "@esbuild/linux-ia32": "0.18.11", + "@esbuild/linux-loong64": "0.18.11", + "@esbuild/linux-mips64el": "0.18.11", + "@esbuild/linux-ppc64": "0.18.11", + "@esbuild/linux-riscv64": "0.18.11", + "@esbuild/linux-s390x": "0.18.11", + "@esbuild/linux-x64": "0.18.11", + "@esbuild/netbsd-x64": "0.18.11", + "@esbuild/openbsd-x64": "0.18.11", + "@esbuild/sunos-x64": "0.18.11", + "@esbuild/win32-arm64": "0.18.11", + "@esbuild/win32-ia32": "0.18.11", + "@esbuild/win32-x64": "0.18.11" + } + }, + "node_modules/esbuild-postcss": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/esbuild-postcss/-/esbuild-postcss-0.0.4.tgz", + "integrity": "sha512-CKYibp+aCswskE+gBPnGZ0b9YyuY0n9w2dxyMaoLYEvGTwmjkRj5SV8l1zGJpw8KylqmcMTK0Gr349RnOLd+8A==", + "dev": true, + "dependencies": { + "postcss-load-config": "^3.1.0" + }, + "peerDependencies": { + "esbuild": "*", + "postcss": "^8.0.0" + } + }, + "node_modules/esbuild-sass-plugin": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.10.0.tgz", + "integrity": "sha512-STv849QGT8g77RRFmroSt4VBVKjv+dypKcO4aWz8IP4G5JbRH0KC0+B8ODuzlUNu9R5MbkGcev/62RDP/JcZ2Q==", + "dev": true, + "dependencies": { + "resolve": "^1.22.2", + "sass": "^1.63.0" + }, + "peerDependencies": { + "esbuild": "^0.18.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": ["darwin"], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/generic-names": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-4.0.0.tgz", + "integrity": "sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==", + "dev": true, + "dependencies": { + "loader-utils": "^3.2.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/immutable": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", + "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, + "node_modules/loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "node_modules/mlly": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.0.tgz", + "integrity": "sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "pathe": "^1.1.1", + "pkg-types": "^1.0.3", + "ufo": "^1.1.2" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/pathe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", + "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, + "node_modules/postcss": { + "version": "8.4.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.25.tgz", + "integrity": "sha512-7taJ/8t2av0Z+sQEvNzCkpDynl0tX3uJMCODi6nT3PfASC7dYCWV9aQ+uiCf+KBD4SEFcu+GvJdGdwzQ6OSjCw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-modules": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-6.0.0.tgz", + "integrity": "sha512-7DGfnlyi/ju82BRzTIjWS5C4Tafmzl3R79YP/PASiocj+aa6yYphHhhKUOEoXQToId5rgyFgJ88+ccOUydjBXQ==", + "dev": true, + "dependencies": { + "generic-names": "^4.0.0", + "icss-utils": "^5.1.0", + "lodash.camelcase": "^4.3.0", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "string-hash": "^1.1.1" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", + "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "3.26.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.26.2.tgz", + "integrity": "sha512-6umBIGVz93er97pMgQO08LuH3m6PUb3jlDUUGFsNJB6VgTCUaDFpupf5JfU30529m/UKOgmiX+uY6Sx8cOYpLA==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/sass": { + "version": "1.63.6", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.63.6.tgz", + "integrity": "sha512-MJuxGMHzaOW7ipp+1KdELtqKbfAWbH7OLIdoSMnVe3EXPMTmxTmlaZDCTsgIpPCs3w99lLo9/zDKkOrJuT5byw==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "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 + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.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 + }, + "node_modules/std-env": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.3.tgz", + "integrity": "sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==", + "dev": true + }, + "node_modules/string-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", + "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==", + "dev": true + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-literal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.0.1.tgz", + "integrity": "sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==", + "dev": true, + "dependencies": { + "acorn": "^8.8.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tinybench": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.0.tgz", + "integrity": "sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.3.1.tgz", + "integrity": "sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-1.1.1.tgz", + "integrity": "sha512-UVq5AXt/gQlti7oxoIg5oi/9r0WpF7DGEVwXgqWSMmyN16+e3tl5lIvTaOpJ3TAtu5xFzWccFRM4R5NaWHF+4g==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tsconfck": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-2.1.1.tgz", + "integrity": "sha512-ZPCkJBKASZBmBUNqGHmRhdhM8pJYDdOXp4nRgj/O0JwUwsMq50lCDRQP/M5GBNAA0elPrq4gAeu4dkaVCuKWww==", + "dev": true, + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^14.13.1 || ^16 || >=18" + }, + "peerDependencies": { + "typescript": "^4.3.5 || ^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/ufo": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.2.tgz", + "integrity": "sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==", + "dev": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.2.tgz", + "integrity": "sha512-zUcsJN+UvdSyHhYa277UHhiJ3iq4hUBwHavOpsNUGsTgjBeoBlK8eDt+iT09pBq0h9/knhG/SPrZiM7cGmg7NA==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.24", + "rollup": "^3.25.2" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.28.5.tgz", + "integrity": "sha512-LmXb9saMGlrMZbXTvOveJKwMTBTNUH66c8rJnQ0ZPNX+myPEol64+szRzXtV5ORb0Hb/91yq+/D3oERoyAt6LA==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "mlly": "^1.1.0", + "pathe": "^1.1.0", + "picocolors": "^1.0.0", + "source-map": "^0.6.1", + "source-map-support": "^0.5.21", + "vite": "^3.0.0 || ^4.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": ">=v14.16.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-jGpus0eUy5qbbMVGiTxCL1iB9ZGN6Bd37VGLJU39kTDD6ZfULTTb1bcc5IeTWqWJKiWV5YihCaibeASPiGi8kw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^2.1.0" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.28.5.tgz", + "integrity": "sha512-pyCQ+wcAOX7mKMcBNkzDwEHRGqQvHUl0XnoHR+3Pb1hytAHISgSxv9h0gUiSiYtISXUU3rMrKiKzFYDrI6ZIHA==", + "dev": true, + "dependencies": { + "@types/chai": "^4.3.4", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "@vitest/expect": "0.28.5", + "@vitest/runner": "0.28.5", + "@vitest/spy": "0.28.5", + "@vitest/utils": "0.28.5", + "acorn": "^8.8.1", + "acorn-walk": "^8.2.0", + "cac": "^6.7.14", + "chai": "^4.3.7", + "debug": "^4.3.4", + "local-pkg": "^0.4.2", + "pathe": "^1.1.0", + "picocolors": "^1.0.0", + "source-map": "^0.6.1", + "std-env": "^3.3.1", + "strip-literal": "^1.0.0", + "tinybench": "^2.3.1", + "tinypool": "^0.3.1", + "tinyspy": "^1.0.2", + "vite": "^3.0.0 || ^4.0.0", + "vite-node": "0.28.5", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": ">=v14.16.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@vitest/browser": "*", + "@vitest/ui": "*", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package-publish.json b/package-publish.json new file mode 100644 index 0000000..64347dd --- /dev/null +++ b/package-publish.json @@ -0,0 +1,15 @@ +{ + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./state": { + "types": "./dist/core/index.d.ts", + "import": "./dist/core/index.js" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4699995 --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "name": "rocky7", + "version": "0.3.18", + "author": "Abhishiv Saxena", + "license": "MIT", + "description": "Fine-grained reactive library with no compiler, no magic, and no virtual DOM", + "keywords": [ + "reactive", + "dom", + "ui", + "frontend", + "framework" + ], + "repository": { + "type": "git", + "url": "https://github.com/abhishiv/rocky7" + }, + "files": [ + "./dist" + ], + "devDependencies": { + "esbuild": "^0.18.11", + "typescript": "^5.2.2", + "vitest": "^0.28.5" + }, + "scripts": { + "build": "make build", + "test": "npx vitest --run", + "coverage": "npx vitest run --coverage --run", + "typecheck": "npx tsc --noEmit" + }, + "sideEffects": false, + "main": "./src/index.ts", + "type": "module", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "require": "./src/index.ts" + }, + "./state": { + "types": "./src/core/index.ts", + "import": "./src/core/index.ts", + "require": "./src/core/index.ts" + } + }, + "dependencies": { + "on-change": "^5.0.1" + } +} diff --git a/src/core/constants.ts b/src/core/constants.ts new file mode 100644 index 0000000..1aa62fc --- /dev/null +++ b/src/core/constants.ts @@ -0,0 +1,4 @@ +export const WIRE = 0 as const; +export const SIGNAL = 1 as const; +export const STORE = 2 as const; +export const SUBTOKEN = 3 as const; diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 0000000..be6f715 --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1 @@ +export * from "./state"; diff --git a/src/core/state.test.ts b/src/core/state.test.ts new file mode 100644 index 0000000..a97bbff --- /dev/null +++ b/src/core/state.test.ts @@ -0,0 +1,73 @@ +import { assert, expect, test, describe, vi } from "vitest"; +import { createSignal, createWire, createStore, reify, produce } from "./state"; + +describe("Basic Implementation of Signals & Wires", (test) => { + test("Signal", () => { + const val = 1; + const sig = createSignal(val); + expect(sig).toBeDefined(); + expect(sig()).toBe(val); + }); + + test("Wire", () => { + const sig = createSignal(2); + const w = createWire(($, wire) => { + const val = $(sig); + return val; + }); + expect(w()).toBe(sig()); + }); +}); + +describe("Nested Signals & Wires", (test) => { + test("Nested Wires should cleanup and not fire multiple times in case of nested wires", () => { + const c = { log: (...v: any[]) => console.log(...v) }; + const lSpy = vi.spyOn(c, "log"); + const sig = createSignal(1); + const w = createWire(($, wire) => { + const val = $(sig); + console.log("count", val); + + const b = wire(($, wire) => { + const doubleCount = sig($) * 2; + c.log("doublecount", doubleCount); + return val; + }); + b(); + return val; + }); + w(); + sig(4); + expect(lSpy.mock.calls.length).toBe(2); + }); +}); + +describe("Basic Implementation of Stores & Wires", (test) => { + // test that wire only runs when subscribed cursors are updated + test("Wire", () => { + const c = { log: (...v: any[]) => console.log(...v) }; + const lSpy = vi.spyOn(c, "log"); + const val: { list: number[]; friends: { id: string; name: string }[] } = { + list: [1, 2, 3], + friends: [{ id: "2", name: "" }], + }; + const s = createStore(val); + + const w = createWire(($, wire) => { + const v = $(s.friends[0].id); + console.log(v); + c.log(JSON.stringify(v)); + return v; + }); + w(); + + produce(s.friends, (obj) => { + obj.push({ id: "2", name: "" }); + }); + produce(s.friends, (obj) => { + obj[0].id = "33"; + }); + + expect(lSpy.mock.calls.length).toBe(2); + }); +}); diff --git a/src/core/state.ts b/src/core/state.ts new file mode 100644 index 0000000..820da5e --- /dev/null +++ b/src/core/state.ts @@ -0,0 +1 @@ +export * from "./state/experimental"; diff --git a/src/core/state/experimental.ts b/src/core/state/experimental.ts new file mode 100644 index 0000000..f63b1e1 --- /dev/null +++ b/src/core/state/experimental.ts @@ -0,0 +1,370 @@ +//import { Observable, Change } from "@gullerya/object-observer"; +import onChange, { ApplyData } from "on-change"; +import { + wrapWithProxy, + StoreProxy, + isProxy, + getProxyPath, + getProxyMeta, + ObjPathProxy, +} from "../../utils/observer"; +import * as Constants from "../constants"; +import { getValueUsingPath } from "../../utils/index"; + +export type { ObjPathProxy } from "../../utils/observer"; +export { getProxyMeta, getProxyPath } from "../../utils/observer"; + +export type Signal = { + id: string; + (): T; + /** Write value; notifying wires */ + // Ordered before ($):T for TS to work + (value: T): void; + /** Read value & subscribe */ + ($: SubToken): T; + /** Wires subscribed to this signal */ + wires: Set>; + /** To check "if x is a signal" */ + type: typeof Constants.SIGNAL; + + value: T; +}; + +export type StoreCursor> = StoreProxy; +type extractGeneric = Type extends ObjPathProxy + ? X + : never; + +export type StoreManager = { + id: string; + value: T; + rootCursor: StoreCursor; + /** Wires subscribed to this signal */ + wires: Set>; + type: typeof Constants.STORE; + tasks: Set<{ path: string[]; observor: Function }>; +}; + +export type StoreChange = { + path: string[]; + data: ApplyData; + value: any; +}; + +/** 3 bits: [RUNNING][SKIP_RUN_QUEUE][NEEDS_RUN] */ +type WireState = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; + +export type Wire = { + id: string; + /** Run the wire */ + (): T; + fn: WireFunction; //| StoreCursor; + /** Signals read-subscribed last run */ + sigRS: Set; + storesRS: WeakMap>; + /** Post-run tasks */ + tasks: Set<(nextValue: T) => void>; + /** Wire that created this wire (parent of this child) */ + upper: Wire | undefined; + /** Wires created during this run (children of this parent) */ + lower: Set; + /** FSM state 3-bit bitmask: [RUNNING][SKIP_RUN_QUEUE][NEEDS_RUN] */ + /** Run count */ + run: number; + value?: T; + /** To check "if x is a wire" */ + type: typeof Constants.WIRE; + token: SubToken; + subWire: WireFactory; + state: WireState; +}; + +export type SubToken = { + /** Allow $(...signals) to return an array of read values */ + (sig: Signal | T): T; + /** Wire to subscribe to */ + wire: Wire; + /** To check "if x is a subscription token" */ + type: typeof Constants.SUBTOKEN; +}; + +export type WireFunction = { + ($: SubToken, wire: WireFactory): T; +}; + +export type WireFactory = ( + arg: WireFunction | Signal //| StoreCursor +) => Wire; + +// Symbol() doesn't gzip well. `[] as const` gzips best but isn't debuggable +// without a lookup Map<> and other hacks. +const S_RUNNING = 0b100; +const S_SKIP_RUN_QUEUE = 0b010; +const S_NEEDS_RUN = 0b001; + +let SIGNAL_COUNTER = 0; +let WIRE_COUNTER = 0; +let STORE_COUNTER = 0; + +export const createWire: WireFactory = (arg) => { + const w: Partial = () => { + const val = runWire(arg, $, subWireFactory); + // Clean up unused nested wires + return val; + }; + const subWireFactory: WireFactory = (subFn) => { + const subWire = createWire(subFn); + subWire.upper = w as Wire; + (w as Wire).lower.add(subWire); + return subWire; + }; + const $: SubToken = getSubtoken(w as Wire); + WIRE_COUNTER++; + w.id = "wire|" + WIRE_COUNTER; + w.sigRS = new Set(); + w.storesRS = new WeakMap(); + w.tasks = new Set(); + w.lower = new Set(); + w.run = 0; + w.type = Constants.WIRE; + w.fn = arg; + w.token = $; + w.subWire = subWireFactory; + return w as Wire; +}; + +const runWire = ( + arg: WireFunction | Signal | StoreCursor, + token: SubToken, + subWireFactory: WireFactory +) => { + if (isProxy(arg)) { + const cursor = arg as StoreCursor; + const v = token(cursor); + token.wire.value = v; + //console.log("vvv", value, cursor); + // debugger; + return v; + } else if ((arg as Signal).type === Constants.SIGNAL) { + const sig = arg as Signal; + const v = token(sig); + token.wire.value = v; + return v; + } else { + const fn = arg as WireFunction; + const v = fn(token, subWireFactory); + token.wire.value = v; + return v; + } +}; + +const wireReset = (wire: Wire): void => { + wire.lower.forEach(wireReset); + wire.sigRS.forEach((signal) => signal.wires.delete(wire)); + _initWire(wire); +}; + +const _initWire = (wire: Wire): void => { + wire.state = S_NEEDS_RUN; + wire.lower = new Set(); + // Drop all signals now that they have been unlinked + wire.sigRS = new Set(); +}; + +/** + * Pauses a wire so signal writes won't cause runs. Affects nested wires */ +const wirePause = (wire: Wire): void => { + wire.lower.forEach(wirePause); + wire.state |= S_SKIP_RUN_QUEUE; +}; + +/** + * Resumes a paused wire. Affects nested wires but skips wires belonging to + * computed-signals. Returns true if any runs were missed during the pause */ +const wireResume = (wire: Wire): boolean => { + wire.lower.forEach(wireResume); + // Clears SKIP_RUN_QUEUE only if it's NOT a computed-signal + // if (!wire.cs) + wire.state &= ~S_SKIP_RUN_QUEUE; + return !!(wire.state & S_NEEDS_RUN); +}; + +const _runWires = (wires: Set>): void => { + // Use a new Set() to avoid infinite loops caused by wires writing to signals + // during their run. + const toRun = new Set(wires); + let curr: Wire | undefined; + // Mark upstream computeds as stale. Must be in an isolated for-loop + toRun.forEach((wire) => { + if (wire.state & S_SKIP_RUN_QUEUE) { + toRun.delete(wire); + wire.state |= S_NEEDS_RUN; + } + // TODO: Test (#3) + Benchmark with main branch + // If a wire's ancestor will run it'll destroy its lower wires. It's more + // efficient to not call them at all by deleting from the run list: + curr = wire; + while ((curr = curr.upper)) if (toRun.has(curr)) return toRun.delete(wire); + }); + toRun.forEach((wire) => { + const previousValue = wire.value; + const val = runWire(wire.fn, wire.token, wire.subWire); + //console.log("val", val, previousValue); + wire.run = wire.run + 1; + if (val === previousValue) return; + //console.log("www", wire.value, previousValue, wire); + //if (wire.value !== previousValue) { + wire.value = val; + for (const task of wire.tasks) { + task(val); + } + //} + }); +}; + +export const createSignal = (val: T): Signal => { + const s: Partial = function (arg?: T) { + const sig = s as Signal; + if (arguments.length == 0) { + return s.value; + } else if ( + arg && + (arg as unknown as SubToken).type === Constants.SUBTOKEN + ) { + const token = arg as unknown as SubToken; + // Two-way link. Signal writes will now call/update wire W + token.wire.sigRS.add(sig); + sig.wires.add(token.wire); + return s.value; + } else { + s.value = arg; + _runWires(sig.wires); + return val; + } + }; + SIGNAL_COUNTER++; + s.id = "signal|" + SIGNAL_COUNTER; + s.value = val; + s.wires = new Set(); + s.type = Constants.SIGNAL; + return s as Signal; +}; +// just to make api similar to haptic +createSignal.anon = createSignal; + +export const createStore = ( + obj: T +): StoreCursor> => { + const storeManager: Partial> = { + wires: new Set(), + type: Constants.STORE, + tasks: new Set(), + }; + const observedObject = onChange( + obj as Record, + (p, value, previousValue, change) => { + const changePath = p.split("."); + const toRun = new Set(); + console.log("chamge", change, changePath); + // todo: improve this logic + const manager = storeManager as StoreManager; + for (const wire of manager.wires) { + const cursors = wire.storesRS.get(manager); + if (cursors) { + for (var cursorStr of cursors) { + let match: boolean | undefined; + const cursor = decodeCursor(cursorStr); + if (change === undefined) { + match = + encodeCursor(changePath.slice(0, cursor.length)) == cursorStr; + } else if ( + change && + ["splice", "push", "pop"].indexOf(change.name) > -1 + ) { + // todo: adjust cursors to handle this case + } + if (match) toRun.add(wire); + } + } + } + //console.log([...toRun]); + _runWires(toRun); + + [...manager.tasks].forEach(({ path, observor }) => { + console.log({ path, changePath, change }); + if (path.join("/") === changePath.join("/")) { + observor({ data: change, path: changePath, value }); + } + }); + + // patch == { op:"replace", path="/firstName", value:"Albert"} + }, + {} + ); + console.log(observedObject); + const s = wrapWithProxy>(observedObject, storeManager); + + storeManager.value = observedObject; + STORE_COUNTER++; + storeManager.id = "store|" + STORE_COUNTER; + return s; +}; + +export const reify = (cursor: T): extractGeneric => { + const s = cursor as unknown as StoreCursor; + const manager: StoreManager = getProxyMeta(s); + const cursorPath = getProxyPath(s); + // console.log({ cursorPath, manager }); + //console.log(JSON.stringify(manager.value)); + const v = getValueUsingPath(manager.value as any, cursorPath); + //console.log({ v, cursorPath }); + return v as extractGeneric; +}; + +export const produce = ( + cursor: T, + setter: (obj: extractGeneric) => void +): void => { + const v = reify(cursor); + setter(v); +}; + +const encodeCursor = (cursor: string[]) => + cursor.map(encodeURIComponent).join("/"); +const decodeCursor = (str: string) => str.split("/").map(decodeURIComponent); + +// todo: figure how to annotate values from store.cursor with Symbol +const getSubtoken = (wire: Wire): SubToken => { + const token: Partial = (arg: Signal | StoreCursor) => { + //console.log("arg", arg); + if (isProxy(arg)) { + const cursor = arg as StoreCursor; + const cursorPath = getProxyPath(cursor); + // todo: improve ts here and remove typecast + const manager = getProxyMeta(cursor); + + const encodedCursor = encodeCursor(cursorPath); + + manager.wires.add(wire); + if (wire.storesRS.has(manager)) { + wire.storesRS.get(manager)?.add(encodedCursor); + } else { + const set = new Set(); + set.add(encodedCursor); + wire.storesRS.set(manager, set); + } + const v = getValueUsingPath(manager.value as any, cursorPath); + //console.log("v", v); + wire.value = v; + return v; + } else { + const sig = arg as Signal; + const v = sig(token); + wire.value = v; + return v; + } + }; + token.wire = wire; + token.type = Constants.SUBTOKEN; + return token as SubToken; +}; diff --git a/src/dom/api.ts b/src/dom/api.ts new file mode 100644 index 0000000..c070d5d --- /dev/null +++ b/src/dom/api.ts @@ -0,0 +1,281 @@ +import { + VElement, + TreeStep, + RenderContext, + ComponentTreeStep, + ComponentUtils, + EEmitter, +} from "./types"; +import { + getDescendants, + getContextProvider, + createError, + getVirtualElementId, + arrayRemove, + getTreeStepRenderContextState, +} from "./utils"; +import * as DOMConstants from "./constants"; +import { reifyTree } from "./traverser"; +import { + Signal, + createSignal, + createWire, + createStore, + StoreCursor, +} from "../core/state"; +import { LiveDocumentFragment } from "./dom"; + +export const insertElement = ( + renderContext: RenderContext, + parentStep: TreeStep, + parentPath: string[], + el: VElement, + after?: string, +) => { + console.log("insert", parentPath, el, after); + + const step = parentPath.reduce((step, key) => { + if (!step) return; + const child = step.children.find((el) => el.id === key); + return child; + }, parentStep); + if (!step) throw new Error(""); + + const afterIndex = after + ? step.children.findIndex((el) => el.id === after) + : undefined; + + const { root, registry } = reifyTree(renderContext, el, step, afterIndex); + + addNode( + renderContext, + step, + root, + afterIndex && afterIndex > -1 + ? step.children[afterIndex as number] + : undefined, + ); +}; + +export const removeElement = ( + renderContext: RenderContext, + parentStep: TreeStep, + parentPath: string[], + key: string, +) => { + const step = parentPath.reduce((step, key) => { + if (!step) return; + const child = step.children.find((el) => el.id === key); + return child; + }, parentStep); + if (!step) throw createError(102); + const child = step.children.find((el) => el.id === key); + if (!child) throw createError(102); + removeNode(renderContext, child); +}; + +export const updateElement = ( + renderContext: RenderContext, + parentStep: TreeStep, + key: string[], + value: string, +) => { + // todo +}; + +export const addNode = ( + renderCtx: RenderContext, + parentStep: TreeStep, + node: TreeStep, + after?: TreeStep, +) => { + // console.log("addNode", parentStep, node, after); + const handle = (node: TreeStep) => { + node.parent = parentStep; + const nodes = getDescendants(node); + //console.log("n", nodes, nodes.length); + nodes.forEach((n) => renderCtx.reg.add(n)); + + // dom + const parentDOM = parentStep.dom; + if (!node.dom) return; + const elementsToInsert = node.dom; + if (after) { + const afterIndex = parentStep.children.indexOf(after); + parentStep.children.splice(afterIndex, 0, node); + + const refNode: HTMLElement = after.dom as HTMLElement; + refNode.after(elementsToInsert); + } else { + parentStep.children.push(node); + if (parentDOM && (parentDOM as HTMLElement)) { + parentDOM.append(elementsToInsert); + } + } + }; + if (Array.isArray(node)) { + node.forEach(handle); + } else { + handle(node); + } +}; + +export const getLiveFragmentChildNodes = (frag: LiveDocumentFragment) => { + const els: Node[] = []; + let node: Node | null = frag.startMarker; + while (node) { + els.push(node); + if (node === frag.endMarker) { + break; + } + node = node.nextSibling; + } + return els; +}; + +export const rmNodes = (node: Node | LiveDocumentFragment) => { + if (node instanceof LiveDocumentFragment) { + const childNodes = getLiveFragmentChildNodes(node); + childNodes.forEach((c) => c.parentNode?.removeChild(c)); + } else { + node.parentElement?.removeChild(node); + } +}; + +export const removeNode = (renderCtx: RenderContext, node: TreeStep) => { + //console.log("removeNodes", nodes); + const nodes = getDescendants(node); + // console.log("removeNode nodes", node, nodes); + nodes.forEach((step) => { + if (step.dom) { + if ( + step.type === DOMConstants.ComponentTreeStep && + step.onUnmount.length > 0 + ) + step.onUnmount.forEach((el) => el()); + rmNodes(step.dom); + step.dom = undefined; + } + renderCtx.reg.delete(step); + step.parent ? arrayRemove(step.parent.children, step) : null; + }); +}; + +export const renderTreeStep = (renderCtx: RenderContext, element: VElement) => { + const { root, registry } = reifyTree(renderCtx, element); + const id = getVirtualElementId(root.node); + if (!id) throw createError(101); + + // todo: move this to getRenderContext so it clears DOM properly + renderCtx.el.innerHTML = ""; + + registry.forEach((n) => renderCtx.reg.add(n)); + + root.dom && renderCtx.el.append(root.dom); +}; + +export const getRenderContext = (container: HTMLElement, element: VElement) => { + const id = getVirtualElementId(element); + if (!id) throw createError(101); + + const renderContext: RenderContext = + (container as any)[id] || + ({ + prevState: new Map(), + el: container, + id, + reg: new Set(), + emitter: new EEmitter(), + } as RenderContext); + + renderContext.prevState.clear(); + + renderContext.reg.forEach((step) => { + if ( + step.type === DOMConstants.ComponentTreeStep && + step.onUnmount.length > 0 + ) + step.onUnmount.forEach((el) => el()); + + if (step.type === DOMConstants.ComponentTreeStep) { + const ids: string[] = []; + let ancestor: TreeStep | undefined = step; + while (ancestor) { + // since primitive elements can't have children this is safe + if (ancestor.id) { + ids.push(ancestor.id); + } + ancestor = ancestor.parent; + } + renderContext.prevState.set(ids, step.state); + } + }); + + renderContext.reg.clear(); + + (container as any)[id] = renderContext; + + return renderContext; +}; + +export const getUtils = ( + renderContext: RenderContext, + parentStep: ComponentTreeStep, +): Omit => { + return { + step: parentStep, + renderContext, + signal(name: string, val) { + const match = getTreeStepRenderContextState(renderContext, parentStep); + + // this enables state preservation during HMR + const s = + match && match.signals && match.signals[name] + ? (match.signals[name] as Signal) + : (createSignal.anon(val) as Signal); + parentStep.state.signals[name] = s; + return s; + }, + wire(arg) { + const w = createWire(arg); + parentStep.wires?.push(w); + return w; + }, + store(name: string, val) { + const match = getTreeStepRenderContextState(renderContext, parentStep); + + // HMR + const s = + match && match.stores && match.stores[name] + ? (match.stores[name] as StoreCursor) + : (createStore(val) as StoreCursor); + parentStep.state.stores[name] = s; + return s as StoreCursor; + }, + setContext(ctx, value?) { + parentStep.state.ctx.set(ctx, value || createSignal.anon(undefined)); + }, + getContext: (token) => { + //console.time("ctx"); + const ancestor = parentStep && getContextProvider(token, parentStep); + //console.timeEnd("ctx"); + if (ancestor && ancestor.type === DOMConstants.ComponentTreeStep) { + return ancestor.state.ctx.get(token); + } else { + //console.error(token, parentStep); + //throw createError(103, `${token.sym.toString()} not found`); + } + }, + onUnmount: (cb) => { + (parentStep as ComponentTreeStep).onUnmount.push(cb); + }, + onMount: (cb) => { + (parentStep as ComponentTreeStep).onMount.push(cb); + }, + api: { + insert: insertElement.bind(null, renderContext, parentStep), + remove: removeElement.bind(null, renderContext, parentStep), + update: updateElement.bind(null, renderContext, parentStep), + }, + }; +}; diff --git a/src/dom/constants.ts b/src/dom/constants.ts new file mode 100644 index 0000000..f256fe5 --- /dev/null +++ b/src/dom/constants.ts @@ -0,0 +1,10 @@ +// dom +// starting from 10 since core/constants is 0-10 +export const PrimitiveTreeStep = 10 as const; +export const NativeTreeStep = 11 as const; +export const ComponentTreeStep = 12 as const; +export const WireTreeStep = 13 as const; + +export const NATIVE = 15 as const; +export const COMPONENT = 16 as const; +export const PRIMITIVE = 17 as const; diff --git a/src/dom/dom.ts b/src/dom/dom.ts new file mode 100644 index 0000000..57b0182 --- /dev/null +++ b/src/dom/dom.ts @@ -0,0 +1,127 @@ +import * as Constants from "../core/constants"; +import { TreeStep } from "./types"; +import { Wire } from "../core/state"; +import * as DOMConstants from "./constants"; +const createTextNode = (arg: string | number | boolean | null | undefined) => + arg ? document.createTextNode(arg + "") : undefined; + +const createMarker = (text: string, owner: DocumentFragment) => { + const marker = document.createComment(text); + (marker as any).__owner__ = owner; + return marker; +}; + +export class LiveDocumentFragment extends DocumentFragment { + startMarker: Comment; + endMarker: Comment; + constructor(t?: string) { + super(); + this.startMarker = createMarker(`<${t}>`, this); + this.endMarker = createMarker(``, this); + super.append(this.startMarker, this.endMarker); + } + append(...nodes: Node[]) { + this.endMarker.before(...nodes); + } + prepend(...nodes: Node[]) { + this.startMarker.after(...nodes); + } +} + +export const createDOMNode = ( + step: TreeStep, + isSvg?: boolean +): Node | LiveDocumentFragment | undefined => { + if (!step || !step.node) return; + + // primitive types + if (step.type === DOMConstants.PrimitiveTreeStep) { + return createTextNode(step.node); + } + + // wire: create text node from wire output + if (step.type == DOMConstants.WireTreeStep) { + const wire = step.node as Wire; + const value = wire(); + let childEL: Node | undefined = undefined; + if ( + ["string", "number", "boolean", "undefined"].indexOf(typeof value) > -1 + ) { + childEL = createTextNode(wire() + ""); + wire.tasks.add((val: any) => { + childEL ? (childEL.textContent = val) : null; + }); + } + return childEL; + } + + if (step.node && typeof step.node === "object") { + const props: Record = + step.node.type === DOMConstants.COMPONENT || + step.node.type === DOMConstants.NATIVE + ? step?.node?.p + : {}; + + // function component + if (step.node.type === DOMConstants.COMPONENT) { + return new LiveDocumentFragment(step.node.t.__name__); + } + + // plain DOM node + if (step.node.type === DOMConstants.NATIVE) { + const { t } = step.node; + const el = isSvg + ? document.createElementNS("http://www.w3.org/2000/svg", t) + : document.createElement(step.node.t); + const { children, ...rest } = props; + + for (const key in rest) { + // onClick to onclick + // TODO: handle it better? + + const value = rest[key]; + + const isFunctionHandler = key[0] == "o" && key[1] == "n"; + const finalKey: string = isFunctionHandler ? key.toLowerCase() : key; + if (key === "dangerouslySetInnerHTML") { + el.innerHTML = value.__html; + } else if (key == "ref" && typeof value === "function") { + (value as any)(el); + } else if (isFunctionHandler) { + // set function on el + (el as any)[finalKey] = value; + } else { + if (value && value.type === Constants.WIRE) { + const w: Wire = value; + const val = w(); + setAttributeValue(el, finalKey, val); + w.tasks.add((val) => { + setAttributeValue(el, finalKey, val); + }); + } else { + setAttributeValue(el, finalKey, value); + } + } + } + + return el; + } + } +}; + +const setAttributeValue = ( + el: HTMLElement | SVGElement, + attr: string, + val: any +) => { + try { + if (val === undefined) { + (el as HTMLElement).removeAttribute(attr); + } else { + (el as HTMLElement).setAttribute(attr, val); + } + } catch (e) { + console.error(el, attr, val); + throw e; + } +}; diff --git a/src/dom/index.ts b/src/dom/index.ts new file mode 100644 index 0000000..1dd55a9 --- /dev/null +++ b/src/dom/index.ts @@ -0,0 +1,120 @@ +import * as DOMConstants from "../dom/constants"; +import { + Context, + ComponentUtils, + VElement, + Component, + ComponentVElement, + NativeVElement, +} from "./types"; +import { getRenderContext, renderTreeStep } from "./api"; +import { reifyTree } from "./traverser"; +import type { + GenericEventAttrs, + HTMLAttrs, + SVGAttrs, + HTMLElements, + SVGElements, +} from "./jsx"; +import { Wire } from "../core/state"; + +export * from "./types"; +export * as DOMConstants from "./constants"; +export { + addNode, + removeNode, + insertElement, + removeElement, + updateElement, +} from "./api"; +export { reifyTree } from "./traverser"; + +export function defineContext(arg?: string): Context { + return { + sym: Symbol(arg), + }; +} + +export function component( + name: string, + def: { + (props: T, helpers: ComponentUtils): VElement; + }, +): Component { + (def as Component).__name__ = name; + return def as Component; +} + +function h( + t: string | Component, + p?: any, + ...children: VElement[] +): VElement { + const props = { + ...(p || {}), + children: [...(children || [])].map((el) => el), + }; + return { + type: typeof t === "string" ? DOMConstants.NATIVE : DOMConstants.COMPONENT, + p: props, + t: t, + } as ComponentVElement | NativeVElement; +} +export { h }; + +export function render(element: VElement, container: HTMLElement) { + const renderContext = getRenderContext(container, element); + //console.log("root", root, registry); + renderTreeStep(renderContext, element); + return renderContext; +} + +// used to store parent wire in context +export const ParentWireContext = defineContext("ParentWireContext"); + +export const Fragment = component("Fragment", (props) => props.children); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type DistributeWire = T extends any ? Wire : never; + +export declare namespace h { + export namespace JSX { + export type MaybeWire = T | DistributeWire; + export type AllowWireForProperties = { [K in keyof T]: MaybeWire }; + + export type Element = VElement; + + export interface ElementAttributesProperty { + props: unknown; + } + export interface ElementChildrenAttribute { + children: unknown; + } + + // Prevent children on components that don't declare them + export interface IntrinsicAttributes { + children?: VElement[] | VElement; + key?: string | number; + } + + // Allow children on all DOM elements (not components, see above) + // ESLint will error for children on void elements like + export type DOMAttributes = + GenericEventAttrs & {}; + + export type HTMLAttributes = + AllowWireForProperties> & { + style?: MaybeWire; + } & DOMAttributes & + IntrinsicAttributes; + + export type SVGAttributes = + AllowWireForProperties & + HTMLAttributes & + IntrinsicAttributes; + + export type IntrinsicElements = { + [El in keyof HTMLElements]: HTMLAttributes; + } & { [El in keyof SVGElements]: SVGAttributes }; + } +} diff --git a/src/dom/jsx.ts b/src/dom/jsx.ts new file mode 100644 index 0000000..563cc49 --- /dev/null +++ b/src/dom/jsx.ts @@ -0,0 +1,818 @@ +// This file contains building blocks to help construct a JSX namespace +export declare type HTMLElements = { + a: HTMLAnchorElement; + abbr: HTMLElement; + address: HTMLElement; + area: HTMLAreaElement; + article: HTMLElement; + aside: HTMLElement; + audio: HTMLAudioElement; + b: HTMLElement; + base: HTMLBaseElement; + bdi: HTMLElement; + bdo: HTMLElement; + big: HTMLElement; + blockquote: HTMLQuoteElement; + body: HTMLBodyElement; + br: HTMLBRElement; + button: HTMLButtonElement; + canvas: HTMLCanvasElement; + caption: HTMLTableCaptionElement; + cite: HTMLElement; + code: HTMLElement; + col: HTMLTableColElement; + colgroup: HTMLTableColElement; + data: HTMLDataElement; + datalist: HTMLDataListElement; + dd: HTMLElement; + del: HTMLModElement; + details: HTMLDetailsElement; + dfn: HTMLElement; + dialog: HTMLDialogElement; + div: HTMLDivElement; + dl: HTMLDListElement; + dt: HTMLElement; + em: HTMLElement; + embed: HTMLEmbedElement; + fieldset: HTMLFieldSetElement; + figcaption: HTMLElement; + figure: HTMLElement; + footer: HTMLElement; + form: HTMLFormElement; + h1: HTMLHeadingElement; + h2: HTMLHeadingElement; + h3: HTMLHeadingElement; + h4: HTMLHeadingElement; + h5: HTMLHeadingElement; + h6: HTMLHeadingElement; + head: HTMLHeadElement; + header: HTMLElement; + hgroup: HTMLElement; + hr: HTMLHRElement; + html: HTMLHtmlElement; + i: HTMLElement; + iframe: HTMLIFrameElement; + img: HTMLImageElement; + input: HTMLInputElement; + ins: HTMLModElement; + kbd: HTMLElement; + keygen: HTMLUnknownElement; + label: HTMLLabelElement; + legend: HTMLLegendElement; + li: HTMLLIElement; + link: HTMLLinkElement; + main: HTMLElement; + map: HTMLMapElement; + mark: HTMLElement; + menu: HTMLMenuElement; + menuitem: HTMLUnknownElement; + meta: HTMLMetaElement; + meter: HTMLMeterElement; + nav: HTMLElement; + noscript: HTMLElement; + object: HTMLObjectElement; + ol: HTMLOListElement; + optgroup: HTMLOptGroupElement; + option: HTMLOptionElement; + output: HTMLOutputElement; + p: HTMLParagraphElement; + param: HTMLParamElement; + picture: HTMLPictureElement; + pre: HTMLPreElement; + progress: HTMLProgressElement; + q: HTMLQuoteElement; + rp: HTMLElement; + rt: HTMLElement; + ruby: HTMLElement; + s: HTMLElement; + samp: HTMLElement; + script: HTMLScriptElement; + section: HTMLElement; + select: HTMLSelectElement; + slot: HTMLSlotElement; + small: HTMLElement; + source: HTMLSourceElement; + span: HTMLSpanElement; + strong: HTMLElement; + style: HTMLStyleElement; + sub: HTMLElement; + summary: HTMLElement; + sup: HTMLElement; + table: HTMLTableElement; + tbody: HTMLTableSectionElement; + td: HTMLTableCellElement; + textarea: HTMLTextAreaElement; + tfoot: HTMLTableSectionElement; + th: HTMLTableCellElement; + thead: HTMLTableSectionElement; + time: HTMLTimeElement; + title: HTMLTitleElement; + tr: HTMLTableRowElement; + track: HTMLTrackElement; + u: HTMLElement; + ul: HTMLUListElement; + var: HTMLElement; + video: HTMLVideoElement; + wbr: HTMLElement; +}; + +export declare type SVGElements = { + svg: SVGSVGElement; + animate: SVGAnimateElement; + animateTransform: SVGAnimateTransformElement; + circle: SVGCircleElement; + clipPath: SVGClipPathElement; + defs: SVGDefsElement; + desc: SVGDescElement; + ellipse: SVGEllipseElement; + feBlend: SVGFEBlendElement; + feColorMatrix: SVGFEColorMatrixElement; + feComponentTransfer: SVGFEComponentTransferElement; + feComposite: SVGFECompositeElement; + feConvolveMatrix: SVGFEConvolveMatrixElement; + feDiffuseLighting: SVGFEDiffuseLightingElement; + feDisplacementMap: SVGFEDisplacementMapElement; + feFlood: SVGFEFloodElement; + feGaussianBlur: SVGFEGaussianBlurElement; + feImage: SVGFEImageElement; + feMerge: SVGFEMergeElement; + feMergeNode: SVGFEMergeNodeElement; + feMorphology: SVGFEMorphologyElement; + feOffset: SVGFEOffsetElement; + feSpecularLighting: SVGFESpecularLightingElement; + feTile: SVGFETileElement; + feTurbulence: SVGFETurbulenceElement; + filter: SVGFilterElement; + foreignObject: SVGForeignObjectElement; + g: SVGGElement; + image: SVGImageElement; + line: SVGLineElement; + linearGradient: SVGLinearGradientElement; + marker: SVGMarkerElement; + mask: SVGMaskElement; + path: SVGPathElement; + pattern: SVGPatternElement; + polygon: SVGPolygonElement; + polyline: SVGPolylineElement; + radialGradient: SVGRadialGradientElement; + rect: SVGRectElement; + stop: SVGStopElement; + symbol: SVGSymbolElement; + text: SVGTextElement; + tspan: SVGTSpanElement; + use: SVGUseElement; +}; + +type TargetedEvent< + Target extends EventTarget = EventTarget, + TypedEvent extends Event = Event +> = Omit & { readonly currentTarget: Target }; + +type EventHandler = { (event: E): void }; + +type AnimationEventHandler = EventHandler< + TargetedEvent +>; +type ClipboardEventHandler = EventHandler< + TargetedEvent +>; +type CompositionEventHandler = EventHandler< + TargetedEvent +>; +type DragEventHandler = EventHandler< + TargetedEvent +>; +type FocusEventHandler = EventHandler< + TargetedEvent +>; +type GenericEventHandler = EventHandler< + TargetedEvent +>; +type KeyboardEventHandler = EventHandler< + TargetedEvent +>; +type MouseEventHandler = EventHandler< + TargetedEvent +>; +type PointerEventHandler = EventHandler< + TargetedEvent +>; +type TouchEventHandler = EventHandler< + TargetedEvent +>; +type TransitionEventHandler = EventHandler< + TargetedEvent +>; +type UIEventHandler = EventHandler< + TargetedEvent +>; +type WheelEventHandler = EventHandler< + TargetedEvent +>; + +// Receives an element as Target such as HTMLDivElement +export declare type GenericEventAttrs = { + // Image Events + onLoad?: GenericEventHandler; + onLoadCapture?: GenericEventHandler; + onError?: GenericEventHandler; + onErrorCapture?: GenericEventHandler; + + // Clipboard Events + onCopy?: ClipboardEventHandler; + onCopyCapture?: ClipboardEventHandler; + onCut?: ClipboardEventHandler; + onCutCapture?: ClipboardEventHandler; + onPaste?: ClipboardEventHandler; + onPasteCapture?: ClipboardEventHandler; + + // Composition Events + onCompositionEnd?: CompositionEventHandler; + onCompositionEndCapture?: CompositionEventHandler; + onCompositionStart?: CompositionEventHandler; + onCompositionStartCapture?: CompositionEventHandler; + onCompositionUpdate?: CompositionEventHandler; + onCompositionUpdateCapture?: CompositionEventHandler; + + // Details Events + onToggle?: GenericEventHandler; + + // Focus Events + onFocus?: FocusEventHandler; + onFocusCapture?: FocusEventHandler; + onBlur?: FocusEventHandler; + onBlurCapture?: FocusEventHandler; + + // Form Events + onChange?: GenericEventHandler; + onChangeCapture?: GenericEventHandler; + onInput?: GenericEventHandler; + onInputCapture?: GenericEventHandler; + onSearch?: GenericEventHandler; + onSearchCapture?: GenericEventHandler; + onSubmit?: GenericEventHandler; + onSubmitCapture?: GenericEventHandler; + onInvalid?: GenericEventHandler; + onInvalidCapture?: GenericEventHandler; + + // Keyboard Events + onKeyDown?: KeyboardEventHandler; + onKeyDownCapture?: KeyboardEventHandler; + onKeyPress?: KeyboardEventHandler; + onKeyPressCapture?: KeyboardEventHandler; + onKeyUp?: KeyboardEventHandler; + onKeyUpCapture?: KeyboardEventHandler; + + // Media Events + onAbort?: GenericEventHandler; + onAbortCapture?: GenericEventHandler; + onCanPlay?: GenericEventHandler; + onCanPlayCapture?: GenericEventHandler; + onCanPlayThrough?: GenericEventHandler; + onCanPlayThroughCapture?: GenericEventHandler; + onDurationChange?: GenericEventHandler; + onDurationChangeCapture?: GenericEventHandler; + onEmptied?: GenericEventHandler; + onEmptiedCapture?: GenericEventHandler; + onEncrypted?: GenericEventHandler; + onEncryptedCapture?: GenericEventHandler; + onEnded?: GenericEventHandler; + onEndedCapture?: GenericEventHandler; + onLoadedData?: GenericEventHandler; + onLoadedDataCapture?: GenericEventHandler; + onLoadedMetadata?: GenericEventHandler; + onLoadedMetadataCapture?: GenericEventHandler; + onLoadStart?: GenericEventHandler; + onLoadStartCapture?: GenericEventHandler; + onPause?: GenericEventHandler; + onPauseCapture?: GenericEventHandler; + onPlay?: GenericEventHandler; + onPlayCapture?: GenericEventHandler; + onPlaying?: GenericEventHandler; + onPlayingCapture?: GenericEventHandler; + onProgress?: GenericEventHandler; + onProgressCapture?: GenericEventHandler; + onRateChange?: GenericEventHandler; + onRateChangeCapture?: GenericEventHandler; + onSeeked?: GenericEventHandler; + onSeekedCapture?: GenericEventHandler; + onSeeking?: GenericEventHandler; + onSeekingCapture?: GenericEventHandler; + onStalled?: GenericEventHandler; + onStalledCapture?: GenericEventHandler; + onSuspend?: GenericEventHandler; + onSuspendCapture?: GenericEventHandler; + onTimeUpdate?: GenericEventHandler; + onTimeUpdateCapture?: GenericEventHandler; + onVolumeChange?: GenericEventHandler; + onVolumeChangeCapture?: GenericEventHandler; + onWaiting?: GenericEventHandler; + onWaitingCapture?: GenericEventHandler; + + // MouseEvents + onClick?: MouseEventHandler; + onClickCapture?: MouseEventHandler; + onContextMenu?: MouseEventHandler; + onContextMenuCapture?: MouseEventHandler; + onDblClick?: MouseEventHandler; + onDblClickCapture?: MouseEventHandler; + onDrag?: DragEventHandler; + onDragCapture?: DragEventHandler; + onDragEnd?: DragEventHandler; + onDragEndCapture?: DragEventHandler; + onDragEnter?: DragEventHandler; + onDragEnterCapture?: DragEventHandler; + onDragExit?: DragEventHandler; + onDragExitCapture?: DragEventHandler; + onDragLeave?: DragEventHandler; + onDragLeaveCapture?: DragEventHandler; + onDragOver?: DragEventHandler; + onDragOverCapture?: DragEventHandler; + onDragStart?: DragEventHandler; + onDragStartCapture?: DragEventHandler; + onDrop?: DragEventHandler; + onDropCapture?: DragEventHandler; + onMouseDown?: MouseEventHandler; + onMouseDownCapture?: MouseEventHandler; + onMouseEnter?: MouseEventHandler; + onMouseEnterCapture?: MouseEventHandler; + onMouseLeave?: MouseEventHandler; + onMouseLeaveCapture?: MouseEventHandler; + onMouseMove?: MouseEventHandler; + onMouseMoveCapture?: MouseEventHandler; + onMouseOut?: MouseEventHandler; + onMouseOutCapture?: MouseEventHandler; + onMouseOver?: MouseEventHandler; + onMouseOverCapture?: MouseEventHandler; + onMouseUp?: MouseEventHandler; + onMouseUpCapture?: MouseEventHandler; + + // Selection Events + onSelect?: GenericEventHandler; + onSelectCapture?: GenericEventHandler; + + // Touch Events + onTouchCancel?: TouchEventHandler; + onTouchCancelCapture?: TouchEventHandler; + onTouchEnd?: TouchEventHandler; + onTouchEndCapture?: TouchEventHandler; + onTouchMove?: TouchEventHandler; + onTouchMoveCapture?: TouchEventHandler; + onTouchStart?: TouchEventHandler; + onTouchStartCapture?: TouchEventHandler; + + // Pointer Events + onPointerOver?: PointerEventHandler; + onPointerOverCapture?: PointerEventHandler; + onPointerEnter?: PointerEventHandler; + onPointerEnterCapture?: PointerEventHandler; + onPointerDown?: PointerEventHandler; + onPointerDownCapture?: PointerEventHandler; + onPointerMove?: PointerEventHandler; + onPointerMoveCapture?: PointerEventHandler; + onPointerUp?: PointerEventHandler; + onPointerUpCapture?: PointerEventHandler; + onPointerCancel?: PointerEventHandler; + onPointerCancelCapture?: PointerEventHandler; + onPointerOut?: PointerEventHandler; + onPointerOutCapture?: PointerEventHandler; + onPointerLeave?: PointerEventHandler; + onPointerLeaveCapture?: PointerEventHandler; + onGotPointerCapture?: PointerEventHandler; + onGotPointerCaptureCapture?: PointerEventHandler; + onLostPointerCapture?: PointerEventHandler; + onLostPointerCaptureCapture?: PointerEventHandler; + + // UI Events + onScroll?: UIEventHandler; + onScrollCapture?: UIEventHandler; + + // Wheel Events + onWheel?: WheelEventHandler; + onWheelCapture?: WheelEventHandler; + + // Animation Events + onAnimationStart?: AnimationEventHandler; + onAnimationStartCapture?: AnimationEventHandler; + onAnimationEnd?: AnimationEventHandler; + onAnimationEndCapture?: AnimationEventHandler; + onAnimationIteration?: AnimationEventHandler; + onAnimationIterationCapture?: AnimationEventHandler; + + // Transition Events + onTransitionEnd?: TransitionEventHandler; + onTransitionEndCapture?: TransitionEventHandler; +}; + +// Note: HTML elements will also need GenericEventAttributes +export declare type HTMLAttrs = { + dangerouslySetInnerHTML?: { __html: string }; + ref?: (el: any) => void; + + // Standard HTML Attributes + accept?: string; + acceptCharset?: string; + accessKey?: string; + action?: string; + allowFullScreen?: boolean; + allowTransparency?: boolean; + alt?: string; + as?: string; + async?: boolean; + autocomplete?: string; + autoComplete?: string; + autocorrect?: string; + autoCorrect?: string; + autofocus?: boolean; + autoFocus?: boolean; + autoPlay?: boolean; + capture?: boolean; + cellPadding?: number | string; + cellSpacing?: number | string; + charSet?: string; + challenge?: string; + checked?: boolean; + class?: string; + // className?: string; + cols?: number; + colSpan?: number; + content?: string; + contentEditable?: boolean; + contextMenu?: string; + controls?: boolean; + controlsList?: string; + coords?: string; + crossOrigin?: string; + data?: string; + dateTime?: string; + default?: boolean; + defer?: boolean; + dir?: "auto" | "rtl" | "ltr"; + disabled?: boolean; + disableRemotePlayback?: boolean; + download?: unknown; + draggable?: boolean; + encType?: string; + form?: string; + formAction?: string; + formEncType?: string; + formMethod?: string; + formNoValidate?: boolean; + formTarget?: string; + frameBorder?: number | string; + headers?: string; + height?: number | string; + hidden?: boolean; + high?: number; + href?: string; + hrefLang?: string; + for?: string; + htmlFor?: string; + httpEquiv?: string; + icon?: string; + id?: string; + inputMode?: string; + integrity?: string; + is?: string; + keyParams?: string; + keyType?: string; + kind?: string; + label?: string; + lang?: string; + list?: string; + loop?: boolean; + low?: number; + manifest?: string; + marginHeight?: number; + marginWidth?: number; + max?: number | string; + maxLength?: number; + media?: string; + mediaGroup?: string; + method?: string; + min?: number | string; + minLength?: number; + multiple?: boolean; + muted?: boolean; + name?: string; + nonce?: string; + noValidate?: boolean; + open?: boolean; + optimum?: number; + pattern?: string; + placeholder?: string; + playsInline?: boolean; + poster?: string; + preload?: string; + radioGroup?: string; + readOnly?: boolean; + rel?: string; + required?: boolean; + role?: string; + rows?: number; + rowSpan?: number; + sandbox?: string; + scope?: string; + scoped?: boolean; + scrolling?: string; + seamless?: boolean; + selected?: boolean; + shape?: string; + size?: number; + sizes?: string; + slot?: string; + span?: number; + spellcheck?: boolean; + src?: string; + srcset?: string; + srcDoc?: string; + srcLang?: string; + srcSet?: string; + start?: number; + step?: number | string; + style?: string | { [key: string]: string | number }; + summary?: string; + tabIndex?: number; + target?: string; + title?: string; + type?: string; + useMap?: string; + value?: string | string[] | number; + volume?: string | number; + width?: number | string; + wmode?: string; + wrap?: string; + + // RDFa Attributes + about?: string; + datatype?: string; + inlist?: unknown; + prefix?: string; + property?: string; + resource?: string; + typeof?: string; + vocab?: string; + + // Microdata Attributes + itemProp?: string; + itemScope?: boolean; + itemType?: string; + itemID?: string; + itemRef?: string; +}; + +// Note: SVG elements will also need HTMLAttributes and GenericEventAttributes +export declare type SVGAttrs = { + accentHeight?: number | string; + accumulate?: "none" | "sum"; + additive?: "replace" | "sum"; + alignmentBaseline?: + | "auto" + | "baseline" + | "before-edge" + | "text-before-edge" + | "middle" + | "central" + | "after-edge" + | "text-after-edge" + | "ideographic" + | "alphabetic" + | "hanging" + | "mathematical" + | "inherit"; + allowReorder?: "no" | "yes"; + alphabetic?: number | string; + amplitude?: number | string; + arabicForm?: "initial" | "medial" | "terminal" | "isolated"; + ascent?: number | string; + attributeName?: string; + attributeType?: string; + autoReverse?: number | string; + azimuth?: number | string; + baseFrequency?: number | string; + baselineShift?: number | string; + baseProfile?: number | string; + bbox?: number | string; + begin?: number | string; + bias?: number | string; + by?: number | string; + calcMode?: number | string; + capHeight?: number | string; + clip?: number | string; + clipPath?: string; + clipPathUnits?: number | string; + clipRule?: number | string; + colorInterpolation?: number | string; + colorInterpolationFilters?: "auto" | "sRGB" | "linearRGB" | "inherit"; + colorProfile?: number | string; + colorRendering?: number | string; + contentScriptType?: number | string; + contentStyleType?: number | string; + cursor?: number | string; + cx?: number | string; + cy?: number | string; + d?: string; + decelerate?: number | string; + descent?: number | string; + diffuseConstant?: number | string; + direction?: number | string; + display?: number | string; + divisor?: number | string; + dominantBaseline?: number | string; + dur?: number | string; + dx?: number | string; + dy?: number | string; + edgeMode?: number | string; + elevation?: number | string; + enableBackground?: number | string; + end?: number | string; + exponent?: number | string; + externalResourcesRequired?: number | string; + fill?: string; + fillOpacity?: number | string; + fillRule?: "nonzero" | "evenodd" | "inherit"; + filter?: string; + filterRes?: number | string; + filterUnits?: number | string; + floodColor?: number | string; + floodOpacity?: number | string; + focusable?: number | string; + fontFamily?: string; + fontSize?: number | string; + fontSizeAdjust?: number | string; + fontStretch?: number | string; + fontStyle?: number | string; + fontVariant?: number | string; + fontWeight?: number | string; + format?: number | string; + from?: number | string; + fx?: number | string; + fy?: number | string; + g1?: number | string; + g2?: number | string; + glyphName?: number | string; + glyphOrientationHorizontal?: number | string; + glyphOrientationVertical?: number | string; + glyphRef?: number | string; + gradientTransform?: string; + gradientUnits?: string; + hanging?: number | string; + horizAdvX?: number | string; + horizOriginX?: number | string; + ideographic?: number | string; + imageRendering?: number | string; + in2?: number | string; + in?: string; + intercept?: number | string; + k1?: number | string; + k2?: number | string; + k3?: number | string; + k4?: number | string; + k?: number | string; + kernelMatrix?: number | string; + kernelUnitLength?: number | string; + kerning?: number | string; + keyPoints?: number | string; + keySplines?: number | string; + keyTimes?: number | string; + lengthAdjust?: number | string; + letterSpacing?: number | string; + lightingColor?: number | string; + limitingConeAngle?: number | string; + local?: number | string; + markerEnd?: string; + markerHeight?: number | string; + markerMid?: string; + markerStart?: string; + markerUnits?: number | string; + markerWidth?: number | string; + mask?: string; + maskContentUnits?: number | string; + maskUnits?: number | string; + mathematical?: number | string; + mode?: number | string; + numOctaves?: number | string; + offset?: number | string; + opacity?: number | string; + operator?: number | string; + order?: number | string; + orient?: number | string; + orientation?: number | string; + origin?: number | string; + overflow?: number | string; + overlinePosition?: number | string; + overlineThickness?: number | string; + paintOrder?: number | string; + panose1?: number | string; + pathLength?: number | string; + patternContentUnits?: string; + patternTransform?: number | string; + patternUnits?: string; + pointerEvents?: number | string; + points?: string; + pointsAtX?: number | string; + pointsAtY?: number | string; + pointsAtZ?: number | string; + preserveAlpha?: number | string; + preserveAspectRatio?: string; + primitiveUnits?: number | string; + r?: number | string; + radius?: number | string; + refX?: number | string; + refY?: number | string; + renderingIntent?: number | string; + repeatCount?: number | string; + repeatDur?: number | string; + requiredExtensions?: number | string; + requiredFeatures?: number | string; + restart?: number | string; + result?: string; + rotate?: number | string; + rx?: number | string; + ry?: number | string; + scale?: number | string; + seed?: number | string; + shapeRendering?: number | string; + slope?: number | string; + spacing?: number | string; + specularConstant?: number | string; + specularExponent?: number | string; + speed?: number | string; + spreadMethod?: string; + startOffset?: number | string; + stdDeviation?: number | string; + stemh?: number | string; + stemv?: number | string; + stitchTiles?: number | string; + stopColor?: string; + stopOpacity?: number | string; + strikethroughPosition?: number | string; + strikethroughThickness?: number | string; + string?: number | string; + stroke?: string; + strokeDasharray?: string | number; + strokeDashoffset?: string | number; + strokeLinecap?: "butt" | "round" | "square" | "inherit"; + strokeLinejoin?: "miter" | "round" | "bevel" | "inherit"; + strokeMiterlimit?: string; + strokeOpacity?: number | string; + strokeWidth?: number | string; + surfaceScale?: number | string; + systemLanguage?: number | string; + tableValues?: number | string; + targetX?: number | string; + targetY?: number | string; + textAnchor?: string; + textDecoration?: number | string; + textLength?: number | string; + textRendering?: number | string; + to?: number | string; + transform?: string; + u1?: number | string; + u2?: number | string; + underlinePosition?: number | string; + underlineThickness?: number | string; + unicode?: number | string; + unicodeBidi?: number | string; + unicodeRange?: number | string; + unitsPerEm?: number | string; + vAlphabetic?: number | string; + values?: string; + vectorEffect?: number | string; + version?: string; + vertAdvY?: number | string; + vertOriginX?: number | string; + vertOriginY?: number | string; + vHanging?: number | string; + vIdeographic?: number | string; + viewBox?: string; + viewTarget?: number | string; + visibility?: number | string; + vMathematical?: number | string; + widths?: number | string; + wordSpacing?: number | string; + writingMode?: number | string; + x1?: number | string; + x2?: number | string; + x?: number | string; + xChannelSelector?: string; + xHeight?: number | string; + xlinkActuate?: string; + xlinkArcrole?: string; + xlinkHref?: string; + xlinkRole?: string; + xlinkShow?: string; + xlinkTitle?: string; + xlinkType?: string; + xmlBase?: string; + xmlLang?: string; + xmlns?: string; + xmlnsXlink?: string; + xmlSpace?: string; + y1?: number | string; + y2?: number | string; + y?: number | string; + yChannelSelector?: string; + z?: number | string; + zoomAndPan?: string; +}; diff --git a/src/dom/resolver.ts b/src/dom/resolver.ts new file mode 100644 index 0000000..7b1d7b2 --- /dev/null +++ b/src/dom/resolver.ts @@ -0,0 +1,21 @@ +import { + ComponentUtils, + VElement, + RenderContext, + ComponentTreeStep, +} from "./types"; +import { getUtils } from "./api"; + +export const resolveComponent = ( + renderContext: RenderContext, + parentStep: ComponentTreeStep +) => { + const { node, parent } = parentStep; + const utils: Omit = getUtils( + renderContext, + parentStep + ); + const utilsCtx: ComponentUtils = { ...utils, utils: utils as ComponentUtils }; + const el = node.t.call(utilsCtx, node.p, utilsCtx) as VElement; + return el; +}; diff --git a/src/dom/traverser.ts b/src/dom/traverser.ts new file mode 100644 index 0000000..830409f --- /dev/null +++ b/src/dom/traverser.ts @@ -0,0 +1,176 @@ +import { + VElement, + ComponentVElement, + NativeVElement, + NativeTreeStep, + PrimitiveTreeStep, + WireTreeStep, + TreeStep, + RenderContext, + ComponentTreeStep, +} from "./types"; +import * as DOMConstants from "./constants"; +import { createError, getVirtualElementId, checkIfSVG } from "./utils"; +import { createDOMNode } from "./dom"; +import { resolveComponent } from "./resolver"; + +import * as Constants from "../core/constants"; +import { crawl } from "../utils/crawl"; + +export const reifyTree = ( + renderContext: RenderContext, + el: VElement, + parent?: TreeStep, + afterIndex?: number +) => { + const root = getTreeStep( + parent, + undefined, + el, + Number.isFinite(afterIndex) ? (afterIndex as number) + 1 : 0 + ) as TreeStep; + const registry: TreeStep[] = []; + + // traverse bottom-up(post-order) and assemble dom tree + crawl( + root, + function (step) { + registry.push(step); + // step.parent.children.push shouldnt be done here for root.parent at least + // reason more about it: maybe reifyTree shouldn't take parent as prop? + if (step !== root && step.parent) step.parent.children.push(step); + const isSVG = checkIfSVG(step); + const dom = createDOMNode(step, isSVG); + if (dom) { + step.dom = dom as HTMLElement; + const children = registry.filter((el) => el.parent === step); + if (children.length > 0) { + const kids = children + .filter((el) => el.dom) + .map((el: TreeStep) => el.dom as Node) + .flat(); + if ((step as ComponentTreeStep).mount) { + //console.log("s", step); + } + // this enables support for portals + ( + (step as ComponentTreeStep).mount || (dom as unknown as Element) + ).append(...kids); + if (step.type == DOMConstants.ComponentTreeStep) { + if (step.onMount) step.onMount.forEach((el) => el()); + } + } + } + }, + { + order: "post", + kids: getChildrenFromStep.bind(null, renderContext), + } + ); + return { registry, root }; +}; + +const getComponentChildrenFromStep = ( + renderContext: RenderContext, + parentStep: ComponentTreeStep +): TreeStep[] => { + const el = resolveComponent(renderContext, parentStep); + const r = (Array.isArray(el) ? el : [el]) + .map((item, i) => getTreeStep(parentStep, undefined, item, i) as TreeStep) + .flat(); + return r; +}; + +const getPlainNodeChildrenFromStep = ( + renderContext: RenderContext, + parentStep: NativeTreeStep +): TreeStep[] => { + const { node, parent } = parentStep; + return node + ? (((node as NativeVElement)?.p?.children || []) as NativeVElement[]) + .map( + (item, i) => getTreeStep(parentStep, undefined, item, i) as TreeStep + ) + .flat() + : []; +}; + +const getChildrenFromStep = ( + renderContext: RenderContext, + parentStep: TreeStep +): TreeStep[] => { + if (!parentStep) return []; + const { node, parent } = parentStep; + + if (node && typeof node === "object") { + if (Array.isArray(node)) { + // todo fix this + return []; + } + if (node.type === DOMConstants.NATIVE) { + // todo: figure how to remove this typecast + return getPlainNodeChildrenFromStep(renderContext, parentStep as any); + } + if (node.type == DOMConstants.COMPONENT) { + // todo: figure how to remove this typecast + return getComponentChildrenFromStep(renderContext, parentStep as any); + } + } + return []; +}; + +export const getTreeStep = ( + parentStep: TreeStep | undefined, + meta: Record | undefined, + el: VElement, + index?: number +): TreeStep | TreeStep[] => { + if (Array.isArray(el)) { + return (el as VElement[]).map( + (el, i) => getTreeStep(parentStep, undefined, el, i) as TreeStep + ); + } else { + const step: Partial = { + id: getVirtualElementId(el, index), + node: el, + meta, + parent: parentStep, + children: [], + }; + if ( + el === null || + el === undefined || + typeof el == "string" || + typeof el === "number" || + typeof el === "boolean" + ) { + return { + type: DOMConstants.PrimitiveTreeStep, + ...step, + } as PrimitiveTreeStep; + } else if (el.type === DOMConstants.NATIVE) { + return { + type: DOMConstants.NativeTreeStep, + ...step, + } as NativeTreeStep; + // why this not DOMConstants + } else if (el.type === Constants.WIRE) { + return { + type: DOMConstants.WireTreeStep, + ...step, + } as WireTreeStep; + } else if (el.type === DOMConstants.COMPONENT) { + return { + type: DOMConstants.ComponentTreeStep, + ...step, + state: { signals: {}, ctx: new WeakMap(), stores: {} }, + wires: [], + onMount: [], + onUnmount: [], + } as ComponentTreeStep; + } else { + console.error(el); + throw createError(110); + } + } +}; diff --git a/src/dom/types.ts b/src/dom/types.ts new file mode 100644 index 0000000..503ed48 --- /dev/null +++ b/src/dom/types.ts @@ -0,0 +1,151 @@ +import { Observable, Change, ChangeType } from "@gullerya/object-observer"; +import { Signal, Wire, StoreCursor, WireFunction } from "../core/state"; + +import * as DOMConstants from "./constants"; +export type DOMNode = HTMLElement | DocumentFragment; + +export type PrimitiveType = string | number | boolean | null | undefined; + +// https://stackoverflow.com/a/50924506 +type ExtractGenericFromContext = Type extends Context + ? X + : never; +export interface ComponentUtils { + renderContext: RenderContext; + signal(name: string, value: T): Signal; + store(name: string, value: T): StoreCursor; + wire(arg: WireFunction): Wire; + // todo: fix this typescript definition + setContext(ctx: T, value: ExtractGenericFromContext): void; + getContext(ctx: T): ExtractGenericFromContext; + onUnmount(cb: Function): void; + onMount(cb: Function): void; + utils: ComponentUtils; + step: TreeStep; + api: { + insert: Function; + remove: Function; + update: Function; + }; +} + +export type Component = { + (this: ComponentUtils, props: T, helpers: ComponentUtils): VElement; +} & { __name__: string }; + +export interface VElementBase { + id: string; +} + +// Virtual Elements + +// NativeVElement +export interface NativeVElement extends VElementBase { + type: typeof DOMConstants.NATIVE; + t: string; + p: Record & { + children: VElement[]; + }; +} + +// ComponentVElement +export interface ComponentVElement extends VElementBase { + type: typeof DOMConstants.COMPONENT; + t: Component; + p: Record & { + children: VElement[]; + }; +} + +export type VElement = + | NativeVElement + // | NativeVElement[] + | ComponentVElement + // | ComponentVElement[] + | Wire + // else jsx throws + | PrimitiveType; + +// Tree steps +export type BaseTreeStep = { + dom?: DOMNode; + id?: string; + parent?: TreeStep; + meta?: Record; + children: Array; +}; + +export interface NativeTreeStep extends BaseTreeStep { + type: typeof DOMConstants.NativeTreeStep; + node: NativeVElement; +} + +export interface ComponentTreeStep extends BaseTreeStep { + type: typeof DOMConstants.ComponentTreeStep; + node: ComponentVElement; + wires: Wire[]; + mount?: HTMLElement; + state: ComponentTreeStepState; + onUnmount: Function[]; + onMount: Function[]; +} +export interface ComponentTreeStepState { + signals: { + [name: string]: Signal; + }; + stores: { + [name: string]: StoreCursor; + }; + ctx: WeakMap; +} + +export interface PrimitiveTreeStep extends BaseTreeStep { + type: typeof DOMConstants.PrimitiveTreeStep; + node: PrimitiveType; +} + +export interface WireTreeStep extends BaseTreeStep { + type: typeof DOMConstants.WireTreeStep; + node: VElement; +} + +export type TreeStep = + | NativeTreeStep + | ComponentTreeStep + | PrimitiveTreeStep + | WireTreeStep; + +// mainly used by HMR & devtools +export interface RenderContext { + id: string; + el: Element; + prevState: Map; + reg: Set; + emitter: EEmitter; +} + +// Context like you'll find in react/solid +export type Context = { + sym: symbol; +}; + +export class EEmitter { + t: EventTarget; + constructor() { + this.t = new EventTarget(); + } + on(eventName: string, listener: EventListenerOrEventListenerObject | null) { + return this.t.addEventListener(eventName, listener); + } + once(eventName: string, listener: EventListenerOrEventListenerObject | null) { + return this.t.addEventListener(eventName, listener, { once: true }); + } + off(eventName: string, listener: EventListenerOrEventListenerObject | null) { + return this.t.removeEventListener(eventName, listener); + } + emit(eventName: string, detail: any) { + return this.t.dispatchEvent( + new CustomEvent(eventName, { detail, cancelable: true }), + ); + } +} diff --git a/src/dom/utils.ts b/src/dom/utils.ts new file mode 100644 index 0000000..946163c --- /dev/null +++ b/src/dom/utils.ts @@ -0,0 +1,143 @@ +import * as Constants from "../core/constants"; +import { + BaseTreeStep, + Context, + Component, + VElement, + TreeStep, + ComponentTreeStepState, + NativeVElement, + ComponentVElement, + RenderContext, +} from "./types"; +import { crawl } from "../utils/crawl"; +import * as DOMConstants from "./constants"; + +export const getDescendants = (node: TreeStep): TreeStep[] => { + const nodes: TreeStep[] = []; + crawl( + node, + (node) => { + nodes.push(node); + }, + { + kids: (parent) => { + if (!parent.children) { + console.log(parent); + } + return [...(Array.isArray(parent.children) ? parent.children : [])]; + }, + order: "post", + } + ); + return nodes; +}; + +// note: optimised to eagerly return using for for/while loops +// todo: use Array.some instead of for/while loops +export const getTreeStepRenderContextState = ( + renderContext: RenderContext, + step: TreeStep +) => { + const findMatchingResult = ( + previousState: Map, + baseTreeStep: TreeStep + ): ComponentTreeStepState | undefined => { + for (const key of previousState.keys()) { + let currentStep: BaseTreeStep | undefined = baseTreeStep, + index = 0, + match = true; + + while (currentStep && index < key.length) { + if (key[index] !== currentStep.id) { + match = false; + break; + } + currentStep = currentStep.parent; + index++; + } + + if (match && index === key.length && !currentStep) { + return previousState.get(key); + } + } + }; + + const match = findMatchingResult(renderContext.prevState, step); + return match; +}; + +export const getContextProvider = ( + ctxName: Context, + node: TreeStep +): TreeStep | null => { + let ancestor = node.parent; + while (ancestor) { + if ( + ancestor.type === DOMConstants.ComponentTreeStep && + ancestor.state.ctx && + ancestor.state.ctx.get(ctxName) + ) { + return ancestor; + } + ancestor = ancestor.parent; + } + return null; +}; + +export const checkIfSVG = (step: TreeStep) => { + let isSVG = false; + let iterNode = step; + while (iterNode.parent) { + if ( + iterNode.node && + typeof iterNode.node === "object" && + iterNode.node.type === DOMConstants.NATIVE + ) { + if (iterNode.node.t === "svg") { + isSVG = true; + break; + } + } + iterNode = iterNode.parent; + } + return isSVG; +}; + +export const getVirtualElementId = ( + el: VElement, + i?: number +): string | undefined => { + const getElKey = (el: NativeVElement | ComponentVElement) => + el.p.key ? "/" + el.p.key : ""; + if ( + el === null || + el === undefined || + typeof el == "string" || + typeof el === "number" || + typeof el === "boolean" + ) { + return Number.isFinite(i) ? i + "" : undefined; + } else if (el.type == DOMConstants.NATIVE) { + return el.t + getElKey(el); + } else if (el.type === DOMConstants.COMPONENT) { + return (el.t as Component).__name__ + getElKey(el); + } else { + return i !== null && Number.isFinite(i) ? i + "" : undefined; + } +}; + +export const arrayRemove = (array: T[], ...items: T[]): void => { + items.forEach((item) => { + const index = array.indexOf(item); + if (index !== -1) { + array.splice(index, 1); + } + }); +}; + +export function createError(code: string | number, desc?: string) { + return new Error( + `Error ${code}: https://github.com/abhishiv/rocky7/wiki/Error-codes#code-${code}` + ); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..c827502 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,45 @@ +import "./object-observer.d.ts"; +export * from "./dom"; +export * from "./core/state"; +export * from "./stdlib/index"; + +import { Signal } from "./core"; +import { ComponentUtils } from "./dom"; + +export type UsePromiseResp = { + $data: Signal; + $error: Signal; + $loading: Signal; + $status: Signal<"loading" | "loaded" | "failed">; +}; + +export function usePromise( + queryName: string, + utils: ComponentUtils, + promise: () => Promise +): UsePromiseResp { + const $loading = utils.signal( + queryName + "/loading", + true + ); + const $data = utils.signal(queryName + "/data", null); + const $error = utils.signal(queryName + "/error", true); + const $status = utils.signal<"loading" | "loaded" | "failed">( + queryName + "/status", + "loading" + ); + promise() + .then((data) => { + $data(data); + $status("loaded"); + $loading(false); + }) + .catch((err) => { + console.log(err, err.message); + $data(undefined); + $error(err.message); + $status("failed"); + $loading(undefined); + }); + return { $data, $loading, $error, $status }; +} diff --git a/src/object-observer.d.ts b/src/object-observer.d.ts new file mode 100644 index 0000000..e260bb7 --- /dev/null +++ b/src/object-observer.d.ts @@ -0,0 +1,7 @@ +declare module "@gullerya/object-observer" { + export { + Observable, + Change, + ChangeType, + } from "@gullerya/object-observer/dist/object-observer.d.ts"; +} diff --git a/src/stdlib/Each/index.tsx b/src/stdlib/Each/index.tsx new file mode 100644 index 0000000..e2a7b00 --- /dev/null +++ b/src/stdlib/Each/index.tsx @@ -0,0 +1,125 @@ +/** @jsx h **/ + +import { + SubToken, + StoreCursor, + StoreManager, + StoreChange, +} from "../../core/state"; +import { h, component, Fragment } from "../../dom/index"; +import { ComponentUtils, VElement } from "../../dom/types"; +import { ParentWireContext } from "../../dom/index"; +import { META_FLAG, ObjPathProxy } from "../../utils/observer"; +import { TreeStep } from "../../dom/types"; +import { getUtils, addNode, removeNode } from "../../dom/api"; +import { reifyTree, getTreeStep } from "../../dom/traverser"; +import { getProxyMeta, getProxyPath } from "../../utils/observer"; +import { getValueUsingPath } from "../../utils/index"; + +type ArrayOrObject = Array | { [key: string]: unknown }; + +type ExtractElement = + ArrayType extends readonly (infer ElementType)[] + ? ElementType + : ArrayType extends { [key: string]: infer ElementType2 } + ? ElementType2 + : never; + +export const Each: ( + props: { + cursor: StoreCursor; + renderItem: ( + item: StoreCursor>, + index: number + ) => VElement; + }, + utils: ComponentUtils +) => VElement = component( + "Each", + ( + props, + { + wire, + setContext, + signal, + utils, + step: parentStep, + renderContext, + onMount, + onUnmount, + } + ) => { + // todo: important memory leak + const $rootWire = wire(($: SubToken) => {}); + setContext(ParentWireContext, signal("$wire", $rootWire)); + + const cursor = props.cursor; + const store: StoreManager = (cursor as any)[META_FLAG]; + const path: string[] = getProxyPath(cursor); + console.log("path,", path); + + const value: any[] = getValueUsingPath(store.value as any, path) as any[]; + + console.log("sss", store); + + const observor = function ({ data, path }: StoreChange) { + //console.debug("change", changes, path); + + console.log("list change", data, path, value); + if (data.name == "push") { + const index = data.result as number; // push returns index + const { treeStep, el } = renderArray( + parentStep, + props.renderItem, + cursor, + value, + index + ); + const previousChildren = [...parentStep.children]; + const { registry, root } = reifyTree(renderContext, el, parentStep); + addNode(renderContext, parentStep, root); + } else if (data.name === "pop") { + } else if (data.name === "splice") { + } + }; + const task = { path, observor }; + onMount(() => { + store.tasks.add(task); + }); + onUnmount(() => { + store.tasks.delete(task); + }); + + if (Array.isArray(value)) { + // array + return ( + + {value.map((el, index) => + props.renderItem((cursor as any)[index], index) + )} + + ); + } else { + // object + return ( + + {Object.keys(value).map((el, index) => + props.renderItem((cursor as any)[el], el as any) + )} + + ); + } + } +); + +const renderArray = ( + parentStep: TreeStep, + renderItem: Function, + cursor: any, + list: any[], + index: number | string +) => { + const vEl = renderItem((cursor as any)[index], index); + const treeStep = getTreeStep(parentStep, undefined, vEl); + return { treeStep, el: vEl }; +}; diff --git a/src/stdlib/Portal/index.tsx b/src/stdlib/Portal/index.tsx new file mode 100644 index 0000000..daea670 --- /dev/null +++ b/src/stdlib/Portal/index.tsx @@ -0,0 +1,29 @@ +/** @jsx h **/ + +import { h, component } from "../../dom"; +import { ComponentTreeStep, VElement } from "../../dom/types"; + +export type PortalProps = { + mount: HTMLElement; + children: VElement; +}; + +export const Portal = component( + "Portal", + ( + props, + { + wire, + setContext, + signal, + utils, + onUnmount, + onMount, + step: parentStep, + renderContext, + } + ) => { + //(parentStep as ComponentTreeStep).mount = props.mount; + return props.children; + } +); diff --git a/src/stdlib/When/index.tsx b/src/stdlib/When/index.tsx new file mode 100644 index 0000000..b312eaf --- /dev/null +++ b/src/stdlib/When/index.tsx @@ -0,0 +1,58 @@ +/** @jsx h **/ + +import { SubToken } from "../../core/state"; +import { h, component } from "../../dom"; +import { VElement } from "../../dom/types"; +import { ParentWireContext } from "../../dom/index"; +import { addNode, removeNode } from "../../dom/api"; +import { reifyTree } from "../../dom/traverser"; + +export type WhenViews = { [key: string]: () => VElement }; +export type WhenProps = { + condition: ($: SubToken) => keyof WhenViews | boolean; + views: WhenViews; + fallback?: () => VElement; + options?: { cached?: boolean }; + key?: string; +}; + +export const When = component( + "When", + ( + props, + { + wire, + setContext, + signal, + utils, + onUnmount, + onMount, + step: parentStep, + renderContext, + } + ) => { + // todo: important memory leak + const rootWire = wire(($: SubToken) => {}); + setContext(ParentWireContext, signal("$wire", rootWire)); + const underlying = utils.wire(props.condition); + const value = underlying(); + const getView = (value: any) => + props.views[value as unknown as any] || props.fallback; + const task = (value: any) => { + const view = getView(value); + const u = view ? view() : null; + const previousChildren = [...parentStep.children]; + const { registry, root } = reifyTree(renderContext, u, parentStep); + addNode(renderContext, parentStep, root); + previousChildren.forEach((n) => removeNode(renderContext, n)); + }; + onMount(() => { + underlying.tasks.add(task); + }); + onUnmount(() => { + underlying.tasks.delete(task); + }); + const view = getView(value); + return view ? view() : null; + } +); diff --git a/src/stdlib/index.tsx b/src/stdlib/index.tsx new file mode 100644 index 0000000..5731165 --- /dev/null +++ b/src/stdlib/index.tsx @@ -0,0 +1,3 @@ +export * from "./Each"; +export * from "./Portal"; +export * from "./When"; diff --git a/src/utils/crawl.ts b/src/utils/crawl.ts new file mode 100644 index 0000000..bcdba18 --- /dev/null +++ b/src/utils/crawl.ts @@ -0,0 +1,45 @@ +// Crawl the tree in post-order. +export const crawl = ( + root: T, + iterate: (node: T) => void, + options: { order: "post"; kids: (node: T) => T[] } +) => { + if (options.order === "post") { + dfsPostOrder(root, iterate, options.kids); + } +}; + +// Helper function for dfsPostOrder. +const dfsPostOrder = ( + root: T, + iterate: (node: T) => void, + kids: (node: T) => T[] +) => { + // The stack holds the nodes we need to visit. + const stack: { node: T; visitedChildren: boolean }[] = []; + // Keep track of the nodes we've visited. + const visited = new Set(); + + // Push the root node onto the stack to start the traversal. + stack.push({ node: root, visitedChildren: false }); + + while (stack.length > 0) { + const { node, visitedChildren } = stack[stack.length - 1]; + // If all the node's children have been visited, visit the node and pop it from the stack. + if (visitedChildren) { + iterate(node); + stack.pop(); + } else { + visited.add(node); + // Mark that we've visited the node's children. + stack[stack.length - 1].visitedChildren = true; + // Otherwise, push all the unvisited children onto the stack. + const children = (kids(node) || []).reverse(); + children.forEach((child) => { + if (!visited.has(child)) { + stack.push({ node: child, visitedChildren: false }); + } + }); + } + } +}; \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..2cf4d58 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,4 @@ +export const getValueUsingPath = ( + record: Record, + path: string[] +) => path.reduce((record, item) => record[item], record); diff --git a/src/utils/observer.ts b/src/utils/observer.ts new file mode 100644 index 0000000..b72e267 --- /dev/null +++ b/src/utils/observer.ts @@ -0,0 +1,40 @@ +import { + createProxy, + getPath, + ObjPathProxy, + PATH_FLAG, +} from "./ts-object-path"; + +export { PATH_FLAG } from "./ts-object-path"; +export type { ObjPathProxy } from "./ts-object-path"; + +export const CONTROL_FLAG = Symbol("CONTROL_FLAG"); +export const META_FLAG = Symbol("META_FLAG"); + +export type StoreProxy = ObjPathProxy; + +export type StoreProxyInternal = StoreProxy & { + [PATH_FLAG]: string[]; + [META_FLAG]: V; +}; + +export const wrapWithProxy = ( + obj: T, + meta: any +): StoreProxy => { + const p = createProxy([], { + [META_FLAG]: meta, + [CONTROL_FLAG]: (obj: any) => getPath(obj), + }); + return p as StoreProxy; +}; + +export const isProxy = (proxy: unknown) => + proxy && !!(proxy as StoreProxyInternal)[PATH_FLAG]; + +export const getProxyPath = (cursor: StoreProxy) => [ + ...(cursor as StoreProxyInternal)[PATH_FLAG], +]; + +export const getProxyMeta = (cursor: StoreProxy) => + (cursor as StoreProxyInternal)[META_FLAG] as T; diff --git a/src/utils/ts-object-path/index.ts b/src/utils/ts-object-path/index.ts new file mode 100644 index 0000000..acae4ec --- /dev/null +++ b/src/utils/ts-object-path/index.ts @@ -0,0 +1,82 @@ +export type ObjPathProxy = { + [P in keyof T]: ObjPathProxy; +}; + +export type ObjProxyArg = + | ObjPathProxy + | ((p: ObjPathProxy) => ObjPathProxy); + +export const PATH_FLAG = Symbol("Object path"); + +export function createProxy( + path: PropertyKey[] = [], + base: any = {} +): ObjPathProxy { + const proxy = new Proxy( + // added to ts-object-path + { [PATH_FLAG]: path, ...base }, + { + get(target, key) { + // added to ts-object-path + if (base[key]) { + return base[key]; + } + if (key === PATH_FLAG) { + return target[PATH_FLAG]; + } + if (typeof key === "string") { + const intKey = parseInt(key, 10); + if (key === intKey.toString()) { + key = intKey as any; + } + } + return createProxy([...(path || []), key], base); + }, + } + ); + return proxy as any as ObjPathProxy; +} + +export function getPath(proxy: ObjProxyArg): PropertyKey[] { + if (typeof proxy === "function") { + proxy = proxy(createProxy()); + } + return (proxy as any)[PATH_FLAG]; +} + +export function isProxy(value: any): value is ObjPathProxy { + return ( + value && + typeof value === "object" && + !!getPath(value as ObjPathProxy) + ); +} + +export function get( + object: TRoot, + proxy: ObjProxyArg, + defaultValue: T | null | undefined = undefined +) { + return getPath(proxy).reduce( + (o, key) => o && valueOrElseDefault(o[key], defaultValue), + object as any + ) as T; +} + +export function set( + object: TRoot, + proxy: ObjProxyArg, + value: T +): void { + getPath(proxy).reduce((o: any, key, index, keys) => { + if (index < keys.length - 1) { + o[key] = o[key] || (typeof keys[index + 1] === "number" ? [] : {}); + return o[key]; + } + o[key] = value; + }, object); +} + +export function valueOrElseDefault(value: T, defaultValue: T): T { + return value !== null && value !== undefined ? value : defaultValue; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d2db4b4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "esnext", + "useDefineForClassFields": true, + "module": "ES2020", + "declaration": true, + "moduleResolution": "Node", + "strict": true, + "jsx": "react", + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "lib": ["ESNext", "DOM"], + "skipLibCheck": true, + "allowJs": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] +}