diff --git a/README.md b/README.md index 962a691..7d60b93 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ All the non-core functions are just examples of how you can compose functions to - **Scalable**: Naturally scale large state into multiple modules and files without performance degradation. - **Middlewares**: Simple and type-safe middleware composition interface. - **Tiny**: About [0.3KB](https://bundlephobia.com/package/stalo) Minified + Gzipped. -- **Devtools**: Native devtools support. +- **Devtools**: Native [devtools](https://github.com/ysmood/stalo/issues/3) support. ## Documentation diff --git a/chrome-extension/README.md b/chrome-extension/README.md index 7585803..c9bfef7 100644 --- a/chrome-extension/README.md +++ b/chrome-extension/README.md @@ -5,3 +5,5 @@ To build the chrome extension, under the root of the repo: ```bash npm run build-extension ``` + +The extension will output to [here](./dist). diff --git a/chrome-extension/manifest.ts b/chrome-extension/manifest.ts index d91bbae..8af4833 100644 --- a/chrome-extension/manifest.ts +++ b/chrome-extension/manifest.ts @@ -52,12 +52,14 @@ function genManifest() { ); console.info("Generated chrome extension html entrypoint"); - execSync(`npx rollup -c rollup.config.js`, { - stdio: "inherit", - env: { - ...process.env, - ENTRIES: [communicator, background, contentScript].join(","), - }, + [communicator, background, contentScript].forEach((entry) => { + execSync(`npx vite -c vite.ext.config.ts build`, { + stdio: "inherit", + env: { + ...process.env, + ENTRY: entry, + }, + }); }); writeFileSync("dist/icon.png", readFileSync("src/icon.png")); diff --git a/chrome-extension/rollup.config.js b/chrome-extension/rollup.config.js deleted file mode 100644 index e1013d5..0000000 --- a/chrome-extension/rollup.config.js +++ /dev/null @@ -1,28 +0,0 @@ -import resolve from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import typescript from '@rollup/plugin-typescript'; -import injectProcessEnv from 'rollup-plugin-inject-process-env'; -import terser from '@rollup/plugin-terser'; - -// eslint-disable-next-line no-undef -const entries = process.env.ENTRIES.split(','); - -export default entries.map(entry => { - return { - input: `src/${entry}.ts`, // Entry file - output: { - file: `dist/${entry}.js`, // Output file - format: 'iife', // Immediately Invoked Function Expression format - }, - treeshake: true, - plugins: [ - typescript(), // Compile TypeScript - resolve(), // Resolve node_modules dependencies - commonjs({ extensions: ['.js', '.ts'] }), // Convert CommonJS modules to ES6 - injectProcessEnv({ - NODE_ENV: 'production', - }), - terser(), - ], - }; -}) \ No newline at end of file diff --git a/chrome-extension/src/communicator.ts b/chrome-extension/src/communicator.ts index e43940d..03d2116 100644 --- a/chrome-extension/src/communicator.ts +++ b/chrome-extension/src/communicator.ts @@ -1,36 +1,66 @@ -import { getDevtools, devtoolsKey } from "stalo/lib/devtools"; -import { eventInit, eventRecord, eventUpdate, StaloEvent } from "./types"; - -(async function connect() { - await new Promise((resolve) => { - if (getDevtools()) resolve(); - window.addEventListener(devtoolsKey, () => { - resolve(); +import { getDevtools, devtoolsKey, Devtools } from "stalo/lib/devtools"; +import { initName } from "@stalo/devtools-ui"; +import { sendMessage, setNamespace, onMessage } from "webext-bridge/window"; +import { + eventGet, + eventInit, + eventRecord, + eventSet, + Get, + Init, + namespace, + Record as Rec, + Set, +} from "./constants"; +import { uid } from "stalo/lib/utils"; + +connectAll(); + +async function connectAll() { + setNamespace(namespace); + + const list: Record> = {}; + + function updateList() { + getDevtools().forEach((d) => { + if (list[d.id]) return; + + list[d.id] = d; + connect(d); }); + } + + window.addEventListener(devtoolsKey, () => { + updateList(); }); - const list = getDevtools(); + setInterval(updateList, 1000); - if (!list) { - return; - } + onMessage(eventGet, ({ data }) => { + return list[data].state; + }); - list.forEach((devtools, i) => { - window.dispatchEvent( - new CustomEvent(eventInit, { - detail: [i, devtools.name, devtools.initRecord], - }) - ); - - devtools.subscribe((record) => { - window.dispatchEvent( - new CustomEvent(eventRecord, { detail: [i, record] }) - ); - }); + onMessage(eventSet, ({ data }) => { + list[data.id].state = data.state; }); +} + +function connect(d: Devtools) { + const init: Init = { + sessionID: d.id, + name: d.name, + record: { + id: uid(), + name: initName, + description: "Initial state when devtools is opened", + state: d.state, + createdAt: Date.now(), + }, + }; + sendMessage(eventInit, init, "devtools"); - window.addEventListener(eventUpdate, (e) => { - const [i, state] = (e as StaloEvent).detail; - list[i].state = JSON.parse(state); + d.subscribe((record) => { + const req: Rec = { id: d.id, record }; + sendMessage(eventRecord, req, "devtools"); }); -})(); +} diff --git a/chrome-extension/src/constants.ts b/chrome-extension/src/constants.ts new file mode 100644 index 0000000..3ad8fbb --- /dev/null +++ b/chrome-extension/src/constants.ts @@ -0,0 +1,31 @@ +export const namespace = "@@stalo"; +export const eventInit = "@@stalo-init"; +export const eventRecord = "@@stalo-record"; +export const eventSet = "@@stalo-set"; +export const eventGet = "@@stalo-get"; + +type Rec = { + id: string; + name: string; + state: S; + description?: string; + createdAt: number; +}; + +export type Init = { + sessionID: string; + name: string; + record: Rec; +}; + +export type Get = string; + +export type Set = { + id: string; + state: object; +}; + +export type Record = { + id: string; + record: Rec; +}; diff --git a/chrome-extension/src/content-script.ts b/chrome-extension/src/content-script.ts index 584d2f7..1fc8674 100644 --- a/chrome-extension/src/content-script.ts +++ b/chrome-extension/src/content-script.ts @@ -1,23 +1,10 @@ -import { sendMessage, onMessage } from "webext-bridge/content-script"; -import { StaloEvent, eventInit, eventRecord, eventUpdate } from "./types"; +import { allowWindowMessaging } from "webext-bridge/content-script"; +import { namespace } from "./constants"; -(() => { - window.addEventListener(eventInit, (e) => { - sendMessage(eventInit, (e as StaloEvent).detail, "devtools"); - }); +allowWindowMessaging(namespace); - window.addEventListener(eventRecord, (e) => { - sendMessage(eventRecord, (e as StaloEvent).detail, "devtools"); - }); - - onMessage(eventUpdate, ({ data }) => { - const event = new CustomEvent(eventUpdate, { detail: data }); - window.dispatchEvent(event); - }); - - window.addEventListener("load", () => { - const script = document.createElement("script"); - script.src = chrome.runtime.getURL("communicator.js"); - document.head.appendChild(script); - }); -})(); +window.addEventListener("load", () => { + const script = document.createElement("script"); + script.src = chrome.runtime.getURL("communicator.js"); + document.head.appendChild(script); +}); diff --git a/chrome-extension/src/index.tsx b/chrome-extension/src/index.tsx index 8babd0f..cfd7ba2 100644 --- a/chrome-extension/src/index.tsx +++ b/chrome-extension/src/index.tsx @@ -1,52 +1,59 @@ import ReactDOM from "react-dom/client"; import { onMessage, sendMessage } from "webext-bridge/devtools"; -import { eventInit, eventRecord, eventUpdate } from "./types"; -import { Record } from "stalo/lib/devtools"; -import { unplug, plug, Connection, Panel } from "@stalo/devtools-ui"; +import { + eventGet, + eventInit, + eventRecord, + eventSet, + Get, + Init, + Record as Rec, + Set, +} from "./constants"; +import { unplug, Connection, Panel, plug } from "@stalo/devtools-ui"; connect(); +render(); -const root = document.createElement("div"); +function render() { + const root = document.createElement("div"); -document.body.appendChild(root); + document.body.appendChild(root); -ReactDOM.createRoot(root).render(); + ReactDOM.createRoot(root).render(); +} function connect() { - const list: Connection[] = []; + const list: Record = {}; chrome.runtime.onConnect.addListener((port) => { port.onDisconnect.addListener(() => { - list.forEach((_, id) => { - unplug(id); - }); + Object.keys(list).forEach((id) => unplug(id)); }); }); - onMessage(eventInit, async ({ data }) => { - const [id, name, rec] = data as unknown as [ - number, - string, - Record - ]; - + onMessage(eventInit, ({ data }) => { const conn: Connection = { - id, - name, - setState(json) { - sendMessage(eventUpdate, [id, json], "content-script"); + id: data.sessionID, + name: data.name, + getState: async () => { + const req: Get = data.sessionID; + return await sendMessage(eventGet, req, "window"); + }, + setState: (state) => { + const req: Set = { id: data.sessionID, state }; + sendMessage(eventSet, req, "window"); }, }; - list[id] = conn; - plug(conn); - conn.onInit?.(rec); + conn.onInit?.(data.record); + + list[conn.id] = conn; }); - onMessage(eventRecord, async ({ data }) => { - const [id, rec] = data as unknown as [number, Record]; - list[id].onRecord?.(rec); + onMessage(eventRecord, ({ data }) => { + list[data.id]?.onRecord?.(data.record); }); } diff --git a/chrome-extension/src/types.ts b/chrome-extension/src/types.ts deleted file mode 100644 index b71c326..0000000 --- a/chrome-extension/src/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const eventInit = "stalo-init"; -export const eventRecord = "stalo-record"; -export const eventUpdate = "stalo-update"; - -export interface StaloEvent extends Event { - detail: [number, string]; -} diff --git a/chrome-extension/vite.config.ts b/chrome-extension/vite.config.ts index 806c5a2..a1973f1 100644 --- a/chrome-extension/vite.config.ts +++ b/chrome-extension/vite.config.ts @@ -5,5 +5,14 @@ import manifest from "./manifest"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), manifest], - optimizeDeps: { exclude: ["fsevents"] }, + optimizeDeps: { + exclude: ["fsevents"], + include: [ + `monaco-editor/esm/vs/language/json/json.worker`, + `monaco-editor/esm/vs/editor/editor.worker`, + ], + }, + build: { + chunkSizeWarningLimit: 10 * 1024 * 1024, + }, }); diff --git a/chrome-extension/vite.ext.config.ts b/chrome-extension/vite.ext.config.ts new file mode 100644 index 0000000..1074bff --- /dev/null +++ b/chrome-extension/vite.ext.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "vite"; + +const name = process.env.ENTRY!; + +export default defineConfig({ + optimizeDeps: { exclude: ["fsevents"] }, + build: { + emptyOutDir: false, + chunkSizeWarningLimit: 10 * 1024 * 1024, + rollupOptions: { + input: `./src/${name}.ts`, + output: { + entryFileNames: "[name].js", + format: "iife", + }, + }, + }, +}); diff --git a/cspell.json b/cspell.json index c79972b..2e93f73 100644 --- a/cspell.json +++ b/cspell.json @@ -2,7 +2,9 @@ "words": [ "codemirror", "immer", + "immerable", "okaidia", + "Stackframe", "stalo", "todomvc", "todos", diff --git a/devtools-ui/README.md b/devtools-ui/README.md new file mode 100644 index 0000000..a9d5353 --- /dev/null +++ b/devtools-ui/README.md @@ -0,0 +1,15 @@ +# Overview + +## Usage + +Check the [example](../examples/Devtools.tsx). + +## Development + +To develop the devtools-ui: + +```bash +npm start +``` + +The entry file is the `index.html`. diff --git a/devtools-ui/dev/Components.tsx b/devtools-ui/dev/Components.tsx index 9aa934f..19b804a 100644 --- a/devtools-ui/dev/Components.tsx +++ b/devtools-ui/dev/Components.tsx @@ -4,11 +4,15 @@ import { compose } from "stalo/lib/utils"; import devtools, { description, name } from "stalo/lib/devtools"; import immer from "stalo/lib/immer"; -const useCountList: UseStore[] = []; -const setCountList: SetStore[] = []; +type Store = { + val: number; +}; -createStore("left"); -createStore("right"); +const useList: UseStore[] = []; +const setList: SetStore[] = []; + +createStore("x", 1); +createStore("y", 2); export function App() { return ( @@ -18,8 +22,8 @@ export function App() { }} >
- - + +
- setCountList[id]((c) => c + 1, { - [name]: "Increment", - [description]: "Increase the count by 1", - }) - } - > - {text} {useCountList[id]()} - +
+

{text}

+
); } -function createStore(name: string) { - const initStat = 0; +function createStore(name: string, val: number) { + const init = { val: val }; + const [use, baseSet] = create(init); - const [useCount, baseSetCount] = create(initStat); + const set = compose(baseSet, immer, devtools(init, name)); - const setCount = compose(baseSetCount, immer, devtools(initStat, name)); + useList.push(use); + setList.push(set); +} - useCountList.push(useCount); - setCountList.push(setCount); +function Button({ id, n }: { id: number; n: number }) { + return ( + + ); } diff --git a/devtools-ui/dev/index.tsx b/devtools-ui/dev/index.tsx index 3d57bbb..dfc089e 100644 --- a/devtools-ui/dev/index.tsx +++ b/devtools-ui/dev/index.tsx @@ -1,20 +1,13 @@ import ReactDOM from "react-dom/client"; -import connect from "../src/connect"; import { App } from "./Components"; import { StrictMode } from "react"; -connect(); +const root = document.createElement("div"); -setup(); +document.body.appendChild(root); -function setup() { - const root = document.createElement("div"); - - document.body.appendChild(root); - - ReactDOM.createRoot(root).render( - - - - ); -} +ReactDOM.createRoot(root).render( + + + +); diff --git a/devtools-ui/package.json b/devtools-ui/package.json index 990b191..9e4f88f 100644 --- a/devtools-ui/package.json +++ b/devtools-ui/package.json @@ -11,7 +11,6 @@ "publishConfig": { "provenance": true }, - "main": "./lib/index.js", "exports": { ".": "./lib/index.js" }, @@ -23,14 +22,14 @@ "url": "https://github.com/ysmood/stalo/tree/main/packages/devtools-ui" }, "dependencies": { - "@codemirror/lang-json": "^6.0.1", "@emotion/css": "^11.13.4", - "@uiw/codemirror-theme-okaidia": "^4.23.5", - "@uiw/react-codemirror": "^4.23.5", - "deep-equal": "^2.2.3", + "debounce": "^2.2.0", + "monaco-editor": "^0.52.0", + "react-icons": "^5.3.0", + "react-virtualized": "^9.22.5", "stalo": "^0.6.4" }, "devDependencies": { - "@types/deep-equal": "^1.0.4" + "@types/react-virtualized": "^9.21.30" } -} \ No newline at end of file +} diff --git a/devtools-ui/src/Components.tsx b/devtools-ui/src/Components.tsx index eb9a364..e9448b3 100644 --- a/devtools-ui/src/Components.tsx +++ b/devtools-ui/src/Components.tsx @@ -1,19 +1,23 @@ import { css, cx } from "@emotion/css"; -import { initName, noName } from "stalo/lib/devtools"; -import { commitName } from "./store"; +import { noName } from "stalo/lib/devtools"; +import { commitName, initName } from "./store/constants"; export function Button({ - text, + icon, + text = "", onClick, className, + title, }: { - text: string; + icon?: React.ReactElement; + text?: string; onClick?: () => void; className?: string; + title: string; }) { return ( -
- {text} +
+ {icon} {text}
); } @@ -25,6 +29,9 @@ const buttonStyle = css({ cursor: "pointer", transition: "background-color 0.3s, transform 0.1s", userSelect: "none", + display: "flex", + gap: 5, + alignItems: "center", "&:hover": { backgroundColor: "#555", diff --git a/devtools-ui/src/CurrentRecord.tsx b/devtools-ui/src/CurrentRecord.tsx index 52a8f12..c6c4509 100644 --- a/devtools-ui/src/CurrentRecord.tsx +++ b/devtools-ui/src/CurrentRecord.tsx @@ -1,6 +1,10 @@ -import { css } from "@emotion/css"; +import { css, cx } from "@emotion/css"; import { Name, Time } from "./Components"; import { useRecord, useSelected } from "./store"; +import { LuClock } from "react-icons/lu"; +import { TfiCommentAlt } from "react-icons/tfi"; +import { VscSymbolNumeric } from "react-icons/vsc"; +import { CiAt } from "react-icons/ci"; export function CurrentRecord() { const id = useSelected(); @@ -11,20 +15,22 @@ export function CurrentRecord() { } return ( -
-
-
- ID:
{id}
+
+
+
+ {id}
- Name: +
- Description: -
{record.description}
+
-
- Created At:
+
+ +
+ {record.description || No description}
@@ -35,23 +41,32 @@ const style = css({ label: "record-details", gridArea: "record-details", paddingTop: 10, + color: "#aaa", - ".info": { + div: { display: "flex", + gap: 3, + alignItems: "center", + }, + + ".first-row": { gap: 10, + height: "2em", + }, - "> div": { - display: "flex", - gap: 5, - color: "#aaa", - }, + ".id": { + color: "#eee", + }, - ".id": { - color: "#eee", - }, + ".desc": { + color: "#ddd", + marginLeft: 4, + lineHeight: "1.5em", + height: "3em", + overflow: "scroll", - ".desc": { - color: "#ddd", + span: { + color: "#666", }, }, }); diff --git a/devtools-ui/src/History.tsx b/devtools-ui/src/History.tsx index 624f3b1..3a4e009 100644 --- a/devtools-ui/src/History.tsx +++ b/devtools-ui/src/History.tsx @@ -2,40 +2,102 @@ import { css, cx } from "@emotion/css"; import { selectRecord, selectSession, - useHistoryIDs, + useRecordIDs, useRecord, useSelected, - useSessionIDs, + useCurrSession, + useSessions, useTimeDiff, + useTotalRecords, + useRecordScroll, + setRecordScroll, + setFilter, + useFilter, + useRecords, } from "./store"; -import { Name, Title } from "./Components"; +import { Button, Name, Title } from "./Components"; +import { List, AutoSizer } from "react-virtualized"; +import { recordHeight } from "./store/constants"; +import { LuArrowUpToLine, LuArrowDownToLine } from "react-icons/lu"; +import { useState } from "react"; +import { fuzzyMatch } from "./store/utils"; export default function History() { return (
-
+
<Sessions /> + <div className="session-id"> + Session: <code>{useCurrSession()}</code> + </div> </div> - <div> - {useHistoryIDs().map((id) => { - return <Item key={id} id={id} />; - })} + <div className="records"> + <ItemList /> </div> + <div className="footer"> + <Footer /> + </div> + </div> + ); +} + +function Footer() { + return ( + <> + <Filter /> + <div className="total"> + <span>{useTotalRecords()} records</span> + </div> + <Button + onClick={() => { + setRecordScroll(0); + }} + icon={<LuArrowUpToLine />} + title="Scroll to top record" + /> + <Button + onClick={() => { + setRecordScroll(Infinity); + }} + icon={<LuArrowDownToLine />} + title="Scroll to bottom record" + /> + </> + ); +} + +function Filter() { + const [val, setVal] = useState(""); + + return ( + <div className="filter"> + <input + placeholder="Filter records" + onChange={({ target: { value } }) => { + setVal(value); + setFilter(value); + }} + value={val} + /> </div> ); } function Sessions() { + const ss = useSessions(); + const list = Object.keys(ss).map((id) => ss[id]); + return ( <div className="sessions"> <select - onChange={(e) => selectSession(parseInt(e.target.value))} + onChange={(e) => selectSession(e.target.value)} + value={useCurrSession()} title="Select a devtools session" > - {useSessionIDs().map(({ id, name }) => ( + {list.map(({ id, name }) => ( <option key={id} value={id}> - {id} {name} + {name || id} </option> ))} </select> @@ -43,20 +105,63 @@ function Sessions() { ); } -function Item({ id }: { id: number }) { +function ItemList() { + const scroll = useRecordScroll(); + const list = useRecords(); + const filter = useFilter(); + const ids = useRecordIDs().filter((id) => { + const rec = list.get(id); + return ( + fuzzyMatch(rec.name, filter) || + fuzzyMatch(rec.description || "", filter) || + fuzzyMatch(id, filter) + ); + }); + + return ( + <AutoSizer> + {({ height, width }) => ( + <List + width={width} + height={height} + rowCount={ids.length} + rowHeight={recordHeight} + rowRenderer={({ key, index, style }) => { + return <Item key={key} id={ids[index]} num={index} style={style} />; + }} + scrollTop={scroll} + onScroll={({ scrollTop }) => { + setRecordScroll(scrollTop); + }} + /> + )} + </AutoSizer> + ); +} + +function Item({ + id, + style, + num, +}: { + id: string; + num: number; + style: React.CSSProperties; +}) { const rec = useRecord(id); return ( <div - className={cx("item", "border-bottom", { + className={cx("item", { selected: useSelected() === id, })} onClick={() => selectRecord(id)} + style={style} > <div className="line title"> <Name className="name" name={rec.name} /> <TimeDiff duration={useTimeDiff(id)} /> - <div className="id">{id}</div> + <code className="index">{num.toString().padStart(4, " ")}</code> </div> <div className="line light"> {rec.description === undefined ? NoDescription() : rec.description} @@ -68,6 +173,8 @@ function Item({ id }: { id: number }) { function TimeDiff({ duration }: { duration: number }) { if (duration === 0) return null; + const minus = duration < 0; + duration = Math.abs(duration); const sec = duration / 1000; @@ -75,11 +182,19 @@ function TimeDiff({ duration }: { duration: number }) { const hrs = Math.floor(min / 60); return ( - <div> - {duration < 0 ? "-" : "+"} + <div className="time-diff"> + {minus ? ( + <span className="minus">-</span> + ) : ( + <span className="plus">+</span> + )} {hrs > 0 ? <span>{hrs}h</span> : null} {min > 0 ? <span className="min">{min % 60}m</span> : null} - <span>{(sec % 60).toFixed(2)}s</span> + {sec % 60 > 1 ? ( + <span>{(sec % 60).toFixed(2)}s</span> + ) : ( + <span>{duration}ms</span> + )} </div> ); } @@ -90,19 +205,18 @@ function NoDescription() { const style = css({ label: "History", - height: "100%", overflowY: "scroll", boxShadow: "0 0 10px rgba(0, 0, 0, 0.5)", gridArea: "history", - ".border-bottom": { - borderBottom: "1px solid #3c3c3c", - }, + display: "grid", + gridTemplateRows: "auto 1fr auto", ".header": { display: "flex", alignItems: "center", gap: 10, + boxShadow: "0 0 5px rgba(0, 0, 0, 0.5)", select: { background: "#4e4e4ec2", @@ -110,6 +224,42 @@ const style = css({ padding: "0 5px", borderRadius: 3, }, + + ".session-id": { + fontSize: 10, + color: "#777", + }, + }, + + ".footer": { + display: "flex", + + ".filter": { + display: "grid", + width: "12em", + + input: { + all: "unset", + background: "#4e4e4ec2", + color: "white", + boxShadow: "0 0 3px rgba(0, 0, 0, 0.5) inset", + padding: "0 10px", + }, + }, + + ".total": { + flex: 1, + display: "flex", + alignItems: "center", + paddingLeft: 10, + fontSize: 10, + }, + + boxShadow: "0 0 5px rgba(0, 0, 0, 0.5)", + }, + + ".records": { + overflowY: "scroll", }, ".title": { @@ -121,7 +271,9 @@ const style = css({ display: "flex", flexDirection: "column", gap: 5, - padding: "5px 0", + boxSizing: "border-box", + paddingTop: 5, + borderBottom: "1px solid #3c3c3c", "&:hover, &.selected": { background: "#3c3c3c", @@ -142,8 +294,26 @@ const style = css({ gap: 10, color: "#ccc", + ".time-diff": { + fontSize: 10, + fontFamily: "monospace", + + ".minus": { + color: "#d84685", + }, + ".plus": { + color: "#66ffac", + }, + }, + + ".index": { + color: "#777", + fontSize: 10, + whiteSpace: "pre", + }, + ".min": { - color: "#d84685", + color: "#e1e05a", }, }, diff --git a/devtools-ui/src/MonacoEditor.tsx b/devtools-ui/src/MonacoEditor.tsx new file mode 100644 index 0000000..2641902 --- /dev/null +++ b/devtools-ui/src/MonacoEditor.tsx @@ -0,0 +1,98 @@ +import "monaco-editor"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-expect-error +import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-expect-error +import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; + +import { useRef, useEffect } from "react"; +import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; +import { setEditorHandlers, useCurrSession, useStaging } from "./store"; +import debounce from "debounce"; + +window.MonacoEnvironment = { + getWorker(_, label: string) { + if (label === "json") { + return new jsonWorker(); + } + return new editorWorker(); + }, +}; + +export function MonacoEditor({ className }: { className?: string }) { + const container = useRef<HTMLDivElement>(null); + const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null); + const value = useStaging(); + const session = useCurrSession(); + + useEffect(() => { + if (!container.current) return; + + const editor = monaco.editor.create(container.current, { + language: "json", + theme: "vs-dark", + wordBasedSuggestions: "currentDocument", + quickSuggestions: true, + }); + + const closeAutoResize = autoResize(editor); + + editorRef.current = editor; + + return () => { + closeAutoResize(); + editor.dispose(); + }; + }, [container]); + + useEffect(() => { + if (session) { + setEditorHandlers( + () => { + return editorRef.current?.getModel()?.getValue() || ""; + }, + (val) => { + if (editorRef.current) setContent(editorRef.current, val); + } + ); + } + + setTimeout(() => { + if (editorRef.current) setContent(editorRef.current, value); + }); + }, [session, value]); + + return <div ref={container} className={className}></div>; +} + +function autoResize(editor: monaco.editor.IStandaloneCodeEditor) { + const ln = debounce(() => { + editor.layout({ height: 0, width: 0 }); + editor.layout(); + }, 300); + window.addEventListener("resize", ln); + + return () => { + window.removeEventListener("resize", ln); + }; +} + +function setContent( + editor: monaco.editor.IStandaloneCodeEditor, + newValue: string +) { + const model = editor.getModel(); + if (!model) return; + + const fullRange = model.getFullModelRange(); + + // Ensure the editor history is preserved + editor.executeEdits("replace-content", [ + { + range: fullRange, + text: newValue, + }, + ]); +} diff --git a/devtools-ui/src/Panel.tsx b/devtools-ui/src/Panel.tsx index a1690ff..85d6db5 100644 --- a/devtools-ui/src/Panel.tsx +++ b/devtools-ui/src/Panel.tsx @@ -1,11 +1,26 @@ +import "./global-css"; import { css } from "@emotion/css"; import Staging from "./Staging"; import History from "./History"; import { CurrentRecord } from "./CurrentRecord"; +import connect from "./connect"; +import { useEffect } from "react"; + +export default function Panel({ + chromeExtension, + width, + height, +}: { + chromeExtension?: boolean; + width?: number; + height?: number; +}) { + useEffect(() => { + if (!chromeExtension) return connect(); + }, [chromeExtension]); -export default function Panel() { return ( - <div className={style}> + <div className={style} style={{ width, height }}> <History /> <CurrentRecord /> <Staging /> @@ -27,7 +42,7 @@ const style = css({ gap: 10, height: "100%", color: "#fff", - fontFamily: "Arial, sans-serif", + fontFamily: "Roboto, sans-serif", fontSize: 12, backgroundColor: "#282828", diff --git a/devtools-ui/src/Staging.tsx b/devtools-ui/src/Staging.tsx index 5b33c2b..9d314c9 100644 --- a/devtools-ui/src/Staging.tsx +++ b/devtools-ui/src/Staging.tsx @@ -1,59 +1,66 @@ -import { - setStaging, - commit, - useStaging, - revert, - useSelectedSession, -} from "./store"; +import { commit, format, revert, useGetState } from "./store"; import { css } from "@emotion/css"; -import CodeMirror from "@uiw/react-codemirror"; -import { json } from "@codemirror/lang-json"; -import { okaidia } from "@uiw/codemirror-theme-okaidia"; import { Button, Title } from "./Components"; +import { MonacoEditor } from "./MonacoEditor"; +import { LuCheck, LuUndoDot } from "react-icons/lu"; +import { VscSymbolNamespace } from "react-icons/vsc"; export default function Staging() { - const staging = useStaging(); - const sessionID = useSelectedSession(); - return ( <div className={style}> - <div className="toolbar"> - <Title text="Staging" /> - <Button onClick={() => revert()} text="Revert" /> - <Button onClick={() => commit(sessionID, staging)} text="Commit" /> - </div> - - <div className="editor"> - <CodeMirror - value={staging} - height="100%" - extensions={[json()]} - theme={okaidia} - onChange={(val) => { - setStaging(val); - }} - /> - </div> + <Toolbar /> + <MonacoEditor className="editor" /> + </div> + ); +} + +function Toolbar() { + const getState = useGetState(); + + return ( + <div className="toolbar"> + <Title text="Staging" /> + <Button + onClick={() => revert(getState)} + icon={<LuUndoDot size={16} />} + title="Use current page state as staging content" + /> + <Button + onClick={() => commit()} + icon={<LuCheck size={16} color="#20cf20" />} + text="Commit" + title="Set page state as staging content" + /> + <Button + onClick={() => format()} + icon={<VscSymbolNamespace size={14} />} + className="format" + title="Format json" + /> </div> ); } const style = css({ label: "Editor", + gridArea: "staging", - display: "flex", + display: "grid", gap: 10, - flexDirection: "column", - gridArea: "staging", + gridTemplateRows: "auto 1fr", ".toolbar": { display: "flex", alignItems: "center", gap: 10, - height: 40, + + ".format": { + fontFamily: "monospace", + }, }, ".editor": { - flex: 1, + height: "100%", + boxShadow: "0 0 5px #1e1e1e", }, }); diff --git a/devtools-ui/src/connect.ts b/devtools-ui/src/connect.ts index 1f9a471..8c1f00c 100644 --- a/devtools-ui/src/connect.ts +++ b/devtools-ui/src/connect.ts @@ -1,26 +1,45 @@ import { getDevtools } from "stalo/lib/devtools"; -import { plug, Connection } from "./store"; +import { plug, unplug } from "./store"; +import { Connection, initName } from "./store/constants"; +import { uid } from "stalo/lib/utils"; export default function connect() { - const list = getDevtools(); + const list = getDevtools<object>(); - if (!list) return; + const closes: (() => void)[] = []; - list.forEach((dt, i) => { + list.forEach((dt) => { const conn: Connection = { - id: i, + id: dt.id, name: dt.name, - setState(json) { - dt.state = JSON.parse(json); + setState(state) { + dt.state = state; + }, + async getState() { + return dt.state; }, }; plug(conn); - conn.onInit?.(dt.initRecord); + conn.onInit?.({ + id: uid(), + name: initName, + state: dt.state, + createdAt: Date.now(), + }); - dt.subscribe((rec) => { + const close = dt.subscribe((rec) => { conn.onRecord?.(rec); }); + + closes.push(() => { + close(); + unplug(conn.id); + }); }); + + return () => { + closes.forEach((c) => c()); + }; } diff --git a/devtools-ui/src/global-css.tsx b/devtools-ui/src/global-css.tsx new file mode 100644 index 0000000..6d365b1 --- /dev/null +++ b/devtools-ui/src/global-css.tsx @@ -0,0 +1,5 @@ +import { injectGlobal } from "@emotion/css"; + +injectGlobal( + `@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;400&display=swap');` +); diff --git a/devtools-ui/src/index.ts b/devtools-ui/src/index.ts index 8bb893b..dfbae2e 100644 --- a/devtools-ui/src/index.ts +++ b/devtools-ui/src/index.ts @@ -1,3 +1,3 @@ export { default as Panel } from "./Panel"; -export { default as connect } from "./connect"; -export { plug, unplug, type Connection } from "./store"; +export { plug, unplug } from "./store"; +export { type Connection, initName } from "./store/constants"; diff --git a/devtools-ui/src/store.ts b/devtools-ui/src/store.ts deleted file mode 100644 index 38f5905..0000000 --- a/devtools-ui/src/store.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { create } from "stalo/lib/immer"; -import { Record } from "stalo/lib/devtools"; -import { useEqual } from "stalo/lib/utils"; -import deepEqual from "deep-equal"; - -export interface Connection { - id: number; - name: string; - setState(json: string): void; - onInit?: (data: Record<unknown>) => void; - onRecord?: (data: Record<unknown>) => void; -} - -const emptySession = { - name: "", - selected: 0, - current: null, - staging: "", - history: [], -}; - -const initStore = { - selectedSession: 0, - sessions: [] as { - name: string; - selected: number; - current: unknown; - staging: string; - history: Record<unknown>[]; - }[], -}; - -type Store = typeof initStore; - -let stateSetters: Connection["setState"][] = []; - -const [useStore, setStore] = create(initStore); - -export function plug(c: Connection) { - stateSetters[c.id] = c.setState; - - c.onInit = (rec) => { - setStore((store) => { - store.sessions.push({ - name: c.name, - selected: 0, - current: rec.state, - staging: JSON.stringify(rec.state, null, 2), - history: [rec], - }); - }); - }; - - c.onRecord = (data) => { - setStore((store) => { - const s = store.sessions[c.id]; - s.current = data.state; - s.history.push(data); - }); - }; -} - -export function unplug(id: number) { - stateSetters = stateSetters.filter((_, i) => i !== id); - - setStore((store) => { - store.sessions = store.sessions.filter((_, i) => i !== id); - }); -} - -export function useSelectedSession() { - return useStore((s) => s.selectedSession); -} - -export function useSessionIDs() { - return useStore( - useEqual( - (s) => - s.sessions.map((_, i) => ({ - id: i, - name: s.sessions[i].name, - })), - deepEqual - ) - ); -} - -export function useRecord(id: number) { - return useStore((s) => history(s)[id]); -} - -// diff between a record with the selected record -export function useTimeDiff(id: number) { - return useStore((s) => { - return history(s)[id].createdAt - history(s)[selected(s)].createdAt; - }); -} - -export function useHistoryIDs() { - return useStore(useEqual((s) => history(s).map((_, i) => i), deepEqual)); -} - -export function useStaging() { - return useStore((s) => session(s).staging); -} - -export function setStaging(val: string) { - setStore((s) => { - session(s).staging = val; - }); -} - -export function useSelected() { - return useStore((s) => selected(s)); -} - -export function selectRecord(i: number) { - setStore((s) => { - session(s).selected = i; - session(s).staging = JSON.stringify(history(s)[i].state, null, 2); - }); -} - -export const commitName = "@@commit"; - -export function commit(sessionID: number, staging: string) { - setStore((s) => { - session(s).current = JSON.parse(staging); - history(s).push({ - state: session(s).current, - name: commitName, - description: "Committed by devtools", - createdAt: Date.now(), - }); - }); - stateSetters[sessionID](staging); -} - -export function revert() { - setStore((s) => { - session(s).staging = JSON.stringify(session(s).current, null, 2); - }); -} - -export function selectSession(id: number) { - setStore((s) => { - s.selectedSession = id; - }); -} - -function history(s: Store) { - return session(s).history; -} - -function selected(s: Store) { - return session(s).selected; -} - -function session(s: Store) { - return s.sessions[s.selectedSession] || emptySession; -} diff --git a/devtools-ui/src/store/constants.ts b/devtools-ui/src/store/constants.ts new file mode 100644 index 0000000..fcad9c1 --- /dev/null +++ b/devtools-ui/src/store/constants.ts @@ -0,0 +1,45 @@ +import { Record as Rec } from "stalo/lib/devtools"; +import { immutable } from "stalo/lib/utils"; +import { noName } from "stalo/lib/devtools"; +import History from "./history"; + +export const initName = "@@init"; +export const commitName = "@@commit"; + +export interface Connection { + id: string; + name: string; + setState(state: object): void; + getState(): Promise<object>; + onInit?: (data: Rec<object>) => void; + onRecord?: (data: Rec<object>) => void; +} + +export const emptySession = { + id: "", + name: "none", + selected: "", + staging: "", + getEditorValue: () => "", + setEditorValue: (() => {}) as (value: string) => void, + history: new History(), + filter: "", + connection: immutable<Connection>({ + id: "", + name: noName, + setState: () => {}, + getState: async () => ({}), + }), + recordScroll: 0, +}; + +export type Session = typeof emptySession; + +export const initStore = { + currSession: "", + sessions: {} as Record<string, Session>, +}; + +export const recordHeight = 42; + +export const bufferDelay = 100; diff --git a/devtools-ui/src/store/history.ts b/devtools-ui/src/store/history.ts new file mode 100644 index 0000000..caa0bf6 --- /dev/null +++ b/devtools-ui/src/store/history.ts @@ -0,0 +1,39 @@ +import { immerable } from "immer"; +import { Record } from "stalo/lib/devtools"; +import { Immutable, immutable } from "stalo/lib/utils"; + +export default class History { + [immerable] = true; + + private map = new Map<string, Immutable<Record<object>>>(); + readonly ids = [] as string[]; + + private static emptyRecord: Record<object> = { + id: "", + state: {}, + name: "", + description: "", + createdAt: 0, + }; + + constructor(...records: Record<object>[]) { + records.forEach((rec) => { + this.add(rec); + }); + } + + add(rec: Record<object>) { + this.map.set(rec.id, immutable(rec)); + + // Use unshift will make the virtual list super slow. + this.ids.push(rec.id); + } + + get(id: string) { + const rec = this.map.get(id); + if (!rec) { + return History.emptyRecord; + } + return rec(); + } +} diff --git a/devtools-ui/src/store/index.ts b/devtools-ui/src/store/index.ts new file mode 100644 index 0000000..c7b5377 --- /dev/null +++ b/devtools-ui/src/store/index.ts @@ -0,0 +1,188 @@ +import { create } from "stalo/lib/immer"; +import { enableMapSet } from "immer"; +import { immutable, uid } from "stalo/lib/utils"; +import History from "./history"; +import { + bufferDelay, + commitName, + Connection, + emptySession, + initStore, + recordHeight, + Session, +} from "./constants"; +import { bufferedCall } from "./utils"; +import debounce from "debounce"; + +enableMapSet(); + +const [useStore, setStore] = create(initStore); + +export function plug(c: Connection) { + const id = c.id; + + c.onInit = (rec) => { + setStore((store) => { + if (!store.currSession) store.currSession = id; + + store.sessions[id] = { + ...emptySession, + id, + name: c.name, + selected: rec.id, + staging: JSON.stringify(rec.state, null, 2), + history: new History(rec), + connection: immutable(c), + }; + }); + }; + + c.onRecord = bufferedCall(bufferDelay, (list) => { + setStore((store) => { + list.forEach((rec) => { + store.sessions[id].history.add(rec); + }); + }); + }); +} + +export function unplug(id: string) { + setStore((store) => { + if (store.currSession === id) store.currSession = ""; + delete store.sessions[id]; + }); +} + +const useSession = <T>(selector: (s: Session, ss: boolean) => T) => { + return useStore((s, ss) => { + return selector(s.sessions[s.currSession] || emptySession, ss); + }); +}; + +const setSession = (set: (s: Session) => void) => { + setStore((s) => { + const ss = s.sessions[s.currSession]; + if (ss) set(ss); + }); +}; + +export function useCurrSession() { + return useStore((s) => s.currSession); +} + +export function useSessions() { + return useStore((s) => s.sessions); +} + +export function useRecord(id: string) { + return useSession((s) => s.history.get(id)); +} + +export function useRecords() { + return useSession((s) => s.history); +} + +// diff between a record with the selected record +export function useTimeDiff(id: string) { + return useSession((s) => { + return s.history.get(id).createdAt - s.history.get(s.selected).createdAt; + }); +} + +export function useRecordIDs() { + return useSession((s) => s.history.ids); +} + +export function useStaging() { + return useSession((s) => s.staging); +} + +export function setEditorHandlers( + get: () => string, + set: (value: string) => void +) { + setSession((s) => { + s.getEditorValue = get; + s.setEditorValue = set; + }); +} + +export function useSelected() { + return useSession((s) => s.selected); +} + +export function selectRecord(id: string) { + setSession((s) => { + s.selected = id; + s.staging = JSON.stringify(s.history.get(id).state, null, 2); + }); +} + +export function commit() { + setSession((s) => { + const state = JSON.parse(s.getEditorValue()); + const id = uid(); + s.history.add({ + id, + state, + name: commitName, + description: "Committed by devtools", + createdAt: Date.now(), + }); + + s.connection().setState(state); + }); +} + +export function useGetState() { + return useSession((s) => s.connection().getState); +} + +export async function revert(getState: Connection["getState"]) { + const state = await getState(); + + setSession((s) => { + s.staging = JSON.stringify(state, null, 2); + }); +} + +export function selectSession(id: string) { + setStore((s) => { + s.currSession = id; + }); +} + +export function useRecordScroll() { + return useSession((s) => s.recordScroll); +} + +export function setRecordScroll(index: number) { + setSession((s) => { + if (index === Infinity) { + s.recordScroll = (s.history.ids.length - 1) * recordHeight; + return; + } + + s.recordScroll = index; + }); +} + +export function useTotalRecords() { + return useSession((s) => s.history.ids.length); +} + +export function format() { + setSession((s) => { + s.setEditorValue(JSON.stringify(JSON.parse(s.getEditorValue()), null, 2)); + }); +} + +export function useFilter() { + return useSession((s) => s.filter); +} + +export const setFilter = debounce((filter: string) => { + setSession((s) => { + s.filter = filter; + }); +}, 300); diff --git a/devtools-ui/src/store/utils.ts b/devtools-ui/src/store/utils.ts new file mode 100644 index 0000000..6f1a801 --- /dev/null +++ b/devtools-ui/src/store/utils.ts @@ -0,0 +1,43 @@ +type Callback<T> = (buffer: T[]) => void; + +export function bufferedCall<T>(delay: number, callback: Callback<T>) { + const buf: T[] = []; + let lastCallTime = 0; + let timeoutId: NodeJS.Timeout | null = null; + + return function (rec: T) { + const now = Date.now(); + buf.push(rec); + + if (timeoutId) { + clearTimeout(timeoutId); + } + + if (now - lastCallTime < delay) { + timeoutId = setTimeout(() => { + callback(buf); + buf.length = 0; + }, delay); + } else { + callback(buf); + buf.length = 0; + } + + lastCallTime = now; + }; +} + +export function fuzzyMatch(input: string, pattern: string): boolean { + let patternIndex = 0; + + for (let i = 0; i < input.length; i++) { + if (input[i] === pattern[patternIndex]) { + patternIndex++; + } + if (patternIndex === pattern.length) { + return true; + } + } + + return false; +} diff --git a/devtools-ui/tsconfig.lib.json b/devtools-ui/tsconfig.lib.json index 6299076..b7b9b51 100644 --- a/devtools-ui/tsconfig.lib.json +++ b/devtools-ui/tsconfig.lib.json @@ -8,7 +8,6 @@ "rootDir": "./src", "outDir": "./lib", "declaration": true, - "sourceMap": true, /* Bundler mode */ "moduleResolution": "Bundler", diff --git a/devtools-ui/vite.config.ts b/devtools-ui/vite.config.ts index f189d15..37e2832 100644 --- a/devtools-ui/vite.config.ts +++ b/devtools-ui/vite.config.ts @@ -1,8 +1,38 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; +import fs from "fs"; +import path from "path"; +import { createRequire } from "module"; + +const require = createRequire(import.meta.url); + +const WRONG_CODE = `import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";`; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], - optimizeDeps: { exclude: ["fsevents"] }, + plugins: [react(), reactVirtualized()], + optimizeDeps: { + exclude: ["fsevents"], + include: [ + `monaco-editor/esm/vs/language/json/json.worker`, + `monaco-editor/esm/vs/editor/editor.worker`, + ], + }, }); + +function reactVirtualized() { + return { + name: "my:react-virtualized", + configResolved() { + const file = require + .resolve("react-virtualized") + .replace( + path.join("dist", "commonjs", "index.js"), + path.join("dist", "es", "WindowScroller", "utils", "onScroll.js") + ); + const code = fs.readFileSync(file, "utf-8"); + const modified = code.replace(WRONG_CODE, ""); + fs.writeFileSync(file, modified); + }, + }; +} diff --git a/examples/App.tsx b/examples/App.tsx index 6a2beb7..ac47f63 100644 --- a/examples/App.tsx +++ b/examples/App.tsx @@ -2,7 +2,13 @@ import "./index.css"; import { Link, Switch, Router, Route } from "wouter"; import { lazy, Suspense } from "react"; -const examples = ["Counter", "CounterPersistent", "MonolithStore", "TodoApp"]; +const examples = [ + "Counter", + "CounterPersistent", + "MonolithStore", + "TodoApp", + "Devtools", +]; export default function App() { return ( diff --git a/examples/Devtools.tsx b/examples/Devtools.tsx new file mode 100644 index 0000000..931ddb5 --- /dev/null +++ b/examples/Devtools.tsx @@ -0,0 +1,31 @@ +import create from "stalo"; +import { compose } from "stalo/lib/utils"; +import devtools from "stalo/lib/devtools"; +import { Panel } from "@stalo/devtools-ui"; + +const init = 0; + +const [useCount, baseSetCount] = create(init); + +const setCount = compose(baseSetCount, devtools(init, "Devtools Demo")); + +export default function Devtools() { + return ( + <div> + <h3>Basic devtools-ui usage</h3> + <Counter /> + <h3>↑ Click the button above to create new state records ↑</h3> + <Panel width={800} height={400} /> + </div> + ); +} + +function Counter() { + return ( + <div className="my-1"> + <button onClick={() => setCount((c) => c + 1)}> + <h4>Increase {useCount()}</h4> + </button> + </div> + ); +} diff --git a/examples/TodoApp/Filter.tsx b/examples/TodoApp/Filter.tsx index 516a566..136d67e 100644 --- a/examples/TodoApp/Filter.tsx +++ b/examples/TodoApp/Filter.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; import { setFilter } from "./store/filter"; -import { filters } from "./store/filter/types"; +import { filters } from "./store/filter/constants"; // The component to filter the todos. export default function Filter() { diff --git a/examples/TodoApp/store/filter/types.ts b/examples/TodoApp/store/filter/constants.ts similarity index 100% rename from examples/TodoApp/store/filter/types.ts rename to examples/TodoApp/store/filter/constants.ts diff --git a/examples/TodoApp/store/filter/index.ts b/examples/TodoApp/store/filter/index.ts index 62b6dc9..de0e416 100644 --- a/examples/TodoApp/store/filter/index.ts +++ b/examples/TodoApp/store/filter/index.ts @@ -1,5 +1,5 @@ import { setStore, useStore } from ".."; -import { Filter } from "./types"; +import { Filter } from "./constants"; export const useFilter = () => { return useStore((state) => state.filter); diff --git a/examples/TodoApp/store/index.ts b/examples/TodoApp/store/index.ts index 6fcc533..c1eeb2a 100644 --- a/examples/TodoApp/store/index.ts +++ b/examples/TodoApp/store/index.ts @@ -1,7 +1,7 @@ import create from "stalo"; import immer from "stalo/lib/immer"; -import { Filter, initFilter } from "./filter/types"; -import { initTodo } from "./todos/types"; +import { Filter, initFilter } from "./filter/constants"; +import { initTodo } from "./todos/constants"; import { compose } from "stalo/lib/utils"; import logger from "./logger"; import devtools from "stalo/lib/devtools"; diff --git a/examples/TodoApp/store/todos/types.ts b/examples/TodoApp/store/todos/constants.ts similarity index 100% rename from examples/TodoApp/store/todos/types.ts rename to examples/TodoApp/store/todos/constants.ts diff --git a/examples/TodoApp/store/todos/index.ts b/examples/TodoApp/store/todos/index.ts index e47fe99..5ba3aaf 100644 --- a/examples/TodoApp/store/todos/index.ts +++ b/examples/TodoApp/store/todos/index.ts @@ -1,6 +1,6 @@ import { setStore, Store, useStore } from ".."; import { numbersEqual } from "./utils"; -import { Filter } from "../filter/types"; +import { Filter } from "../filter/constants"; import { useEqual } from "stalo/lib/utils"; import { name } from "stalo/lib/devtools"; diff --git a/examples/TodoApp/store/toolbar.ts b/examples/TodoApp/store/toolbar.ts index fd8d5af..60662f0 100644 --- a/examples/TodoApp/store/toolbar.ts +++ b/examples/TodoApp/store/toolbar.ts @@ -1,5 +1,5 @@ import { setStore, useStore } from "."; -import { initTodo } from "./todos/types"; +import { initTodo } from "./todos/constants"; import { filterTodos } from "./todos"; // Add a new empty todo to the list. diff --git a/package-lock.json b/package-lock.json index 46954b9..5e05254 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,17 +55,17 @@ }, "devtools-ui": { "name": "@stalo/devtools-ui", - "version": "0.0.3", + "version": "0.6.4", "dependencies": { - "@codemirror/lang-json": "^6.0.1", "@emotion/css": "^11.13.4", - "@uiw/codemirror-theme-okaidia": "^4.23.5", - "@uiw/react-codemirror": "^4.23.5", - "deep-equal": "^2.2.3", - "stalo": "^0.6.3" + "debounce": "^2.2.0", + "monaco-editor": "^0.52.0", + "react-icons": "^5.3.0", + "react-virtualized": "^9.22.5", + "stalo": "^0.6.4" }, "devDependencies": { - "@types/deep-equal": "^1.0.4" + "@types/react-virtualized": "^9.21.30" } }, "node_modules/@ampproject/remapping": { @@ -410,102 +410,6 @@ "tough-cookie": "^4.1.4" } }, - "node_modules/@codemirror/autocomplete": { - "version": "6.18.1", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.1.tgz", - "integrity": "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA==", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.17.0", - "@lezer/common": "^1.0.0" - }, - "peerDependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@codemirror/commands": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.7.0.tgz", - "integrity": "sha512-+cduIZ2KbesDhbykV02K25A5xIVrquSPz4UxxYBemRlAT2aW8dhwUgLDwej7q/RJUHKk4nALYcR1puecDvbdqw==", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.4.0", - "@codemirror/view": "^6.27.0", - "@lezer/common": "^1.1.0" - } - }, - "node_modules/@codemirror/lang-json": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", - "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@lezer/json": "^1.0.0" - } - }, - "node_modules/@codemirror/language": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.3.tgz", - "integrity": "sha512-kDqEU5sCP55Oabl6E7m5N+vZRoc0iWqgDVhEKifcHzPzjqCegcO4amfrYVL9PmPZpl4G0yjkpTpUO/Ui8CzO8A==", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.23.0", - "@lezer/common": "^1.1.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0", - "style-mod": "^4.0.0" - } - }, - "node_modules/@codemirror/lint": { - "version": "6.8.2", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.2.tgz", - "integrity": "sha512-PDFG5DjHxSEjOXk9TQYYVjZDqlZTFaDBfhQixHnQOEVDDNHUbEh/hstAjcQJaA6FQdZTD1hquXTK0rVBLADR1g==", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/search": { - "version": "6.5.6", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz", - "integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/state": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", - "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" - }, - "node_modules/@codemirror/theme-one-dark": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz", - "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/highlight": "^1.0.0" - } - }, - "node_modules/@codemirror/view": { - "version": "6.34.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.34.1.tgz", - "integrity": "sha512-t1zK/l9UiRqwUNPm+pdIT0qzJlzuVckbTEMVNFhfWkGiBQClstzg+78vedCvLSX0xJEZ6lwZbPpnljL7L6iwMQ==", - "dependencies": { - "@codemirror/state": "^6.4.0", - "style-mod": "^4.1.0", - "w3c-keyname": "^2.2.4" - } - }, "node_modules/@emotion/babel-plugin": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz", @@ -999,37 +903,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@lezer/common": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", - "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==" - }, - "node_modules/@lezer/highlight": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", - "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", - "dependencies": { - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@lezer/json": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz", - "integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@lezer/lr": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", - "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", - "dependencies": { - "@lezer/common": "^1.0.0" - } - }, "node_modules/@mswjs/interceptors": { "version": "0.35.8", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.35.8.tgz", @@ -1701,12 +1574,6 @@ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", "dev": true }, - "node_modules/@types/deep-equal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@types/deep-equal/-/deep-equal-1.0.4.tgz", - "integrity": "sha512-tqdiS4otQP4KmY0PR3u6KbZ5EWvhNdUoS/jc93UuK23C220lOZ/9TvjfxdPcKvqwwDVtmtSCrnr0p/2dirAxkA==", - "dev": true - }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -1782,6 +1649,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-virtualized": { + "version": "9.21.30", + "resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.21.30.tgz", + "integrity": "sha512-4l2TFLQ8BCjNDQlvH85tU6gctuZoEdgYzENQyZHpgTHU7hoLzYgPSOALMAeA58LOWua8AzC6wBivPj1lfl6JgQ==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -1999,86 +1876,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@uiw/codemirror-extensions-basic-setup": { - "version": "4.23.5", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.5.tgz", - "integrity": "sha512-eTMfT8TejVN/D5vvuz9Lab+MIoRYdtqa2ftZZmU3JpcDIXf9KaExPo+G2Rl9HqySzaasgGXOOG164MAnj3MSIw==", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@codemirror/autocomplete": ">=6.0.0", - "@codemirror/commands": ">=6.0.0", - "@codemirror/language": ">=6.0.0", - "@codemirror/lint": ">=6.0.0", - "@codemirror/search": ">=6.0.0", - "@codemirror/state": ">=6.0.0", - "@codemirror/view": ">=6.0.0" - } - }, - "node_modules/@uiw/codemirror-theme-okaidia": { - "version": "4.23.5", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-okaidia/-/codemirror-theme-okaidia-4.23.5.tgz", - "integrity": "sha512-0O5f7xDlBU8QInNzZKmhjoia0eyEqWcPVF2EF+djGue4L8dyr9nyWpkUZFHiXdMA/CWr5Mdv8uepv7Onj4lIew==", - "dependencies": { - "@uiw/codemirror-themes": "4.23.5" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - } - }, - "node_modules/@uiw/codemirror-themes": { - "version": "4.23.5", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.23.5.tgz", - "integrity": "sha512-yWUTpaVroxIxjKASQAmKaYy+ZYtF+YB6d8sVmSRK2TVD13M+EWvVT2jBGFLqR1UVg7G0W/McAy8xdeTg+a3slg==", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@codemirror/language": ">=6.0.0", - "@codemirror/state": ">=6.0.0", - "@codemirror/view": ">=6.0.0" - } - }, - "node_modules/@uiw/react-codemirror": { - "version": "4.23.5", - "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.23.5.tgz", - "integrity": "sha512-2zzGpx61L4mq9zDG/hfsO4wAH209TBE8VVsoj/qrccRe6KfcneCwKgRxtQjxBCCnO0Q5S+IP+uwCx5bXRzgQFQ==", - "dependencies": { - "@babel/runtime": "^7.18.6", - "@codemirror/commands": "^6.1.0", - "@codemirror/state": "^6.1.1", - "@codemirror/theme-one-dark": "^6.0.0", - "@uiw/codemirror-extensions-basic-setup": "4.23.5", - "codemirror": "^6.0.0" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.11.0", - "@codemirror/state": ">=6.0.0", - "@codemirror/theme-one-dark": ">=6.0.0", - "@codemirror/view": ">=6.0.0", - "codemirror": ">=6.0.0", - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -2383,21 +2180,6 @@ "dequal": "^2.0.3" } }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", - "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -2413,20 +2195,6 @@ "node": ">=12" } }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", @@ -2567,6 +2335,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -2720,18 +2489,12 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/codemirror": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", - "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" } }, "node_modules/color-convert": { @@ -2822,11 +2585,6 @@ "node": ">=10" } }, - "node_modules/crelt": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", - "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2846,6 +2604,17 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/debounce": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz", + "integrity": "sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -2871,37 +2640,6 @@ "node": ">=6" } }, - "node_modules/deep-equal": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", - "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2921,6 +2659,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -2933,22 +2672,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2995,6 +2718,15 @@ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -3040,6 +2772,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, "dependencies": { "get-intrinsic": "^1.2.4" }, @@ -3051,29 +2784,11 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "engines": { "node": ">= 0.4" } }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -3660,14 +3375,6 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dependencies": { - "is-callable": "^1.1.3" - } - }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -3730,14 +3437,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3760,6 +3459,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", @@ -3841,6 +3541,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -3863,14 +3564,6 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -3883,6 +3576,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -3894,6 +3588,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -3905,20 +3600,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dependencies": { - "has-symbols": "^1.0.3" - }, + "dev": true, "engines": { "node": ">= 0.4" }, @@ -4037,19 +3719,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -4059,78 +3728,11 @@ "node": ">= 0.10" } }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-core-module": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", @@ -4145,20 +3747,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "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", @@ -4189,17 +3777,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -4221,20 +3798,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -4253,105 +3816,6 @@ "@types/estree": "*" } }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", - "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", - "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4753,6 +4217,11 @@ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "dev": true }, + "node_modules/monaco-editor": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz", + "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==" + }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", @@ -4948,50 +4417,19 @@ "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, + "dev": true, "engines": { "node": ">= 0.4" }, @@ -5260,14 +4698,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/postcss": { "version": "8.4.47", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", @@ -5331,6 +4761,21 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -5457,12 +4902,25 @@ "react": "^18.3.1" } }, + "node_modules/react-icons": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", + "integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==", + "peerDependencies": { + "react": "*" + } + }, "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/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -5472,28 +4930,28 @@ "node": ">=0.10.0" } }, + "node_modules/react-virtualized": { + "version": "9.22.5", + "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.5.tgz", + "integrity": "sha512-YqQMRzlVANBv1L/7r63OHa2b0ZsAaDp1UhVNEdUaXI8A5u6hTpA5NYtUueLH2rFuY/27mTGIBl7ZhqFKzw18YQ==", + "dependencies": { + "@babel/runtime": "^7.7.2", + "clsx": "^1.0.4", + "dom-helpers": "^5.1.3", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0", + "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", - "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/regexparam": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-3.0.0.tgz", @@ -5791,6 +5249,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -5803,20 +5262,6 @@ "node": ">= 0.4" } }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -5848,6 +5293,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", @@ -5967,17 +5413,6 @@ "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", "dev": true }, - "node_modules/stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dependencies": { - "internal-slot": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/strict-event-emitter": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", @@ -6050,11 +5485,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/style-mod": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", - "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" - }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -6549,11 +5979,6 @@ } } }, - "node_modules/w3c-keyname": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" - }, "node_modules/webext-bridge": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/webext-bridge/-/webext-bridge-6.0.1.tgz", @@ -6586,56 +6011,6 @@ "node": ">= 8" } }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", diff --git a/src/devtools.test.tsx b/src/devtools.test.tsx index 5c6b9bd..4d3d56c 100644 --- a/src/devtools.test.tsx +++ b/src/devtools.test.tsx @@ -18,9 +18,9 @@ it("devtools", async () => { compose(set, addOne, devtools(init)); - const d = getDevtools<number>()![0]; + const d = getDevtools<number>()[0]; - expect(d.initRecord.state).toBe(1); + expect(d.state).toBe(1); let count = 0; const close = d.subscribe((record) => { diff --git a/src/devtools.ts b/src/devtools.ts index c25c0fe..75a7069 100644 --- a/src/devtools.ts +++ b/src/devtools.ts @@ -1,5 +1,5 @@ import { producer, SetStore } from "."; -import { type Middleware } from "./utils"; +import { uid, type Middleware } from "./utils"; export const name = Symbol("action-name"); export const description = Symbol("description"); @@ -12,6 +12,7 @@ export type DevtoolsOptions = { }; export type Record<S> = { + id: string; name: string; state: S; description?: string; @@ -20,8 +21,6 @@ export type Record<S> = { export const noName = "@@no-name"; -export const initName = "@@init"; - export default function devtools<S>(init: S, devName = ""): Middleware<S> { return (set) => { const subscribers = new Set<Subscriber<S>>(); @@ -33,14 +32,7 @@ export default function devtools<S>(init: S, devName = ""): Middleware<S> { win[devtoolsKey] = []; } - win[devtoolsKey].push( - new Devtools( - devName, - { name: initName, state: init, createdAt: Date.now() }, - set, - subscribers - ) - ); + win[devtoolsKey].push(new Devtools(devName, init, set, subscribers)); window.dispatchEvent(new CustomEvent(devtoolsKey)); @@ -49,14 +41,15 @@ export default function devtools<S>(init: S, devName = ""): Middleware<S> { set((prev) => { const curr = produce(prev); - subscribers.forEach((s) => - s({ - state: curr, - name: options && options[name] ? options[name] : noName, - description: options?.[description], - createdAt: Date.now(), - }) - ); + const rec = { + id: uid(), + state: curr, + name: options && options[name] ? options[name] : noName, + description: options?.[description], + createdAt: Date.now(), + }; + + subscribers.forEach((s) => s(rec)); return curr; }, options); @@ -64,20 +57,28 @@ export default function devtools<S>(init: S, devName = ""): Middleware<S> { }; } -export function getDevtools<S>(): Devtools<S>[] | undefined { +export function getDevtools<S>(): Devtools<S>[] { // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (window as any)[devtoolsKey]; + return (window as any)[devtoolsKey] || []; } type Subscriber<S> = (record: Record<S>) => void; export class Devtools<S> { + private current: S; + constructor( readonly name: string, - readonly initRecord: Record<S>, + init: S, private set: SetStore<S>, - private subscribers: Set<Subscriber<S>> - ) {} + private subscribers: Set<Subscriber<S>>, + readonly id = uid() + ) { + this.current = init; + this.subscribe(({ state }) => { + this.current = state; + }); + } subscribe(cb: (record: Record<S>) => void) { this.subscribers.add(cb); @@ -91,6 +92,14 @@ export class Devtools<S> { * Set the current state without creating history. */ set state(s: S) { + this.current = s; this.set(() => s); } + + /** + * Peak the current state without hooking into React. + */ + get state() { + return this.current; + } } diff --git a/src/utils.test.tsx b/src/utils.test.tsx index 71c336c..e48e54b 100644 --- a/src/utils.test.tsx +++ b/src/utils.test.tsx @@ -2,7 +2,14 @@ import { it, expect } from "vitest"; import { userEvent } from "@vitest/browser/context"; import { render, screen } from "@testing-library/react"; import create, { producer, SetStore } from "."; -import { compose, Middleware, useEqual } from "./utils"; +import { + compose, + deepEqual, + immutable, + Middleware, + uid, + useEqual, +} from "./utils"; it("selector with equal", async () => { const [useVal, setVal] = create({ val: "test" }); @@ -49,3 +56,22 @@ it("compose", async () => { expect(state).toBe(6); }); + +it("deepEqual", async () => { + expect(deepEqual({ a: 1, b: [1, 2] }, { a: 1, b: [1, 2] })).toBeTruthy(); + expect(deepEqual(null, 1)).toBeFalsy(); + expect(deepEqual([], {})).toBeFalsy(); + expect(deepEqual([1], [])).toBeFalsy(); + expect(deepEqual([1], [2])).toBeFalsy(); + expect(deepEqual({ a: 1 }, {})).toBeFalsy(); + expect(deepEqual({ a: 1 }, { a: 2 })).toBeFalsy(); +}); + +it("uid", async () => { + expect(uid()).toHaveLength(8); +}); + +it("immutable", async () => { + const a = 1; + expect(immutable(a)()).toEqual(1); +}); diff --git a/src/utils.ts b/src/utils.ts index 0339569..eaf0c0d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -39,3 +39,50 @@ export function compose<S>( ) { return middlewares.reduceRight((s, middleware) => middleware(s), setStore); } + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function deepEqual(a: any, b: any): boolean { + if (a === b) return true; + + if ( + a === null || + b === null || + typeof a !== "object" || + typeof b !== "object" + ) { + return false; + } + + if (Array.isArray(a) !== Array.isArray(b)) return false; + + if (Array.isArray(a)) { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (!deepEqual(a[i], b[i])) return false; + } + return true; + } + + const keysA = Object.keys(a); + const keysB = Object.keys(b); + + if (keysA.length !== keysB.length) return false; + + for (const key of keysA) { + if (!keysB.includes(key) || !deepEqual(a[key], b[key])) return false; + } + + return true; +} + +export function uid(length: number = 8): string { + return Array.from({ length }, () => + Math.floor(Math.random() * 36).toString(36) + ).join(""); +} + +export type Immutable<T> = () => T; + +export function immutable<T>(obj: T): Immutable<T> { + return () => obj; +} diff --git a/tsconfig.lib.json b/tsconfig.lib.json index e17ab46..ccb737c 100644 --- a/tsconfig.lib.json +++ b/tsconfig.lib.json @@ -5,7 +5,7 @@ "target": "ESNext", "lib": ["DOM", "ES2020"], "module": "ESNext", - "moduleResolution": "Node", + "moduleResolution": "Bundler", "skipLibCheck": true, "rootDir": "./src", "outDir": "./lib",