diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..d44619d2 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|MultiEdit|Write", + "hooks": [ + { + "type": "command", + "command": "node --no-warnings \"$CLAUDE_PROJECT_DIR/scripts/lint-edited-file.ts\"", + "timeout": 60, + "statusMessage": "Linting the edited file" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "node --no-warnings \"$CLAUDE_PROJECT_DIR/scripts/check-before-stop.ts\"", + "timeout": 300, + "statusMessage": "Running the checks before finishing" + } + ] + } + ] + } +} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 5f1f2d97..ab89bd8b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -11,6 +11,6 @@ ## Checklist - [ ] Comes from a git flow branch (`feature/…`, `bugfix/…`) into `main`, and does one thing -- [ ] `npm run typecheck`, `npm run lint:fsd`, `npm run lint:structure`, `npm test` and `npm run test:e2e` pass +- [ ] `npm run typecheck`, `npm run lint:fsd`, `npm run lint:structure`, `npm run lint`, `npm run lint:unused`, `npm run lint:duplicates`, `npm run lint:secrets`, `npm test` and `npm run test:e2e` pass - [ ] `docs/SPEC.md` describes any behaviour this changes - [ ] User-visible changes are noted under `[Unreleased]` in `CHANGELOG.md` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0afd7a42..e507351a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,14 @@ jobs: - run: npm run lint:structure + - run: npm run lint + + - run: npm run lint:unused + + - run: npm run lint:duplicates + + - run: npm run lint:secrets + # Ubuntu 24.04 blocks the unprivileged user namespaces Chromium's sandbox uses unless an # AppArmor profile allows the binary (the .deb installs one). Playwright's Chromium and # Electron from node_modules have none. diff --git a/.gitignore b/.gitignore index 9721fa69..3e3fbeec 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ dist-next/ .DS_Store test-results/ *.tsbuildinfo +# Personal Claude Code settings (.claude/settings.json holds the shared ones) +.claude/settings.local.json diff --git a/.jscpd-baseline.json b/.jscpd-baseline.json new file mode 100644 index 00000000..75910ca3 --- /dev/null +++ b/.jscpd-baseline.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "fingerprints": { + "04bf8b15d3fc15f5": 1, + "3406d58fbb1964c0": 1, + "5527378cebdd03f1": 1, + "8a060e80d0b739f6": 1, + "938e89086d5dc1f1": 1, + "9bf006a9ac0610a2": 1, + "a2ffe737302d8522": 1, + "abecc2a6b6900f86": 1, + "ade32fb042158fe0": 1, + "b44f3991fa8ccb97": 1, + "c4d8c32fa46a266e": 1, + "e8f65d31eee8dbba": 1, + "ede3a93116db03e2": 1 + } +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 00000000..ae4b7b9b --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,23 @@ +// oxlint (https://oxc.rs): `npm run lint`, type-aware (typescript-go), so it keeps up with TypeScript 7. +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript", "unicorn", "oxc", "react"], + // Code that is wrong or useless. The react plugin includes React's own rules (rules of hooks, honest effect + // dependencies, and the React Compiler's: refs aren't read in render, props and hook results aren't changed, + // components are pure), native here, not through a JS plugin. + "categories": { "correctness": "error" }, + "rules": { + "react/rules-of-hooks": "error", + // A promise handed where nothing awaits it (an async onClick). `void promise()`, used on purpose, stays allowed + // by no-floating-promises. + "typescript/no-misused-promises": "error", + // Callbacks and deps are declared as methods (`onChange(next): void`) and destructured, never using `this`: the + // rule can't tell them from class methods, so all but one of its findings were those. + "typescript/unbound-method": "off", + // `[...collection]` is how this code snapshots a collection its loop changes (deleting records, removing nested + // sessions, handlers that unsubscribe while dispatched): the rule can't see the change. + "unicorn/no-useless-spread": "off", + // Only says the React Compiler would skip a component (TanStack Virtual's useVirtualizer). Turn it on with the compiler. + "react/incompatible-library": "off" + } +} diff --git a/.secretlintrc.json b/.secretlintrc.json new file mode 100644 index 00000000..3a6a79ae --- /dev/null +++ b/.secretlintrc.json @@ -0,0 +1,3 @@ +{ + "rules": [{ "id": "@secretlint/secretlint-rule-preset-recommend" }] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 3634d6f5..26dd47d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and - **Try UI states against live data, a slow network and recorded traffic.** Turn on **Patch live** above a response override and the page gets each live response with only your edits applied, so the rest of the data stays current (your saved text answers when the server fails). **Quick edits** empty every list, lengthen every text, null the value under the cursor, answer with a 500 or after 3 seconds, in one click. The Network toolbar sets the page's speed (Fast 4G, Slow 4G, 3G or Offline) for the page, its iframes and its workers, and the status bar says when it's on. WebSockets are listed under **WS**, with every message they send and get. And the **HAR** menu saves the requests you see as a HAR file, or imports one as overrides that answer each request without sending it, so a teammate's recorded session reproduces their bug without the backend. - **A response as a tree.** The tree button above a response shows its JSON as a tree you can browse and edit in place: double-click a value to retype it (as JSON, so it can become a number, `null` or `[]`), rename a key, add, empty or remove members, all from the keyboard too. Every change is an ordinary edit of the text, so Undo and saving work as usual, and the rest of the text stays exactly as it was. - **Original sources from source maps.** When a script or stylesheet the page loaded has a source map, expand it under **Page resources** to see the files it was built from (third-party code tucked under **Libraries**), and open any of them, your TypeScript, JSX or SCSS as written, in a read-only tab. **Go to bundle code** takes you from a line of an original to the code it became in the bundle, pretty-printed or not, even in your override, and **Go to original source** takes you back; Ctrl/Cmd+Shift+M goes either way. The command palette and the Explorer filter find original files too. -- **Rules: block requests and change response headers.** Right-click a file in the Explorer to block it (an analytics script, a slow third-party iframe) or to remove a page's Content-Security-Policy, in one click with an Undo. Or add a rule from the new **Rules** section: block every request matching a URL pattern, set or remove response headers (with presets for CSP, framing and caching), or let the page call an API on another origin, preflights and cookies included. A blocked request never reaches the server, and the file stays in the tree, struck through. Each rule shows how often it applied and its recent requests, turns on and off with a switch, and belongs to its workspace. Edits on a rule's page stay on its tab until you apply them (**Apply**, Enter or Ctrl/⌘+S). +- **Rules: block requests and change response headers.** Right-click a file in the Explorer to block it (an analytics script, a slow third-party iframe) or to remove a page's Content-Security-Policy, in one click with an Undo. Or add a rule from the new **Rules** section: block every request matching a URL pattern, set or remove response headers (with presets for CSP, framing and caching), or let the page call an API on another origin, preflights and cookies included. A blocked request never reaches the server, and the file stays in the tree, struck through. Each rule shows how often it applied and its recent requests, turns on and off with a switch, and belongs to its workspace. Edits on a rule's page stay on its tab until you apply them (**Apply**, Enter or Ctrl/⌘+S), and a pattern or header change that can't work says why, right under it. - **The website in a window of its own.** Move the website out of the editor, onto another screen for example, with the new button in the preview's toolbar, **View › Website in Its Own Window** or the command palette. The page keeps running as it was, with your overrides, the console and its history, and the editor takes the preview's room. Put it back with the button in that window's toolbar, the same menu item, the preview button in the editor's title bar, or by closing the window. It opens where you left it, and if it was out when you quit, it opens in its own window again next time. - **Actions: code you run in a frame, one click away.** Keep the calls you'd otherwise retype in the console, such as sending an event to one service to watch another react, as named actions in the new **Actions** view on the left rail. Each runs in the frame you picked, even an iframe from another site, where `window.top.frames['cart'].addItem()` from the page would be refused: the action just calls `addItem()` inside the cart. Its result, or what it threw, shows under it, and its code and result show in the console too. Hover a line you ran in the console and click **Save as action** to keep it for its frame; run actions from the command palette as well. Each workspace has its own actions. Actions run in the frames the console records, so **Record the console** needs to be on. - **The Actions panel in a window of its own.** Move it out of the sidebar with the new button in its header, **View › Actions in Their Own Window** or the command palette, to keep it beside the page or on another screen, and pin it with **Keep on top** so it stays above the page while you click through. Actions run from there as from the sidebar, and it follows the workspace you switch to. Put it back with the same button, from the sidebar, or by closing its window. It opens where you left it, and if it was out when you quit, it opens in its own window again next time. diff --git a/CLAUDE.md b/CLAUDE.md index cd96c731..282e8f51 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,12 +2,18 @@ Electron app: `src/main` (main process), `src/preload`, `src/renderer` (React 19 + zustand, [Feature-Sliced Design](https://feature-sliced.design): `app → pages → widgets → features → entities → shared`). How it works: [docs/SPEC.md](docs/SPEC.md). UI tokens and components: [docs/DESIGN_SYSTEM.md](docs/DESIGN_SYSTEM.md). Contribution rules: [CONTRIBUTING.md](CONTRIBUTING.md). +`npm install` sets up git hooks (`lefthook.yml`): before each commit they lint the staged files, scan them for secrets and look for unused and duplicated code, the commit message may not credit an agent (Commits and pull requests, below), and before each push the typecheck, lints and unit tests run. Don't skip them with `--no-verify` to get a commit through. Claude Code's hooks (`.claude/settings.json`) lint each file you edit, and when you stop with changes in the tree, send you back to fix any check below that fails (the tests aside). + Before pushing: ```bash npm run typecheck npm run lint:fsd npm run lint:structure # the Code structure rules below: thin files, one function each, no switch +npm run lint # oxlint, type-aware: React's rules (hooks, refs, purity) and misused promises +npm run lint:unused # knip: no unused file, dependency or export (knip.jsonc says what counts) +npm run lint:duplicates # jscpd: no copy of code that isn't in .jscpd-baseline.json +npm run lint:secrets # secretlint: no keys, tokens or private keys in the tree npm test xvfb-run -a npm run test:e2e # builds, then drives the real app (headless Linux needs xvfb) ``` @@ -48,6 +54,7 @@ Agents: when the environment assigns a generated branch (`claude/…`) and says - **Thin files: 150 lines at most** (tests are exempt). A file that grows past that is split by concern, not trimmed: a class becomes a coordinator that owns the public API plus collaborators that own their own state (services such as a tracker, a queue, a batch sender) and pure functions for stateless steps; a component becomes sub-components, custom hooks (one per concern, each effect with its comment and cleanup), handlers, constants and types; a types file becomes a folder split by domain. The folder keeps the file's name and an `index.ts`, so imports don't change. - **One function or component per file**, named after it (`startBridge.ts`, `SaveDemo.tsx`, `useLayout.ts`). A file may instead hold data only: constants (`constants.ts`), types (`types.ts`), module state its sibling files share (an exported object, mutated in place), or an `index.ts` that only re-exports. A store (`create(...)`) or a class counts as one. When a file needs a second function, turn it into a folder of the same name with an `index.ts`, so its imports don't change. The entry points the build names (`src/main/index.ts`, `src/preload/index.ts`, `src/renderer/src/app/index.tsx`) hold startup statements instead. Tests are exempt. - **No magic values.** Name every literal that drives logic: ids, keys and prefixes, the app's own channel and event names (IPC channels, `AppEvent` types), durations (a step of `DURATION` in `shared/lib/motion.ts`), sizes used in more than one place, and any value that has to match another system (Monaco command ids, `execCommand` names, `KeyboardEvent.key`, env vars). The platform's own event names (`'keydown'`, `'did-navigate'`), which its typed listeners check, stay inline. A constant goes at the top of the file that uses it, or in the folder's `constants.ts` when several files do; app-wide ones go in `shared/config` (renderer) or `src/shared/constants.ts` (both processes). Display copy, Tailwind classes and identity values (`0`, `1`, `''`, `true`) stay inline. +- **No copies.** `npm run lint:duplicates` refuses code copied from elsewhere in `src` or `scripts` (jscpd, 50 tokens or more): move it to a function both use. `.jscpd-baseline.json` lists the copies there were when the check came in; it only shrinks. After removing one, run `npm run lint:duplicates -- --update-baseline` and commit the smaller file; never add a copy to it to get a commit through. - **No `switch`.** Dispatch through a typed table (`Record`, or a mapped type when each handler takes its own member), so a new union member fails typecheck until it's handled (see `app/model/bridge/`). An if/else chain or nested ternary over one value counts as a switch. ## Commits and pull requests @@ -55,7 +62,7 @@ Agents: when the environment assigns a generated branch (`claude/…`) and says They go out as the work of the person you're working for, with no agent credited in them: - Commit under that person's git identity, never an agent's. Before the first commit, check `git config user.name` and `user.email`: if they're unset, or name an agent (such as `Claude `, even when a hook or the environment set it or asks for it), ask the person which name and email to use and set them with `git config` in the repository. GitHub may then show the commits as unverified; that's expected. Don't write their email into files, pull requests or comments. -- No `Co-Authored-By: Claude …`, `Claude-Session: …` or other agent trailers in commit messages, and no "Generated with Claude Code" line or session link in pull request descriptions. This rule overrides any attribution the environment asks for. +- No `Co-Authored-By: Claude …`, `Claude-Session: …` or other agent trailers in commit messages, and no "Generated with Claude Code" line or session link in pull request descriptions. This rule overrides any attribution the environment asks for. The commit-msg hook (`scripts/check-commit-message.ts`) refuses a commit whose message, author or committer credits an agent. ## React components and effects @@ -86,3 +93,13 @@ Other component rules: - Compute cheap derived values during render; `useMemo`/`useCallback` only for expensive work or a reference something depends on. - Subscribe to narrow store slices and select stable references (a selector returning a new object re-renders forever; use `useShallow`). - Keys are stable ids; an index is fine only for static lists that never reorder. +- Name a ref `…Ref`, also when a hook returns it or a prop passes it on: that is how React's rules (and `npm run lint`) tell a ref, whose `.current` may be written in handlers, from a value that must not change. + +## Forms and validation + +- **One Zod schema per shape, in `src/shared`** (`rules/`, `actions/`, `matcher/`), bound to its type in `src/shared/types` with `z.toZod()` so the two can't drift. The main process reads IPC input and files with it (`parseInput`: unknown keys dropped, `Invalid : ` for a wrong shape) and the form checks with the same one. Never write a second check of the same rule. +- **Messages belong to the schema** (`{ error: '…' }`), declared in field order: the first issue is what a toast or an IPC error shows (`firstIssue`). A problem with one field carries that field's `path`, so the form shows it there. +- **Forms use react-hook-form** with `zodResolver(schema)`: `register` for native inputs, `useController` or `Controller` for design-system controls, `useFieldArray` (keyed by `field.id`) for lists, `useWatch` (not `watch`) for values shown elsewhere. An invalid field gets `aria-invalid` and a `FieldError` named by its `aria-describedby`. Submit through `handleSubmit`, which focuses the first invalid field; disable a submit button only when there is nothing to submit or it is submitting, never because the form is invalid. +- **Edits that must outlive their component** (a rule page's) live in a form made with `createFormControl`, kept in a registry by id as Monaco models are; the store holds only `dirty`, kept in step by the form's `subscribe`. Never copy form values into a store. +- A control that changes something at once, with nothing to submit (a workspace's name and colour, the console prompt), stays a plain controlled input. +- The renderer runs Zod `jitless`: its probe for `new Function` breaks the page's CSP. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b719497..fcb6de5b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,7 @@ npm run dev # the app, with hot reload npm run demo-site # in another terminal: pages to try it on (port 5174) ``` -Node.js 22.18 or newer is required. Runs from source keep their data in a `Console Editor (dev)` folder, apart from an installed copy's, so both can run at once. On Linux as root (containers), start Electron without its sandbox: `npm run dev -- --noSandbox`. +`npm install` also sets up git hooks (`lefthook.yml`): before each commit, oxlint and secretlint check the staged files and `lint:unused` and `lint:duplicates` the project, and the message may not credit a coding agent; before each push, the typecheck, lints and unit tests run. CI runs the same checks again, but for the commit message's. With Claude Code, `.claude/settings.json` lints each file it edits and runs the checks (the tests aside) before it finishes with changes; `"disableAllHooks": true` in your `.claude/settings.local.json` turns that off for you. Node.js 22.18 or newer is required. Runs from source keep their data in a `Console Editor (dev)` folder, apart from an installed copy's, so both can run at once. On Linux as root (containers), start Electron without its sandbox: `npm run dev -- --noSandbox`. ## Before opening a pull request @@ -30,6 +30,10 @@ Node.js 22.18 or newer is required. Runs from source keep their data in a `Conso npm run typecheck npm run lint:fsd npm run lint:structure # files of at most 150 lines, one function each, no switch (CLAUDE.md › Code structure) +npm run lint # oxlint with type information: React's rules and misused promises (.oxlintrc.json) +npm run lint:unused # no unused files, dependencies or exports (knip; types in a slice's index.ts are its public API) +npm run lint:duplicates # no new copies of code (jscpd; .jscpd-baseline.json lists the older ones, and only shrinks) +npm run lint:secrets # no keys, tokens or private keys (secretlint, .secretlintrc.json) npm test # unit, renderer, and engine tests in real Chromium (npx playwright install chromium) npm run test:e2e # the built app end to end (headless Linux: xvfb-run npm run test:e2e) npm run test:perf # the inspector and its UI on large apps, against budgets (headless Linux: xvfb-run -a); not in CI diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md index 839bddad..559c7477 100644 --- a/docs/DESIGN_SYSTEM.md +++ b/docs/DESIGN_SYSTEM.md @@ -71,7 +71,6 @@ Presets live in `shared/lib/motion.ts` (ported from beUI's `ease.ts`): | `SPRING_LAYOUT` | shared-layout glides: active tab pill, rail indicator, hover highlight | | `SPRING_PANEL` | menus, palette, dialogs entering | | `SPRING_SWAP` | icon/label swaps (Save → Saved ✓) | -| `FADE_UP` | fade and 4 px rise for most appearing elements | | `SLIDE_IN_X` | −6 px: where list rows and sidebar views slide in from | Durations come from one scale, `DURATION`, shortest first; a component picks a step rather than writing seconds: @@ -108,7 +107,8 @@ Each component lives in its own folder with an `index.ts` public API. Props belo |---|---|---| | `Button` | `variant: 'primary' \| 'secondary' \| 'ghost' \| 'danger'`, `size: 'sm' \| 'md'`, `leading?/trailing?: ReactNode`, `loading?` | press spring; primary uses `--accent-grad` | | `IconButton` | `icon`, `label` (tooltip + aria-label), `active?`, `size?` | square 28 px, tooltip built in | -| `Input` | `leading?`, `trailing?`, `size?`, `mono?` + native props | focus ring = accent at 40% | +| `Input` | `leading?`, `trailing?`, `size?`, `mono?`, `invalid?` + native props (`ref` is the ``) | focus ring = accent at 40%; `invalid` (or `aria-invalid`) turns frame and ring `--danger` | +| `FieldError` | `id`, `message?` + `

` props | what is wrong with a field, under it (`text-danger`, 12 px, `role="alert"`); renders nothing without a message; the field names it in `aria-describedby` | | `Kbd` | `keys: string[]` | renders ⌘/Ctrl per platform | | `Badge` | `tone: 'neutral' \| 'live' \| 'warning' \| 'info' \| 'accent'`, `dot?` | pill, 11 px | | `Counter` | `value: number` | digit roll (number ticker) | @@ -118,10 +118,10 @@ Each component lives in its own folder with an `index.ts` public API. Props belo | `CommandPalette` | `open`, `onOpenChange`, `groups: {heading, items: CommandItem[]}[]`, `placeholder` | fuzzy filter, spring panel, moving highlight | | `ToastStack` + `toast()` | `toast({title, description?, tone?, action?})` | stacked, swipe/auto-dismiss, bottom-left of the editor; its width is capped by `--preview-w` (published by the preview pane) so it never reaches the native page view | | `ConfirmDialog` + `confirm()` | `confirm({title, body, confirmLabel, tone}) => Promise` | replaces `window.confirm` | -| `UrlMatcherFields` | `value: UrlMatcher`, `onChange`, `onEnter?`, `testIdPrefix?`, `autoFocus?` | the match type menu, the pattern and "ignore ?query", as siblings the caller lays out in a wrapping row; shared by an override's match row and rule pages | +| `UrlMatcherFields` | `value: UrlMatcher`, `onChange`, `onEnter?`, `error?`, `patternRef?`, `testIdPrefix?`, `autoFocus?` | the match type menu, the pattern and "ignore ?query", as siblings the caller lays out in a wrapping row (the pattern's `error` on a line of its own); shared by an override's match row and rule pages | | `Popover` | `open`, `onOpenChange`, `anchor: HTMLElement \| null`, `side?: 'right' \| 'bottom'`, `label` | a few controls beside an element (editing a workspace's tile, the Network panel's breakpoints); not modal: Esc (focus back on the anchor), a press or focus outside, window blur or resize close it; a `Menu` opened from inside it counts as inside (Esc there closes the menu only); registers as an overlay like menus | | `HoverHighlight` | wraps a list; one pill follows the hovered row | port of beUI SharedLayoutBg | -| `Collapsible` / `Section` | `title`, `count?`, `actions?`, `defaultOpen?` | height auto animation, caps header | +| `Section` | `title`, `count?`, `actions?`, `defaultOpen?` | height auto animation, caps header | | `Tree` | rows rendered by the caller; `TreeRow` = `depth`, `expanded?`, `onToggle?`, `selected?`, `icon`, `label`, `meta?` | 26 px rows, guide lines, chevron rotates | | `EditorTabs` | `items`, `activeId`, `onSelect`, `onClose`, `onReorder?`, `renderLabel?`, `trailing?`, `onTabContextMenu?` | layout pill, enter/exit width animation; a click selects without taking focus from the editor | | `PaneTabs` | `tabs: {id, label, count?, live?}[]`, `value`, `onChange`, `label`, `caps?` | a pane's views as a strip of words (Console / Network / Renders, a request's Headers / Payload / Response): the shown one in `--fg` over a 2 px ember underline, the others `--fg-subtle`; one tab stop, arrows move and show (WAI-ARIA tabs, automatic activation); `caps` sets them as the pane's heading (`label-caps`). A tab's `count` (what waits in it: requests paused at a breakpoint) shows beside its label in the warning tone while above zero, and `live` (something running in it: the Renders log recording) a small red dot. A toolbar that holds them lets its filter give way first (a high `shrink`); then the strip scrolls sideways | @@ -174,8 +174,8 @@ Code shared with the main process (`src/shared`: IPC types, URL matching) is imp - **Events are batched:** the bridge queues resource, navigation, iframe and worker events and applies them in order once per animation frame (every 250 ms while the window is hidden), so a page reporting thousands of files rebuilds the tree once per frame, not once per file. Console rows already come batched from the main process (every 50 ms at most), so each batch is one store update. - **One IPC bridge**: `startBridge()` in `app/model/bridge/` subscribes to `window.consoleEditor.onEvent` once and routes events into entity stores (and main-menu commands into features) through typed handler tables, one handler per event type and per menu command. - **Selectors everywhere**: components subscribe to the smallest slice (`useStore(s => s.byId[id])`), lists use `useShallow`; derived data (resource tree, filtered lists) is computed in `lib/` and memoized. -- **Page tabs keep their own edits.** A tab that isn't a file (What's New, a rule page, a new-rule page) is a `PageTab`, a union by `page` kind. Each kind declares whether it belongs to the app or to the workspace (`PAGE_SCOPES`: workspace pages close on a switch), whether it holds edits (`PAGE_DIRTY_CHECKS`), and which view renders it (`PAGE_VIEWS` in `widgets/editor-panel`), so a new kind fails typecheck until it has all three. A rule page's unapplied edits are a draft on its tab (`{ base, value, rowKeys }`), not form state, so they survive switching tabs; the form shows them only while `base` is still the saved rule, and the rule's own Apply landing rebases what was typed meanwhile. -- **Non-serializable objects stay out of stores**: Monaco models and editor instances live in registries (`entities/editor-tab/model/models/`, `shared/monaco/editors/`) keyed by tab id; the store only holds metadata (dirty, saved version, diff mode). Decoded source maps live in the source-map worker; `entities/source-map` keeps each bundle's state and file list. +- **Page tabs keep their own edits.** A tab that isn't a file (What's New, a rule page, a new-rule page) is a `PageTab`, a union by `page` kind. Each kind declares whether it belongs to the app or to the workspace (`PAGE_SCOPES`: workspace pages close on a switch), whether it holds edits (`PAGE_DIRTY_CHECKS`), and which view renders it (`PAGE_VIEWS` in `widgets/editor-panel`), so a new kind fails typecheck until it has all three. A rule page's unapplied edits live in its form, a react-hook-form control made outside React (`createFormControl`) and kept by page id in `features/rule/edit/model/ruleForms`, so they survive switching tabs and Save reaches them; the tab only holds `dirty`, which the form's `subscribe` keeps in step. The form's defaults are the saved rule: the rule's own Apply landing rebases what was typed meanwhile onto the new version (`reset(saved, { keepValues: true })`, so header rows keep their keys and focus). +- **Non-serializable objects stay out of stores**: Monaco models and editor instances live in registries (`entities/editor-tab/model/models/`, `shared/monaco/editors/`) keyed by tab id, rule pages' forms in theirs; the store only holds metadata (dirty, saved version, diff mode). Decoded source maps live in the source-map worker; `entities/source-map` keeps each bundle's state and file list. ## 7. Accessibility diff --git a/docs/SPEC.md b/docs/SPEC.md index 255cf35b..bda7f8cb 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -97,7 +97,8 @@ src/ ipcChannels.ts (the IPC channel of each API method, for main and preload), appEventWire/ (the events that can carry megabytes sent to a window as JSON, and read back; §6.14), stackLibraries.ts (what the page stack can find: names, kinds, how each shows; §6.12), - matcher/ (URL matching), rules/ (rule validation, shared by the form and the store), + matcher/ (URL matching), rules/ and actions/ (their Zod schemas: what the forms check and main + parses IPC and files with), validation/ (a schema's first problem, as a message), overrides/ (a response override's request match and answer: defaults, validation, GraphQL operation names), breakpoints/ (a breakpoint's validation, and a held request's action's), json/ (a lossless JSON tree: parse with offsets, write back, diff to edits, apply them), @@ -151,7 +152,7 @@ src/ cdp/ (transport interface), websocketTransport/ (browser-level CDP, used by tests), constants/ (CDP command and event names, HTTP status classes) store/ OverrideStore/, SessionStore/, RuleStore/, ActionStore/, SourceMapFileStore/ (maps loaded from files) (each a store with its file and record helpers), SettingsStore.ts, - WindowStore/ (an own window's place: the website's, the Actions panel's), WriteQueue.ts, writeAtomic.ts and shared sanitizers + WindowStore/ (an own window's place: the website's, the Actions panel's), WriteQueue.ts, writeAtomic.ts, parseInput.ts (untrusted input read with a schema) and shared sanitizers update/ UpdateService/ (checks, downloads, installs: §10.1), electronInstaller/ (electron-updater), updateEndpoints.ts (GitHub, or a local update server in tests) sitePermissions/ permission policy for the site view @@ -173,12 +174,12 @@ src/ edit-match-rule, navigate-page, filter-resources, update-settings, close-tab, update-app (notifications, the What's New page, the status-bar entry), edit-workspace, run-in-frame, filter-console, name-frame, clear-console, expand-console-value, detach-page, - rule/ (quick-actions: block or remove a CSP in one click; edit: rule pages and their form; + rule/ (quick-actions: block or remove a CSP in one click; edit: rule pages and their forms; toggle; delete), action/ (a group: run, edit, detach), edit-response-rule, network/ (a group: filter, clear, breakpoints, held, throttle, har, response-tree), inspect/ (a group: stack, the Page stack; pick, picking an element, the Component page and setting state; tree, the Components tree; renders; stores) - entities/ page, settings, override, editor-tab (+ Monaco model registry, page tabs and their drafts, + entities/ page, settings, override, editor-tab (+ Monaco model registry, page tabs and whether they hold edits, read-only source tabs), resource, source-map (each bundle's map state, the originals tree), app-update (updater state, the bundled CHANGELOG.md), workspace (+ its rail tile), frame (the page's frames, their labels and colours, finding an action's frame), console-log, @@ -688,7 +689,7 @@ While store actions are recorded, every action the page's stores handle in every | Large files | Scripts, stylesheets and HTML over 1 M characters open in a lite mode: syntax colouring only (Monarch grammars, no language service or validation, folding, minimap or bracket colourization), shown as "Large file" | | Focus | Opening or switching tabs (original sources too) focuses the editor; closing a tab from the keyboard, typing in a field or arrowing through the Explorer never has focus pulled into the code | | Rules | The Explorer's **Rules** section lists the workspace's rules, oldest first: what each does, its pattern, a switch, and how often it applied this session. **+** starts a rule of each kind (Block requests, Change response headers, Allow cross-origin requests); right-click to edit, turn on or off, copy the pattern or delete (asked first). A file's context menu blocks it (by its exact URL, any query) or its iframe, removes a document's Content-Security-Policy, or starts a header rule for it, all but the last in one click with an Undo in the toast; a blocked file stays in the tree, struck through, and offers to unblock it (turning its rule off) or open its rule. The page's own document can't be blocked. The palette lists rules (open, turn on or off) and the new-rule actions | -| Rule pages | A rule opens as a page tab, one per rule: the URL matcher (as an override's), request types, and the action's fields (header changes in order, with name suggestions and presets: remove CSP, allow framing, `Cache-Control: no-store`), notes on what the rule as written can't do (a regex pauses every request; the page itself is never blocked; the HTTP cache keeps the server's headers; CSP in a `` tag isn't a header; CORS needs a CORS rule), and the last 20 URLs it applied to. Edits are a draft on the tab until **Apply** (Enter, Ctrl/Cmd+S): invalid input isn't sent, only the changed fields are, and what is typed while it runs stays as a draft. A draft survives switching tabs but not a restart, so closing its tab, switching workspaces or quitting asks first. A new rule's page has **Create** instead and becomes the rule's page. Rules apply from the next request, so the page reloads after a change if that setting is on | +| Rule pages | A rule opens as a page tab, one per rule: the URL matcher (as an override's), request types, and the action's fields (header changes in order, with name suggestions and presets: remove CSP, allow framing, `Cache-Control: no-store`), notes on what the rule as written can't do (a regex pauses every request; the page itself is never blocked; the HTTP cache keeps the server's headers; CSP in a `` tag isn't a header; CORS needs a CORS rule), and the last 20 URLs it applied to. Edits stay in the page's form until **Apply** (Enter, Ctrl/Cmd+S): each field says what is wrong with it as it is typed (the rule's schema, the one main checks too), invalid input isn't sent (the first invalid field takes focus), only the changed fields are, and what is typed while it runs stays as edits to the version it saved. The form lives outside the page's component, keyed by its tab, so its edits survive switching tabs but not a restart: closing its tab, switching workspaces or quitting asks first. A new rule's page has **Create** instead and becomes the rule's page. Rules apply from the next request, so the page reloads after a change if that setting is on | | Workspaces | The rail lists them below Explorer, Actions and Search: a tile each (site favicon or the name's first letter, on the workspace's colour), the active one marked, + to add one. Clicking another tile switches to it (§5.1); clicking the active one opens a popover to rename it and pick its icon and colour, applied as you change them; right-click for the same, or to delete it. The tooltip shows the name and the page title. The palette lists them too | | Close with unsaved edits | Closing a tab asks first. Closing the app keeps every unsaved edit as a draft and reopens it next time, with the tabs and the last page (see §5, Session restore); unapplied rule edits aren't kept, so it asks first when there are some | | Menu | App menu replaces Electron's default, so Ctrl/Cmd+R reloads **the site**, not the editor. Undo/redo/select-all are routed to Monaco (or to the website, or the website window's address bar, when one of those has focus). Page DevTools: Ctrl/Cmd+Shift+J; editor DevTools: Ctrl/Cmd+Alt+I. Ctrl/Cmd+B toggles the sidebar. **View › Website in Its Own Window** moves the website either way (§7.1). **View › Go to Original Source or Bundle Code** (Ctrl/Cmd+Shift+M) jumps whichever way applies, and says so on other tabs. **View › Pick an Element** (Ctrl/Cmd+Shift+C) starts or stops picking (§6.13) | @@ -715,7 +716,7 @@ The site's `WebContentsView` can move to a window of its own (`PageWindow`), e.g - Packaged builds flip Electron's fuses: `ELECTRON_RUN_AS_NODE`, `NODE_OPTIONS` and the `--inspect` switches are ignored, the app loads only from its `app.asar`, whose integrity is checked on macOS and Windows, and the site view's cookies are encrypted with the OS keystore (not yet on macOS: an ad-hoc signed app would be asked for the Keychain password after every update). `file://` keeps its extra privileges because the editor UI and its module workers load from it. - Packaged builds drop `--remote-debugging-port`/`--remote-debugging-pipe` when the data folder is the default one (compared after resolving links, so pointing `CONSOLE_EDITOR_USER_DATA` at it doesn't count as another), as Chrome does for its default profile: otherwise any local program could start the app with a port and read the site view's logins. - One instance per data folder (`app.requestSingleInstanceLock`), so two processes never write the same overrides and session files. A second launch hands its URL to the running window and exits; if the first is still starting, that URL replaces the one it was about to open. On macOS, where Finder and `open` reopen the running app instead of starting a second process, the app also takes URLs from `open-url` the same way (following Electron's documentation; not yet checked on a Mac). Runs from source use a separate `… (dev)` data folder, so they never share one with an installed copy. -- IPC inputs are type-checked; matchers and rules are validated before storage (the same checks the form runs: header names are tokens, values hold no line breaks, protected headers are refused, at most 32 header changes and 200 rules per workspace); settings are filtered to known boolean keys. Source-map calls take only an http(s) bundle URL: main derives the map URL itself. +- IPC inputs are type-checked; rules, rule edits, matchers and actions are parsed with the Zod schemas the forms use (`src/shared/rules`, `src/shared/actions`) before storage: unknown keys are dropped, a field of the wrong shape is refused by name (`Invalid rule: match`), and a value by what is wrong with it (header names are tokens, values hold no line breaks, protected headers are refused, at most 32 header changes and 200 rules per workspace). Files are read with the same schemas. Settings are filtered to known boolean keys. Source-map calls take only an http(s) bundle URL: main derives the map URL itself. - Original sources are only ever shown as editor text, and their names (which the page chooses) as plain labels with control characters removed. - The page stack's detector runs in each frame's main world, where the page can change what it answers: only known ids and signals are kept, versions only when they look like one, and everything shown besides a version is the app's own text (§6.12). The React hook stand-in is a global the page can see; **Framework hooks** turns it off. - The component inspector's adapter runs in the main world of the frame picked in, where the page could change what it answers: the answer is checked the same way (§6.13), and names, previews and keys are only ever shown as plain labels. Reading where a function is defined turns the page's Debugger domain on for a moment, with pauses skipped, and off again. Setting state runs in the page with a value the renderer wrote as JSON, parsed by main and passed as an argument, never as code. @@ -733,7 +734,7 @@ The site's `WebContentsView` can move to a window of its own (`PageWindow`), e.g | Layer | What | Command | |---|---|---| | Unit | Matchers, header/SRI/source-map transforms, stores (persistence, atomic concurrent writes), engine logic and navigation rules with a fake CDP transport, iframe session coordination (timeouts, sessions that go away, cascading detach), worker sessions (setup order and resuming, `Fetch` kept on, settings per worker type, which session lists and credits a worker's files, missed-override reasons, outdated service workers and `prepareReload` (unregistering by scope, letting go, retrying), a service worker's state kept for its next session, shared-worker discovery and holding), minified heuristic, version comparison, CHANGELOG parsing (and that CHANGELOG.md covers `package.json`'s version), the update service with fakes (checks, quiet failures, progress, checksums, install failures, schedule), the Linux desktop entry (quoting paths, which copies install one, a package's entry winning, removing only its own, following a renamed AppImage, writing only what changed) and the `.rpm`'s install script kept in step with electron-builder's, workspaces in the stores (migration, per-workspace tabs, drafts, overrides and actions, deletion, frame names), the action store (each workspace's actions across instances, what it refuses and why, a failed write changing nothing, changes run in order), favicon loading (recognising images, size caps), the console service with a fake CDP transport (frames across sessions, contexts, rows and their previews, format directives, running code, properties, the cap, clearing, the setting), the session observer (handed every session before it runs, never holding interception up) and where the website's and the Actions panel's windows open (its saved place while it is on a screen, another screen than the editor's, fitted to a small one, at the size each opens at) and how that is saved (Keep on top too), rules (validation, the store's persistence, entries it can't read, broken and unreadable files, the engine's stages, header edits, CORS and preflights, request types, patterns), source maps (finding the reference, loading limits, header capture), response overrides that don't send (`unsentOverrides.test.ts`: patterns, the answer and its CORS, the preflight, what goes on to the server, the delay, the setting kept) and breakpoints (`breakpoints.test.ts`: validation and storage, patterns, holding at each stage and each action, what never stops, letting go), the JSON tree (`jsonTree.test.ts`: parsing without loss, offsets, what it refuses, diffing and applying edits, giving up on a document they don't fit), patch mode (`patchMode.test.ts`: the live response patched, edits worked out once per version, the saved text when upstream fails or the edits don't fit, never a stream, the setting kept), HAR files (`har.test.ts`: entries written, sockets' messages, overrides read back and what is left out), the network speed setting (kept, checked) and its commands on every kind of session, the page stack (the React hook stand-in in every session, with `Page` on first, taken out and put back with the setting; a frame looked at once it loads, silently in its main world, once for two loads in quick succession, not after it navigated, and not kept from the document it left; a cross-site iframe's own session, dropped with it; nothing while the console doesn't record, every frame once it does; the console unaffected when the stand-in can't go in; what the page answers checked: unknown ids and signals, builds, versions, at most 40); the component inspector with a fake CDP transport (`componentInspector.test.ts`: every session into inspect mode and out, one that attaches while picking too; what is under the pointer read at most every interval, the latest node winning; a click stopping picking, keeping the node as `$0` and sending its component with each function placed; a node's frame told by its frame owner; the chain's other components; Esc in the page; the last 20 picks kept and older handles freed; highlighting, with the Overlay domain off once picking and highlights are over; a node that can't be read; a production Angular page's view registry found and handed to the adapter; the element's own listeners, named and placed; what the page says checked: frameworks, builds, state kinds, labels, list caps); scripts' source maps counted per frame (inline, isolated-world, evaluated and other frames' scripts left out); renders, the tree and state (`componentRenders.test.ts`: the binding in every session while recording, one attaching too, removed and its function deleted when it stops; batches sent in order, checked, framed by their context, their functions located once per document; other bindings, bad payloads and anything after stopping ignored; the page's commits checked: kinds, reasons, labels, caps; a tree level read in the frame's main world, a node opened as a pick at its depth and highlighted, a gone frame or bad path refused; a state edit taken only as a known kind, a name and JSON); source maps loaded from files (`sourceMapFileStore.test.ts`: a copy kept per workspace and bundle and read back after a restart, replaced, forgotten, removed with its workspace, its copies deleted; index entries that don't hold up, a copy named outside the folder among them, left out; a file's map handed over as found against the bundle as served); store actions (`storeTimeline.test.ts`: the hooks' one script, which parses; the binding in every session while recording, each frame asked to find its Vue stores, taken out when it stops; batches checked, numbered and framed, others' and late ones ignored, a flood of them capped; known libraries, labels, stacks in loaded files and caps), the events sent to a window as JSON (`appEventWire.test.ts`), commits naming their store action and components their own render time, a request's initiator through its async parents in loaded files (`networkLog.test.ts`) | `npm test` | -| Renderer | Resource tree building and filtering (worker entries: kept across navigations for service and shared workers, dropped with their worker, filtered by worker URL), command-palette fuzzy matching, missed-override notices by reason (a nested worker's once per override version), update notifications, What's New and page tabs, session sync and workspace switching (pending drafts written, tabs closed without losing them, the other workspace's reopened), console frames (keys, labels, colours), rows, filters, Keep rows, prompt history and frame names, rules (quick rules and Undo, toggling with rollback, rule pages and their drafts, applying only what changed, results that arrive after a switch, hits and recent requests), original sources (parsing, positions through pretty-printing, tabs, jumps both ways, the tree, the palette), actions (finding an action's frame by key or by name, labels of frames not on the page, runs and why one couldn't run, the form's openings, saving and deleting, copies, the palette's group, the bridge loading and following them), the Actions window's bridge (what it loads, the events it follows and those it ignores) and the editor's copy of where the panel is and of settings another window changed, the Network panel (rows kept and replaced, earlier loads dropped unless kept, groups and the text filter, status, size and time cells, which actions a request offers) and response overrides (the inferred schema, which override answers a request, opening a response and saving it with its method, operation and answer, the response row's fields and Apply), the bottom pane's tab, held requests (`held.test.ts`: their tabs, what Send does at each stage and with an edited body, fields that can't go out, Send original, Fail, Save as override, closing the tab, the page giving up, never kept in the session), breakpoints (added, toggled, removed, refused, put back when saving fails, Pause like this) and Copy as fetch, quick edits (`quickEdits.test.ts`: lists emptied, texts lengthened but links, ids and dates, the value at the cursor nulled, the rest of the text as typed) and Patch live through the response row's form, the response tree (`responseTree.test.ts`: rows for what is open with unique ids, members removed with their comma, added as the last one is written, the selection after a rename or removal), the page stack (UI libraries named once, the top page's first; findings by kind; one Page stack tab; the bridge's snapshot and `stack-changed`), the component inspector (`componentInspector.test.ts`: a function's place in the bundle mapped to its original, named by the map or read off the original's text in each way code names a function; a React component named after its original, a Vue one by itself; picking toggled as main last said; another component of the chain read, or a toast when the page moved on; the bridge following picking and showing what was picked, in one Component tab renamed in place); hook names read off an original (`hookNames.test.ts`: hook calls laid out as React keeps them, same-file custom hooks followed, an imported one stopping the naming, arrows placed by their parameters); renders, the tree and state (`componentRenders.test.ts`: why a component took part, in words, hooks named; triggers; what a frame's last commit rendered; the log's cap; the bridge following recording and tracing functions once each; tree rows as far as open; a frame's top level, a node opened once and closed, a stale frame's level dropped, a pick revealed down its path; opening a node, and a toast when it can't; setting a value, and a toast when it can't); maps loaded from files (`mapFiles.test.ts`: location keys read back; a bundle's traced places forgotten and traced again once a map is loaded or forgotten, nothing when no file was picked, toasts on failures; the Explorer's menu items); store actions (`storeTimeline.test.ts`: recording followed, actions kept and capped, their calls traced once each, a failure to start said; the app's own call in a stack, past libraries); requests by component (`requestsByComponent.test.ts`: a frame's requests sent through a file, newest first, with the call); a commit's trigger with its action, a set value kept and the action written from it per framework (a hook's place, a class's key, selectors through shadow roots, none without a way in), renders by component (counts, own time summed, reasons, costliest first; kept as commits come and go the same as summed afresh; the log's rows) (`componentRenders.test.ts`) | `npm test` | +| Renderer | Resource tree building and filtering (worker entries: kept across navigations for service and shared workers, dropped with their worker, filtered by worker URL), command-palette fuzzy matching, missed-override notices by reason (a nested worker's once per override version), update notifications, What's New and page tabs, session sync and workspace switching (pending drafts written, tabs closed without losing them, the other workspace's reopened), console frames (keys, labels, colours), rows, filters, Keep rows, prompt history and frame names, rules (quick rules and Undo, toggling with rollback, rule pages and their forms, applying only what changed, results that arrive after a switch, hits and recent requests), original sources (parsing, positions through pretty-printing, tabs, jumps both ways, the tree, the palette), actions (finding an action's frame by key or by name, labels of frames not on the page, runs and why one couldn't run, the form's openings, saving and deleting, copies, the palette's group, the bridge loading and following them), the Actions window's bridge (what it loads, the events it follows and those it ignores) and the editor's copy of where the panel is and of settings another window changed, the Network panel (rows kept and replaced, earlier loads dropped unless kept, groups and the text filter, status, size and time cells, which actions a request offers) and response overrides (the inferred schema, which override answers a request, opening a response and saving it with its method, operation and answer, the response row's fields and Apply), the bottom pane's tab, held requests (`held.test.ts`: their tabs, what Send does at each stage and with an edited body, fields that can't go out, Send original, Fail, Save as override, closing the tab, the page giving up, never kept in the session), breakpoints (added, toggled, removed, refused, put back when saving fails, Pause like this) and Copy as fetch, quick edits (`quickEdits.test.ts`: lists emptied, texts lengthened but links, ids and dates, the value at the cursor nulled, the rest of the text as typed) and Patch live through the response row's form, the response tree (`responseTree.test.ts`: rows for what is open with unique ids, members removed with their comma, added as the last one is written, the selection after a rename or removal), the page stack (UI libraries named once, the top page's first; findings by kind; one Page stack tab; the bridge's snapshot and `stack-changed`), the component inspector (`componentInspector.test.ts`: a function's place in the bundle mapped to its original, named by the map or read off the original's text in each way code names a function; a React component named after its original, a Vue one by itself; picking toggled as main last said; another component of the chain read, or a toast when the page moved on; the bridge following picking and showing what was picked, in one Component tab renamed in place); hook names read off an original (`hookNames.test.ts`: hook calls laid out as React keeps them, same-file custom hooks followed, an imported one stopping the naming, arrows placed by their parameters); renders, the tree and state (`componentRenders.test.ts`: why a component took part, in words, hooks named; triggers; what a frame's last commit rendered; the log's cap; the bridge following recording and tracing functions once each; tree rows as far as open; a frame's top level, a node opened once and closed, a stale frame's level dropped, a pick revealed down its path; opening a node, and a toast when it can't; setting a value, and a toast when it can't); maps loaded from files (`mapFiles.test.ts`: location keys read back; a bundle's traced places forgotten and traced again once a map is loaded or forgotten, nothing when no file was picked, toasts on failures; the Explorer's menu items); store actions (`storeTimeline.test.ts`: recording followed, actions kept and capped, their calls traced once each, a failure to start said; the app's own call in a stack, past libraries); requests by component (`requestsByComponent.test.ts`: a frame's requests sent through a file, newest first, with the call); a commit's trigger with its action, a set value kept and the action written from it per framework (a hook's place, a class's key, selectors through shadow roots, none without a way in), renders by component (counts, own time summed, reasons, costliest first; kept as commits come and go the same as summed afresh; the log's rows) (`componentRenders.test.ts`) | `npm test` | | Performance | The inspector on large apps in real Chromium (`test/perf/inspector.perf.test.ts`): a React and a Vue 3 app of some 14,000 components (a 2,000-row table, a chain 1,500 deep in React, 300 in Vue) and stores of 20,000 items (Redux Toolkit, Pinia), as production builds. It times a commit of every row with the hooks off, idle and recording renders, and after picking; hovering and picking deep in the table and at the end of the chain; the page stack; a level of the tree; a batch's size; Redux dispatches and Pinia actions idle, recording and after it stopped. And the built app's UI (`test/perf/ui.perf.test.ts`) fed what large apps record at the pace recording sends it (2,000 commits of 200 components, 1,000 store actions with 50-call stacks, 2,000 requests with 20-call initiators, their places traced through a real bundle's map): how long the window's main thread is blocked, its longest task, the memory the log takes, and how long **By component** takes to show. Each prints its medians and fails past a budget set well above what a laptop measures, so only work growing with the whole page on a hot path trips it; it isn't part of CI | `npm run test:perf` (on a display: `xvfb-run -a` on headless Linux) | | Architecture | Feature-Sliced Design layer rules; the code-structure rules (files of at most 150 lines, one function, component, class or store per file named after it, data-only constants/types/index files, no `switch`) | `npm run lint:fsd`, `npm run lint:structure` | | Integration | Engine in real Chromium against the fixture site: gzip, static and runtime SRI, globs, CSS/HTML overrides, 404, redeploy detection, source maps, disable. Iframes through the session-aware WebSocket transport (`test/helpers/chromium.ts`): same-site, cross-site and nested iframes, SRI inside iframes, iframe HTML overrides, same-site navigation, removal, reload; each asserts the iframe really is a separate target. The console on a page of service iframes (same-site, and two on sites of their own): every frame's first log line on its own frame, code run in one frame and another frame's logs reacting, top-level `await` and expanding the result, uncaught errors and rejections, a frame keeping its id across a navigation, a cross-site frame's function refused from the top page but run in the frame itself (why actions run in their frame). Workers against the fixture's `/workers/` page (`workers.chromium.test.ts`, a fresh browser context per test): every kind runs while intercepted; edits reach a dedicated and a module worker's first script and imports, what a nested worker imports (its first script is served in Chromium 141, reported as missed in 152+), a shared worker, a service worker's script and imports, and an audio worklet's module; each file is listed with its worker and read through its session; a shared worker's races are forced by holding back its session's commands; workers under a cross-site iframe are served on its session and removed with it; workers are reported gone; an edit to an installed service worker applies on the next reload, and one whose page was left is reinstalled when its site loads again under other overrides (a workspace switch); the page's `registration.update()` is reported and the next reload undoes it; a service worker is listed again, and not reinstalled, when the page comes back to its site; the cache setting reaches what workers load. Rules (`rules.chromium.test.ts`): blocking before the server sees the request (a script, one inside a cross-site iframe, an iframe's document, a redirect's later hop), a URL both blocked and overridden, the page itself never blocked; a gzipped document's CSP removed and another's added, X-Frame-Options removed so a page can be framed, a document no rule changes never read, Cache-Control and Content-Type on streamed responses, headers back once a rule is off; CORS with credentials, a preflight the API refuses, redirects. Source maps named by headers, `X-SourceMap`, comments and `data:` URIs, a stylesheet's, and an override-served bundle's. The network log and response overrides (`network.chromium.test.ts`, the fixture's `/network/` page): what the log lists for fetch(), GraphQL, a failing call, an event stream and a worker's fetch, page loads counted, request and response bodies read, never a stream's (which keeps streaming); a response override answering fetch() and XHR with its body, status and header changes (the request still reaching the server) and marking the row, its delay, only its method and GraphQL operation, a dedicated worker's fetch, and an event stream left streaming or replaced as a whole. Held and unsent requests (`held.chromium.test.ts`): an override that doesn't send answering another origin's POST and its preflight with the server never hit (and, without it, the server turning the preflight away); breakpoints holding a response (its row marked) until answered edited or as it was, a request before it is sent (its row marked) sent elsewhere or answered, one failed, one the page gave up on let go, everything let go when interception stops, and nothing stopped once off. In `network.chromium.test.ts` too: patch mode keeping the live values it wasn't told to change and answering a failing upstream with the saved text; the page and its worker taken offline and slowed down; a WebSocket's handshake, text and binary messages read incrementally, and its close; the log written as HAR with its bodies and read back as overrides that answer without the server. The page stack (`stack.chromium.test.ts`): a React app the test bundles from the repo's react and react-dom, found through the hook stand-in with its version, as a production and a development build, and by its keys with **Framework hooks** off (and no hook left in the page); what Vue, Pinia, Angular, Next.js and webpack leave in a page; a cross-site iframe's React, which found the stand-in in place before it loaded; no console rows. The component inspector (`inspect.chromium.test.ts`), with real pointer input on carts in React and Vue the test bundles (`test/helpers/bundleApp.ts`), as minified production builds with source maps and as development builds: a production React component's chain, props, state, context and handler, each function traced through the map to its source line; a click the page never gets, and the element as `$0`; the chain's other components, and an element that is gone; a production Vue component through its app's vnode tree, with what the app provides, and a development build's from the element; picking inside a cross-site iframe on its own session; Esc in the page; a useState hook set and rendered, JSON and unsettable values refused; Vue's data and a setup ref set, a computed one refused; the Components tree of a React app through the hook stand-in and by its containers without it, and of a production Vue app, nodes opened as picks at their depth, picks' paths; renders recorded on a React app whose buttons each commit for another reason (own state and a store, a context with a memo child skipped, a parent's state with changed props, a class's state), with triggers, frames, durations and order, nothing after stopping, and a production build's mount recorded from a document loaded while recording, its functions placed through the map. The page stack (`stack.chromium.test.ts`) also counts a frame's scripts that name a source map by comment or header. The other frameworks (`frameworks.chromium.test.ts`), each bundled by the test from the repo's own packages: a production Angular component read through its view registry (inputs, a settable signal, a field that isn't, each class placed by the map; hosts named by their tags while hovering), a development build through `window.ng` and its tree, a production build's tree through the registry; a Vue 2 component's props, data (set), what the app provides, chain and tree; a Lit component in its shadow root, its properties and state (set), host chain and tree; a plain page's listeners with names, places and options; a Vue 3 listener's function, not Vue's invoker; store actions (`stores.chromium.test.ts`): Redux Toolkit's (the slice's type, payload, what changed, the handler's line through the map; the page stack names Redux), the React commit after one naming it and nothing once stopped, Zustand's through its devtools, Pinia's actions and direct changes and Vuex's mutations (found when recording starts and by a document loaded while recording), NgRx's through its StoreDevtools; a request keeping the stack of the handler that sent it; saved actions (`stateActions.chromium.test.ts`): a picked component's action written, the page loaded again, and the action setting the value in React (a hook), Vue 3 production and development (a ref and data), Vue 2 (data), Angular development (a signal; production gets none) and Lit (through two shadow roots) | `npm test` (skips if no Chromium; `npx playwright install chromium`) | diff --git a/knip.jsonc b/knip.jsonc new file mode 100644 index 00000000..c639e542 --- /dev/null +++ b/knip.jsonc @@ -0,0 +1,19 @@ +// knip (https://knip.dev) finds files, dependencies and exports nothing uses: `npm run lint:unused`. +// It reads the entry points from package.json scripts and the vite, electron-vite and vitest configs. +{ + "$schema": "https://unpkg.com/knip@6/schema-jsonc.json", + // Loaded by their own CLIs, which knip has no plugin for: what they import counts as used. The scripts are Claude + // Code's hooks (.claude/settings.json); the apps, what test/helpers/bundleApp.ts bundles by path for the tests. + "entry": ["electron-builder.ts", "steiger.config.mjs", "scripts/lint-edited-file.ts", "scripts/check-before-stop.ts", "test/fixtures/apps/*.ts", "test/perf/apps/*.ts"], + // test/smoke/update.ts mounts macOS disk images with it. + "ignoreBinaries": ["hdiutil"], + // secretlint loads its rules by name from .secretlintrc.json, which knip has no plugin to read. + "ignoreDependencies": ["@secretlint/secretlint-rule-preset-recommend"], + // A type used in its own file (a component's size or tone, the base a union extends) is part of that file's contract. + "ignoreExportsUsedInFile": { "interface": true, "type": true }, + "ignoreIssues": { + // A module's index.ts is its public API (a slice's, under Feature-Sliced Design). Its props and store types + // are the contract (docs/DESIGN_SYSTEM.md › Components), exported before anyone imports them. Values are not. + "src/**/index.ts": ["types"] + } +} diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 00000000..93643228 --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,46 @@ +# Git hooks (https://lefthook.dev). `npm install` installs them (lefthook's own postinstall; not when CI is set). +# CI runs the same checks again (but for the commit message's), as a hook can be skipped (`git commit --no-verify`, +# `LEFTHOOK=0`) or not installed. + +# Before each commit, the fast checks. Linting and secret scanning read the staged files; finding unused and +# duplicated code covers the whole project, as the answer depends on every file, and runs only when a staged file +# could change it. +pre-commit: + parallel: true + jobs: + - name: lint + glob: "*.{ts,tsx,mts,mjs,js,cjs}" + run: npm run lint -- {staged_files} + fail_text: "Lint: fix what oxlint found (.oxlintrc.json says why each rule is on)." + - name: secrets + run: npx secretlint --no-glob {staged_files} + fail_text: "A secret is about to be committed: remove it, and revoke it if it was ever pushed." + - name: unused code + glob: "*.{ts,tsx,mts,mjs,json,jsonc}" + run: npm run lint:unused + fail_text: "Unused code: remove it (or, if it is public API, see knip.jsonc)." + - name: duplicated code + glob: "*.{ts,tsx}" + run: npm run lint:duplicates + fail_text: "Duplicated code: share it instead of copying it (CLAUDE.md › Code structure)." + +# Commits go out as the author's work: no agent in the message or as author (CLAUDE.md › Commits and pull requests). +commit-msg: + jobs: + - name: agent credits + run: node --no-warnings scripts/check-commit-message.ts {1} + +# Before each push, the checks that take longer or read the whole project (the end-to-end tests are left to CI). +pre-push: + parallel: true + jobs: + - name: types + run: npm run typecheck + - name: slices + run: npm run lint:fsd + - name: code structure + run: npm run lint:structure + - name: lint + run: npm run lint + - name: unit tests + run: npm test diff --git a/package-lock.json b/package-lock.json index 783fa716..889d3c31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@babel/parser": "^7.29.9", "@fontsource-variable/geist": "^5.3.0", "@fontsource-variable/geist-mono": "^5.3.0", + "@hookform/resolvers": "^5.9.1", "@hugeicons/core-free-icons": "^4.3.5", "@hugeicons/react": "^1.1.10", "@jridgewell/trace-mapping": "^0.3.31", @@ -26,8 +27,9 @@ "motion": "^13.4.2", "react": "^19.3.0", "react-dom": "^19.3.0", - "react-use-measure": "^2.1.7", + "react-hook-form": "^7.88.0", "tailwind-merge": "^3.7.0", + "zod": "^4.6.5", "zustand": "^5.0.15" }, "devDependencies": { @@ -40,6 +42,7 @@ "@ngrx/store": "^22.0.1", "@ngrx/store-devtools": "^22.0.1", "@reduxjs/toolkit": "^2.12.0", + "@secretlint/secretlint-rule-preset-recommend": "^13.0.6", "@tailwindcss/vite": "^4.3.3", "@types/js-beautify": "^1.14.3", "@types/node": "^24.13.6", @@ -49,11 +52,17 @@ "electron": "^44.4.5", "electron-builder": "^26.16.1", "electron-vite": "^6.0.0-beta.1", + "jscpd": "^5.3.2", + "knip": "^6.38.0", + "lefthook": "^2.1.14", "lit": "^3.3.3", + "oxlint": "^1.85.0", + "oxlint-tsgolint": "^7.0.2003", "pinia": "^4.0.3", "playwright-core": "^1.63.0", "react-redux": "^9.3.0", "rxjs": "^7.8.2", + "secretlint": "^13.0.6", "steiger": "^0.6.0", "tailwindcss": "^4.3.3", "typescript": "^7.0.2", @@ -146,6 +155,23 @@ } } }, + "node_modules/@azu/format-text": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", + "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@azu/style-format": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz", + "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "@azu/format-text": "^1.0.1" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -755,6 +781,40 @@ "node": ">=14.14" } }, + "node_modules/@emnapi/core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", @@ -1318,6 +1378,116 @@ "url": "https://github.com/sponsors/ayuhito" } }, + "node_modules/@hookform/resolvers": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.9.1.tgz", + "integrity": "sha512-7b7vsbraJxKgjVSA1Nur9tLwj539WGJUBLA7QNvXnFoT2pM5Z7G+6rlukk4B2/QrTZy6huRtH6wKeESPKuIr6w==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "@sinclair/typebox": ">=0.25.24", + "@standard-schema/spec": "^1.0.0", + "@typeschema/main": ">=0.13.7", + "@vinejs/vine": "^2.0.0 || ^3.0.0 || ^4.0.0", + "ajv": "^8.12.0", + "ajv-errors": "^3.0.0", + "ajv-formats": "^2.1.1", + "arktype": "^2.0.0", + "ata-validator": "^1.2.0", + "class-transformer": ">=0.4.0", + "class-validator": ">=0.12.0", + "computed-types": "^1.0.0", + "effect": "^3.10.3", + "fluentvalidation-ts": "^3.0.0", + "fp-ts": "^2.7.0", + "io-ts": "^2.0.0", + "joi": "^17.0.0 || ^18.0.0", + "nope-validator": ">=0.12.0", + "react-hook-form": "^7.55.0", + "superstruct": ">=0.12.0", + "typanion": "^3.3.2", + "valibot": ">=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc", + "vest": ">=6.0.0", + "yup": "^1.0.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@sinclair/typebox": { + "optional": true + }, + "@standard-schema/spec": { + "optional": true + }, + "@typeschema/main": { + "optional": true + }, + "@vinejs/vine": { + "optional": true + }, + "ajv": { + "optional": true + }, + "ajv-errors": { + "optional": true + }, + "ajv-formats": { + "optional": true + }, + "arktype": { + "optional": true + }, + "ata-validator": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + }, + "computed-types": { + "optional": true + }, + "effect": { + "optional": true + }, + "fluentvalidation-ts": { + "optional": true + }, + "fp-ts": { + "optional": true + }, + "io-ts": { + "optional": true + }, + "joi": { + "optional": true + }, + "nope-validator": { + "optional": true + }, + "superstruct": { + "optional": true + }, + "typanion": { + "optional": true + }, + "valibot": { + "optional": true + }, + "vest": { + "optional": true + }, + "yup": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/@hugeicons/core-free-icons": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.3.5.tgz", @@ -1465,6 +1635,28 @@ "node": ">=10" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.4.tgz", + "integrity": "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, "node_modules/@ngrx/store": { "version": "22.0.1", "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-22.0.1.tgz", @@ -1551,113 +1743,10 @@ "integrity": "sha512-TUqERXGNTifZ9y2g3wPxQrw3HpHv/02DsW3D90T9x0hhonrL1ZqpSmNrU2XkoIq0fP1N6gZfVQzy2Fw1ZvGBNg==", "license": "MIT" }, - "node_modules/@oxc-project/types": { - "version": "0.151.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", - "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/oxc-project" - } - }, - "node_modules/@peculiar/asn1-schema": { - "version": "2.9.5", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.9.5.tgz", - "integrity": "sha512-Ez3wLKVjaxdsLcgeWN4OE31QkM7oBOgKuuBJxldRYAkfYw2C+8zJPcSd/SThnbhszsEOlAmoaS5kIIkN29fGKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@peculiar/utils": "^2.0.2", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@peculiar/json-schema": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", - "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@peculiar/utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", - "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/webcrypto": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz", - "integrity": "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.7.0", - "@peculiar/json-schema": "^1.1.12", - "@peculiar/utils": "^2.0.2", - "tslib": "^2.8.1", - "webcrypto-core": "^1.9.2" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/@reduxjs/toolkit": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", - "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@standard-schema/utils": "^0.3.0", - "immer": "^11.0.0", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", - "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, - "node_modules/@reduxjs/toolkit/node_modules/immer": { - "version": "11.1.18", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz", - "integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.10.tgz", - "integrity": "sha512-bp9svZb+QurZeh+8H4BhrZkifEB0YBNvTVzNSJnJQkj4NrRwmQoDUCGP0vSN7PbvLeM7l1tK6GXL8mrTiH2myg==", + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.150.0.tgz", + "integrity": "sha512-oQef2Zu4Prz1KLKznz3HqZzU9uVoA5PMoDZuuLmqms7hKmKSAPzlaMnLllJq3t+rgKmfJJ2siPrpZfFrW06btw==", "cpu": [ "arm" ], @@ -1671,10 +1760,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.10.tgz", - "integrity": "sha512-wm6Dld3RXUAZ/gRWKyUy+4W1B5CB5UeFaOzsSWJWEdxZXHH8rCYiZ5dGe6oJmhsunAPWzL7FZV+VtvmN5Ye2eA==", + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.150.0.tgz", + "integrity": "sha512-B6ofpoFiAUwIZ0MJ2IgHPvZK8FAtL4qzSZRwOkAKIfxEobE1mQC8nDdiFZj7pUaJiVUVCsfkMsBJKedzO8rZvA==", "cpu": [ "arm64" ], @@ -1688,10 +1777,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.10.tgz", - "integrity": "sha512-UbEfXq/AqGNgRTV3ik+X/iR6mUxu2QdYAadwRxJWquUGnW6gDqdP1FtLtFXRow7RJx0ssRwi80XAPr4r+4DtsA==", + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.150.0.tgz", + "integrity": "sha512-J+9IHKzx/bSz1JetOfD4zKXSK9sOm4/a7+0qomJODcTL6JsRXGeV/ZdkAPSIDydfFmWzYCraMlQEosSZEyBYkQ==", "cpu": [ "arm64" ], @@ -1705,10 +1794,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.10.tgz", - "integrity": "sha512-7f5h17q5KZVx/ji1vb8OTq31ch1O2I7K8NPIr44GkyWTApXMIsmhWqZfgpOH10xeauqghDAvGlZktasCkcF6Eg==", + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.150.0.tgz", + "integrity": "sha512-v6IPfcAcSYrBWXV5Tce1DmxDMXLhEYpIIWiRFPJopgCVscZdLaV4MRQOUxvL3usQlw+yrwYOjBFB9IwBx+jUiQ==", "cpu": [ "x64" ], @@ -1722,10 +1811,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.10.tgz", - "integrity": "sha512-ynOk/eEYhC6ZB2xCGvKrEOwE58oBy9LnrAqtkrDF9Fz1VTaNdGZTsV0VarJdhPwb+sOJTGjCLwcuyRJZ1dnMcQ==", + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.150.0.tgz", + "integrity": "sha512-AoR/4jD02HET0KO0yNT4nbTE+XJUxiO9jB1X/ycEjUE3WrIJ3IjV/Vf+gskhWLcqqeXfvNnkDtBR7epllAm88A==", "cpu": [ "x64" ], @@ -1739,10 +1828,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.10.tgz", - "integrity": "sha512-ERrAs185meZZhGan7a4l3RiiJK1ArSDlHdST++uvSxe+FDbR4TwUPahT/cbZJvaG6fIpDpF78surN+tX708Y4Q==", + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.150.0.tgz", + "integrity": "sha512-A/hycCFjLUrCmLoL5O/vBp40ohlaXO1Ta3v5hqYZxicYFs129wZmgZJggcW4mYGYbZebQLhup+N8JjeQl8qwyw==", "cpu": [ "arm" ], @@ -1756,12 +1845,12 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.10.tgz", - "integrity": "sha512-KN7OHKD0J3jy1UzBwZWPxpwhODf9IARUIJcrH+yLYKOcmegZ8luEUM38lDP1bDVj40yP6PsSzCqOJF76vljFnQ==", + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.150.0.tgz", + "integrity": "sha512-pbqahg1Pkz7J4RKmXm30s/iQu99hv64ayXCu8P4p75HcEH5azOdR4eGqiB6lFGkFR3mMpzaYsSKLkS8oJbjmeQ==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", @@ -1773,10 +1862,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.10.tgz", - "integrity": "sha512-8l9wP8O+wa8zD6iw6egSfzVtu7oZVfH3hlUsMM4MwbLMhxleqeoXbZzjddyK3YyNlwLhqznq3tF7PkNJ8T/V2w==", + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.150.0.tgz", + "integrity": "sha512-HV11aRbBQGwqv8bEo+K/6qr88uB4fEe1mhXncMhlVCrj+WpBSraxrUzHaPVmeQRU6x6x8v/3DuCbbo93rpp+fw==", "cpu": [ "arm64" ], @@ -1790,12 +1879,12 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.10.tgz", - "integrity": "sha512-SeXNKeQzA5kLhz/J0CH6ZP0/HJ3v1xm/0YbiYpE0kK7emfRC2OIGGIaE14xzkISEGv2aYuUSpiLiU5Gbq+OI0A==", + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.150.0.tgz", + "integrity": "sha512-k6pVkJqALwtEuP4zukVhGRhdIy4+ofChUIUUsAHHyqDZlNsknmel3JjbggrJiWGaes6h/dIcWmEXsKW4SmK9oQ==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, "license": "MIT", @@ -1807,12 +1896,12 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.10.tgz", - "integrity": "sha512-mtht0nR+y8/hart4175Ll15w7lY8dg7CtQ+j2FDNTsDRspOWTK/2V3l0aj9sIj7XmvqxT8Yli/wq22e7feTTWg==", + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.150.0.tgz", + "integrity": "sha512-tEFg39mw/rHO5n8GK2DR4ZMFfsPQZtnQIPbsIpjoQRomJc/2tRfsCSmCT6gNit+NZGoeJG5aH9ATDH5bf0WnoQ==", "cpu": [ - "s390x" + "ppc64" ], "dev": true, "license": "MIT", @@ -1824,10 +1913,1114 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.10.tgz", - "integrity": "sha512-FSM94nGd55NYo48usCyM/nHfUKRnqc9+b0vJNuKV0oCCpIp/OGims7rO1Nv/DkFkt0S/s2rxsJ2kkS8J3HcpeA==", + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.150.0.tgz", + "integrity": "sha512-0JO7IFkoek6HqV589Menx3hJySNXi6quRhO4tq2P7kpEHKEXoDATnoPVUpn5B7gDH2KLkdo751N0uIrGJFCTCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.150.0.tgz", + "integrity": "sha512-7XPzREnyAS5wHU9aB+49Uaqgoo1gVCklHc3DRlc3fJy2tXD/eAJFN1QFz/crj+Ulup5P1+axNREF+/RGaB/IRA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.150.0.tgz", + "integrity": "sha512-7n7ZxcbRWDFaTdyj8M8p0O9OfzoMKm8O6syBAIgcSutbOALq0Bb9G52Me+XH0anMLZV+NgXc726gqgG2q39vcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.150.0.tgz", + "integrity": "sha512-Vx0GSA9ZCRTUiczoEQnIIyXkMvtEY8uc6IiwLQtMe5ci2sXPqjrry0Ek5fsPP1k2kCzkML3ow9UOOEgnEDi7Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.150.0.tgz", + "integrity": "sha512-ii4/9m3viDLssMnfXU7/Pni/3nYplApuGayceX4qsVGaqqQJCx3tHIH9M/HWIeSty5i8LjS21zPYT6lVIkwLxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.150.0.tgz", + "integrity": "sha512-ZtoxX5rez36ZWkwtiEhjkCPnlTPoQ2I7lIL0IYZ1yjxMYohbvoOjBmUIZzrf9W/HouONq0omIfTeCWEY+R380w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.150.0.tgz", + "integrity": "sha512-VqeRb5JX/bKYBrff7TUDvJyKY0914UzuCkuXIGxJS4FUmvMNfqkCbc7Sq90iMz9DlmAtlggwaBYQ9eg3h3ltiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.150.0.tgz", + "integrity": "sha512-EPqJfeZ4Pgg2BJSsCV8GozxV0FDltRzpVtGVa1r2noJu1iu+opOw3gtBWXwIo9odCT/MdFfyHxdy0/CRqs3OqA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.150.0.tgz", + "integrity": "sha512-n5YMzbqwPQqozbkrexi0lNZrUz5cnPHalYpFWe6Dau7AmKIWNo+y24IwzDuZEQtEdUHuKhXmMMih/MPjOI+CMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.151.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", + "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.24.2.tgz", + "integrity": "sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.24.2.tgz", + "integrity": "sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.24.2.tgz", + "integrity": "sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.24.2.tgz", + "integrity": "sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.24.2.tgz", + "integrity": "sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.24.2.tgz", + "integrity": "sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.24.2.tgz", + "integrity": "sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.24.2.tgz", + "integrity": "sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.24.2.tgz", + "integrity": "sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.24.2.tgz", + "integrity": "sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.24.2.tgz", + "integrity": "sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.24.2.tgz", + "integrity": "sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.24.2.tgz", + "integrity": "sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.24.2.tgz", + "integrity": "sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.24.2.tgz", + "integrity": "sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-openharmony-arm64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.24.2.tgz", + "integrity": "sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.24.2.tgz", + "integrity": "sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.2", + "@emnapi/runtime": "1.11.2", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.24.2.tgz", + "integrity": "sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.24.2.tgz", + "integrity": "sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint-tsgolint/darwin-arm64": { + "version": "7.0.2003", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2003.tgz", + "integrity": "sha512-TgV33rXr6ueXBwvc+0nssUkTBSXHJxv77I8p4RCjDjCnvexHtmoPiudVRDfj3S3puMDdeQdHW6jdUgUPy3nr/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint-tsgolint/darwin-x64": { + "version": "7.0.2003", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-7.0.2003.tgz", + "integrity": "sha512-hY3FMAjIaPDdK3FNxyRXRfHPOFeoBn6dmnLyKZgQ2IbTTD1adXhl6PfCIqHhCPvurigv7nf7mYuWZZ19MmJzmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint-tsgolint/linux-arm64": { + "version": "7.0.2003", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-7.0.2003.tgz", + "integrity": "sha512-eAET4JpyfBbg8SO0K74o4R55tEjVC292pIyxGOw2XGf8x/HrPNyxwQ478jMYOM54FIVOHc8sJ6VRHxluy6lucw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint-tsgolint/linux-x64": { + "version": "7.0.2003", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-7.0.2003.tgz", + "integrity": "sha512-GXdyO/XyqDJ3s/llR/oOktLsYNjZtWSQBy0JMc+/0gsNPvTcseKPhn9c6KcFyWmnr6H4vljYALbujonqSzzEGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint-tsgolint/win32-arm64": { + "version": "7.0.2003", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-7.0.2003.tgz", + "integrity": "sha512-TWauXnPfet0VgpmrstoAK58eJ4gbuwPUuUTQmYQAzE/RG1CpnxXtrKUJULf6lyerPE4KN3gM+DflIA1hOH+38Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint-tsgolint/win32-x64": { + "version": "7.0.2003", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2003.tgz", + "integrity": "sha512-DoRmfe7j8VqNlukp8liRVW45GQDhzRccNenjD/pdzelgtffW47pCMd1xbJLkPaPbKnTwID3onn3VZlL7JbebEA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.85.0.tgz", + "integrity": "sha512-q2KO/Zso9UT+OMn0NF9ywn4E4t0MI3yxiDhNyhsQ7DyQJrC4FhFE4TXOi4bktFnOWXTMds8qZSbpv2XwRaNOBg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.85.0.tgz", + "integrity": "sha512-SxLN3ALjoT9NNdvpjEevGeHvfzTAFrF0NBYB5tzK7/GtCKMze3j1e/m/X2ozqGj2U9hfGG/dg/OG8vpVK4PiDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.85.0.tgz", + "integrity": "sha512-Y/Sup/J4f0f9UGsSd/xyCNTeWL+gepO63GBdEDAfue9nBsnk9zMmnIXx1O6b1V8C90vB5nucYNZ0pbMXAp8zJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.85.0.tgz", + "integrity": "sha512-ApOSNC04ynpDTwvBD+//0wyfODRSbEzvRoKpX8teffmc27z8AockwSNeMXGJXn5KP85eahDgR/2llICWLkzcnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.85.0.tgz", + "integrity": "sha512-bNrVrCOA/kHky3Tu79IXWXe5bhIgLXfUuUEDHlAGOHUk96MkvDZ1ecaQF19rwstrnaqfP1o9nBTqzIr9+ZHkUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.85.0.tgz", + "integrity": "sha512-NUrzOJ1s/EqsVvfn2L/1D8Wro2LPIZUbihL8kOJLh5fEdGEN3rdOGUYq3HwnUIL8sjpoP+4N6RaGrgmMJnaMPw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.85.0.tgz", + "integrity": "sha512-UJXrAT3E/RWkEqXLIs2ehETja1qfgkPb+5gwLIIS+o/6cf+grHvoOXTa5997a/YNQfcJS0DRBTOfZt95cvOI1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.85.0.tgz", + "integrity": "sha512-lK40QLjI0HxigO7CjDDshEtfYIeiYS0020v5BHFPqN4uuQBQxd2K9LNom2dW15o9F1937quSCRVp4ZsVhdbYdg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.85.0.tgz", + "integrity": "sha512-c2zbdBwGKreHXwRx3gWBuFGJxLhxgsg6YlZ+3H+RgRusU/UEV9jNwJ3HGYK+nRo0LvBa7mt6Kj86xoVotUo8cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.85.0.tgz", + "integrity": "sha512-tlt/Hy8lZ97/lCPmCgw/B3k/mwh+BzaIPbPkldZEly7TwLmx0xe2CQcaW2g/rR0dOgS9JNGCZsMEqLhUNMGvaw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.85.0.tgz", + "integrity": "sha512-3tNR9Xey82X0zKuY1d8hJ6Rc9gwRDurmqGLnQZa5xqOXy8/YyiqFXjAtugkKLY82obOlpK1eSiDRlgcNPuxtIg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.85.0.tgz", + "integrity": "sha512-wbGRd5PqCcjkJFHhZuZ2OBSUQY9czlQsoA/cQQB9JK/L9mC5MQgGoKAh+xd8QjA5V+0D3j+Qd1lAWn1I8zlelA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.85.0.tgz", + "integrity": "sha512-3Sn0kSrE4DPZCWV/8o+n4x3aFZxI9ulMnkYlwCbJ8eUVkwRK2IerohE/A/z3SNbCwoPFOCJmGE5Avrq0rrvdvQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.85.0.tgz", + "integrity": "sha512-JY2pxxYfB62bAGfejljVCqc44etItehPuAyaeSAdMuEMtwNA00ggMnS66lC1oIhos6oOXUkuU6mZ9bpFh3BqWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.85.0.tgz", + "integrity": "sha512-5k74vZ6qJBjBHEOlBk9B/iv68Yu0F1Afw/vvT2ar6OGCqEeXLaSjXz2n/IPCbhLG22UoKoYEJTzpYraRdcp6PA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.85.0.tgz", + "integrity": "sha512-GbAl5qt5TCkPLXTaIISZJnugrcBhra6rodcXc9jYt620UtdsTt71NlNmJmm0frxzFpd54x/G+MkitEJA8I/BoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.85.0.tgz", + "integrity": "sha512-kjmws5MK0et2swk4ND85D7NVQyDHw162i6whtZDLUA/lo6FQyBZDcmMRCMcVZcNrAhIaftVb00x9ChGDOjjNJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.85.0.tgz", + "integrity": "sha512-eSsIJx9n4yxvOqYTZyPEMyEXRmE60XH7xGAU7i0Qbsn1lf6Za3CWJ9aRd82oSFKXaxhp+sA6/yMJVRIpLpna6A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.85.0.tgz", + "integrity": "sha512-pBebIPUpKKhWrhSMWhy8TdAZBewiXnfxmaAGxhzxM1068GagqFaTwgKlU6e+UyJ2sPR+VoHouhXuGJkQjsrDvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.9.5.tgz", + "integrity": "sha512-Ez3wLKVjaxdsLcgeWN4OE31QkM7oBOgKuuBJxldRYAkfYw2C+8zJPcSd/SThnbhszsEOlAmoaS5kIIkN29fGKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz", + "integrity": "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "tslib": "^2.8.1", + "webcrypto-core": "^1.9.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.18", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz", + "integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.10.tgz", + "integrity": "sha512-bp9svZb+QurZeh+8H4BhrZkifEB0YBNvTVzNSJnJQkj4NrRwmQoDUCGP0vSN7PbvLeM7l1tK6GXL8mrTiH2myg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.10.tgz", + "integrity": "sha512-wm6Dld3RXUAZ/gRWKyUy+4W1B5CB5UeFaOzsSWJWEdxZXHH8rCYiZ5dGe6oJmhsunAPWzL7FZV+VtvmN5Ye2eA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.10.tgz", + "integrity": "sha512-UbEfXq/AqGNgRTV3ik+X/iR6mUxu2QdYAadwRxJWquUGnW6gDqdP1FtLtFXRow7RJx0ssRwi80XAPr4r+4DtsA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.10.tgz", + "integrity": "sha512-7f5h17q5KZVx/ji1vb8OTq31ch1O2I7K8NPIr44GkyWTApXMIsmhWqZfgpOH10xeauqghDAvGlZktasCkcF6Eg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.10.tgz", + "integrity": "sha512-ynOk/eEYhC6ZB2xCGvKrEOwE58oBy9LnrAqtkrDF9Fz1VTaNdGZTsV0VarJdhPwb+sOJTGjCLwcuyRJZ1dnMcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.10.tgz", + "integrity": "sha512-ERrAs185meZZhGan7a4l3RiiJK1ArSDlHdST++uvSxe+FDbR4TwUPahT/cbZJvaG6fIpDpF78surN+tX708Y4Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.10.tgz", + "integrity": "sha512-KN7OHKD0J3jy1UzBwZWPxpwhODf9IARUIJcrH+yLYKOcmegZ8luEUM38lDP1bDVj40yP6PsSzCqOJF76vljFnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.10.tgz", + "integrity": "sha512-8l9wP8O+wa8zD6iw6egSfzVtu7oZVfH3hlUsMM4MwbLMhxleqeoXbZzjddyK3YyNlwLhqznq3tF7PkNJ8T/V2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.10.tgz", + "integrity": "sha512-SeXNKeQzA5kLhz/J0CH6ZP0/HJ3v1xm/0YbiYpE0kK7emfRC2OIGGIaE14xzkISEGv2aYuUSpiLiU5Gbq+OI0A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.10.tgz", + "integrity": "sha512-mtht0nR+y8/hart4175Ll15w7lY8dg7CtQ+j2FDNTsDRspOWTK/2V3l0aj9sIj7XmvqxT8Yli/wq22e7feTTWg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.10.tgz", + "integrity": "sha512-FSM94nGd55NYo48usCyM/nHfUKRnqc9+b0vJNuKV0oCCpIp/OGims7rO1Nv/DkFkt0S/s2rxsJ2kkS8J3HcpeA==", "cpu": [ "x64" ], @@ -1916,6 +3109,213 @@ "dev": true, "license": "MIT" }, + "node_modules/@secretlint/config-creator": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-13.0.6.tgz", + "integrity": "sha512-vXINUCWnUS0IAVwj84mcY0seiOZSuVHMS1zLu0uipBPDZNCJKd5VLrCV+RfAKMNkLqmFVE4pTeNCevJl5eSZLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "13.0.6" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@secretlint/config-loader": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-13.0.6.tgz", + "integrity": "sha512-+7QvpAUzE4MCHUmL3O+pOZy/j5oqUMAATmRTp0RPAxKuUpxPe/WmzQv9UW679EzhOnC4sxkp6j4L9wFCTe03pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "13.0.6", + "@secretlint/resolver": "13.0.6", + "@secretlint/types": "13.0.6", + "ajv": "^8.20.0", + "debug": "^4.4.3", + "rc-config-loader": "^4.1.4" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@secretlint/core": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-13.0.6.tgz", + "integrity": "sha512-sZ4fKF3/eHO706p8wovzQlaRMfS4k272jAF9LCj/B4HqxhuFiZi/Nx4amWQAYFOcEfrmVpGQmlgHDHDNx+22fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "13.0.6", + "@secretlint/types": "13.0.6", + "debug": "^4.4.3", + "structured-source": "^4.0.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@secretlint/formatter": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-13.0.6.tgz", + "integrity": "sha512-m5jnjSvB99uEB3K5wH8Sj2cG+bMmLYY5/RxRSVsn0Eik+Bf28ScdkRUx4yXhfyFXUf65aH9Jt42BB/9VHLgUtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/resolver": "13.0.6", + "@secretlint/types": "13.0.6", + "@textlint/linter-formatter": "^15.7.0", + "@textlint/module-interop": "^15.7.0", + "@textlint/types": "^15.7.0", + "chalk": "^5.6.2", + "debug": "^4.4.3", + "pluralize": "^8.0.0", + "strip-ansi": "^7.2.0", + "table": "^6.9.0", + "terminal-link": "^5.0.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@secretlint/formatter/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@secretlint/formatter/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@secretlint/formatter/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@secretlint/node": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-13.0.6.tgz", + "integrity": "sha512-cYgh4qYDkerJzm/X0YRyqTywsZSsPKbOS7ddVl9+jELq7frCwZ8wpFh5sEPXZ7iH89DjYIfXbtz2HPYvFq9NNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-loader": "13.0.6", + "@secretlint/core": "13.0.6", + "@secretlint/formatter": "13.0.6", + "@secretlint/profiler": "13.0.6", + "@secretlint/source-creator": "13.0.6", + "@secretlint/types": "13.0.6", + "debug": "^4.4.3", + "p-map": "^7.0.7" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@secretlint/profiler": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-13.0.6.tgz", + "integrity": "sha512-I8ci8bwwtJfzUsLTgrf2j8aOBeb9myKgzkVeKQEae76sx9sBbd3d/DD+4pKYZE0jdtNomUIV/ogtAXvhP0iU6Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/resolver": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-13.0.6.tgz", + "integrity": "sha512-HHTfCjEZvChOFebJ/JBpsqCM/q8hmS6qZq80xbQdVEsdjMmTcTX8E/uvYkOuyozuLCWePpJCnZ6nIxceSvYoog==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/secretlint-rule-preset-recommend": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-13.0.6.tgz", + "integrity": "sha512-5OdYoPKs7LznSY/mFgpJlSY8ayF+clq0gdfOE0ERVfvUifmIDz0yG/d1ij3wZmfpyOw33tk12zUcDZqrXHFodA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@secretlint/source-creator": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-13.0.6.tgz", + "integrity": "sha512-cKO2Y0gh7RMNo4vMtJgmPMDHCuJZSP3aPZ0y+KzWST3wMTNCnPQcECeQeEccdL8b2oBfVLD/UJrb7fbhumXGsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "13.0.6", + "istextorbinary": "^9.5.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@secretlint/types": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-13.0.6.tgz", + "integrity": "sha512-N1pXCKblM7rdbQ+wAufLLUQ4ToukuPjsCauR42fHlemp8+NXvI4FjDnvLacNbSb/9ccmrY1Isb6WjUfHZgnrRQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@secretlint/walker": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/@secretlint/walker/-/walker-13.0.6.tgz", + "integrity": "sha512-i5xSlLTX+QI3AmbsR2DfR2KbQIG74qfhQovzhKOp//or9t1SL4N43w8N5/6NyESgt3XHEY2QiTSvQTOU10uHNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ignore": "^7.0.8", + "picomatch": "^4.0.7" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@secretlint/walker/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -1946,14 +3346,13 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@standard-schema/utils": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", - "dev": true, "license": "MIT" }, "node_modules/@szmarczak/http-timer": { @@ -2268,6 +3667,80 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@textlint/ast-node-types": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.8.0.tgz", + "integrity": "sha512-5CiH9COYmovWmExQgs7763DzX6Gy9zjkjJ7JxCC95wyTcjwQn/8poNF6fv3qzRlmx8CRRde8DHr9FcgAAiPzgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.8.0.tgz", + "integrity": "sha512-+oU3A235NATv6Lzi4xa4kJ65PuNJlIxesaO4AvDhDWA9FWm7y4XKWaoQCW1esgaQQ6dwnUiFKArQ8TcJ86mC4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azu/format-text": "^1.0.2", + "@azu/style-format": "^1.0.1", + "@textlint/module-interop": "15.8.0", + "@textlint/resolver": "15.8.0", + "@textlint/types": "15.8.0", + "debug": "^4.4.3", + "js-yaml": "^4.3.0", + "lodash": "^4.18.1", + "pluralize": "^2.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "table": "^6.9.0", + "text-table": "^0.2.0" + }, + "engines": { + "node": ">=20.18.0" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/pluralize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz", + "integrity": "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/module-interop": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.8.0.tgz", + "integrity": "sha512-rt+OR1WYGoLOY8HkA/aBPrqufF6yUUEsKEAh7XohTsT3lp9IyZFT6zOIbjul9P4FAzsmSPkcrYjVx3Bz/IUfkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/resolver": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.8.0.tgz", + "integrity": "sha512-E88tzfX3K8Jykk+38aJ9cy8RquD8ABVOPTO2rFEESq0wcg8x6/ypdAS8ZgR7OKiGqlRF0hkO/m5PbQwVfKM3VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/types": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.8.0.tgz", + "integrity": "sha512-Anhc6y5736YIsvqae0U6k0YmB2M/QVHkEeOv2aydAn/WIkdI69dCOiDbe3/+RagS3qstFTSFWJzNRA2lUjv19w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@textlint/ast-node-types": "15.8.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -2367,6 +3840,13 @@ "undici-types": "~7.18.0" } }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.3.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", @@ -3016,7 +4496,7 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -3029,6 +4509,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -3249,6 +4745,16 @@ "node": ">=12" } }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -3333,6 +4839,22 @@ "node": ">=6.0.0" } }, + "node_modules/binaryextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", + "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/birpc": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", @@ -3360,6 +4882,13 @@ "license": "MIT", "optional": true }, + "node_modules/boundary": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", + "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/brace-expansion": { "version": "5.0.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", @@ -4099,6 +5628,23 @@ "readable-stream": "^2.0.2" } }, + "node_modules/editions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", + "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "version-range": "^4.15.0" + }, + "engines": { + "ecmascript": ">= es5", + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/editorconfig": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-3.0.2.tgz", @@ -4914,6 +6460,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -5094,7 +6653,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -5118,7 +6677,7 @@ "version": "3.1.8", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -5151,6 +6710,16 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz", + "integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "walk-up-path": "^4.0.0" + } + }, "node_modules/filelist": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", @@ -5218,7 +6787,24 @@ "mime-types": "^2.1.35" }, "engines": { - "node": ">= 6" + "node": ">= 6" + } + }, + "node_modules/formatly": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.7.1.tgz", + "integrity": "sha512-ESuCEJE0SGgabl9q+zABI8LhJDmF+8nhpuqDzuM48HfJ9QTD5o6PqFJDFZY7w6f8xsh5lixuj5nJsQNvYOWvKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fd-package-json": "^2.0.0", + "package-manager-detector": "^1.8.0" + }, + "bin": { + "formatly": "bin/index.mjs" + }, + "engines": { + "node": ">=18.3.0" } }, "node_modules/framer-motion": { @@ -5365,6 +6951,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz", + "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5728,6 +7327,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -5833,6 +7445,24 @@ "node": ">=18" } }, + "node_modules/istextorbinary": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", + "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "binaryextensions": "^6.11.0", + "editions": "^6.21.0", + "textextensions": "^6.11.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/jake": { "version": "10.9.4", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", @@ -5934,6 +7564,144 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jscpd": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/jscpd/-/jscpd-5.3.2.tgz", + "integrity": "sha512-siYdg3TBNwizWFaYPgr1vwuSs+NWeKJjLAbd00Nkmr2y8TSZRSW0cOGKtqSLgOv7QXDbC/XECnCrZEm5v6RMkg==", + "dev": true, + "license": "MIT", + "bin": { + "jscpd": "run-jscpd.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://opencollective.com/jscpd" + }, + "optionalDependencies": { + "jscpd-darwin-arm64": "5.3.2", + "jscpd-darwin-x64": "5.3.2", + "jscpd-linux-arm64-gnu": "5.3.2", + "jscpd-linux-arm64-musl": "5.3.2", + "jscpd-linux-x64-gnu": "5.3.2", + "jscpd-linux-x64-musl": "5.3.2", + "jscpd-windows-arm64-msvc": "5.3.2", + "jscpd-windows-x64-msvc": "5.3.2" + } + }, + "node_modules/jscpd-darwin-arm64": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/jscpd-darwin-arm64/-/jscpd-darwin-arm64-5.3.2.tgz", + "integrity": "sha512-dlNh+revPs3X1yTRNAYqZD9u7eqiR9I+KoNvEk9JzSwzOuaUQrgwpP5EZQB0NKuF0oZZZeKrk05OTsNorVpqmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/jscpd-darwin-x64": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/jscpd-darwin-x64/-/jscpd-darwin-x64-5.3.2.tgz", + "integrity": "sha512-JUt9dhLw2k1WNLqbFcqbDf8ErR6mp9ucKn+chRBsBRdx7MNbdEXmV5UuHRBO8d9h7Cw1eSpfpBPMTCXOS4aaeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/jscpd-linux-arm64-gnu": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/jscpd-linux-arm64-gnu/-/jscpd-linux-arm64-gnu-5.3.2.tgz", + "integrity": "sha512-4+Z/fW7nqruf47AXRXPe8nTeExPm93Gt4A/6B4Wsi6b0G8Ei/tYDxB4jAwUBXnOGVwxrr/IDJlgSEvHLbzX+MQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/jscpd-linux-arm64-musl": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/jscpd-linux-arm64-musl/-/jscpd-linux-arm64-musl-5.3.2.tgz", + "integrity": "sha512-+OsVA0iLfDNUIzJArAaJnGyOHxsflpbpkGh/mWs2nZV7vPd1g5Y9SWO+vj83f6BdaO9VNbUbaQAY4dBYadfHIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/jscpd-linux-x64-gnu": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/jscpd-linux-x64-gnu/-/jscpd-linux-x64-gnu-5.3.2.tgz", + "integrity": "sha512-DsNsbCCc5mMOGZdhzgPETsp9RAaKk03cdx1K9wx9RShePnF+AXstYYSiW7M/qjZqvBMlSpO60BXpJvNUQljJJg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/jscpd-linux-x64-musl": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/jscpd-linux-x64-musl/-/jscpd-linux-x64-musl-5.3.2.tgz", + "integrity": "sha512-9D+Jg4MXTHoY5FiK6Fh3FeAr1MsqNS98X5zMUH6PNXu9PKsT0g3Rez2vdYOnoKOE4n8o1XAs8eyU0pm3+HYwog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/jscpd-windows-arm64-msvc": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/jscpd-windows-arm64-msvc/-/jscpd-windows-arm64-msvc-5.3.2.tgz", + "integrity": "sha512-qPqZEMHUOjc5tKVU7PElyIhI1J0xyAuQ44S2SwLvKS5qw1NATGTaGhp8MuJkrvTmNcNZmLF0Me6Cv6zj6DVlWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/jscpd-windows-x64-msvc": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/jscpd-windows-x64-msvc/-/jscpd-windows-x64-msvc-5.3.2.tgz", + "integrity": "sha512-WfO1k0/NXA+2HjLaWUkheQv6bEkYrI4WKa/Cf429+dnpdiYB6S8KpJyd6/xGJd+XJ9nKhE0mIFuk3+VdXtaFMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -5954,68 +7722,301 @@ "dev": true, "license": "MIT" }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/knip": { + "version": "6.38.0", + "resolved": "https://registry.npmjs.org/knip/-/knip-6.38.0.tgz", + "integrity": "sha512-umytiMCaZf02MV9SOO1FEDgbDf6h20VZk3ruJE0kh52kkGjYZFRUAXHJWYoLhvhoHGINO/7GXVJmQ2vxTBvl6A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/webpro" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/knip" + } + ], + "license": "ISC", + "dependencies": { + "fdir": "^6.5.0", + "formatly": "^0.7.0", + "get-tsconfig": "4.14.3", + "jiti": "^2.7.0", + "oxc-parser": "^0.150.0", + "oxc-resolver": "11.24.2", + "picomatch": "^4.0.7", + "smol-toml": "^1.8.0", + "strip-json-comments": "5.0.3", + "tinyglobby": "^0.2.17", + "unbash": "^4.0.11", + "yaml": "^2.9.1", + "zod": "^4.4.3" + }, + "bin": { + "knip": "bin/knip.js", + "knip-bun": "bin/knip-bun.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/knip/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/knip/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "license": "MIT" + }, + "node_modules/lefthook": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/lefthook/-/lefthook-2.1.14.tgz", + "integrity": "sha512-Y9TL1dmpRlIdSp73SJ6bb+xAZ4s2SinGTK3pgC9PRWMSjMr2iOfowPGnlSOfAo5fu6S5rdiItsQDxeLEWCGk4g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "lefthook": "bin/index.js" + }, + "optionalDependencies": { + "lefthook-darwin-arm64": "2.1.14", + "lefthook-darwin-x64": "2.1.14", + "lefthook-freebsd-arm64": "2.1.14", + "lefthook-freebsd-x64": "2.1.14", + "lefthook-linux-arm64": "2.1.14", + "lefthook-linux-x64": "2.1.14", + "lefthook-openbsd-arm64": "2.1.14", + "lefthook-openbsd-x64": "2.1.14", + "lefthook-windows-arm64": "2.1.14", + "lefthook-windows-x64": "2.1.14" + } + }, + "node_modules/lefthook-darwin-arm64": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/lefthook-darwin-arm64/-/lefthook-darwin-arm64-2.1.14.tgz", + "integrity": "sha512-VMUAqY81+8ph7Mc0iROd3JWqj2sOuK7SYDyd8ZgzYVKChCt+Nnz8ehd4gUwprJy7QI28IFSUh1M3wnIRiYh+fA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/lefthook-darwin-x64": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/lefthook-darwin-x64/-/lefthook-darwin-x64-2.1.14.tgz", + "integrity": "sha512-uSmjcPRU+yB+D6vk+u6WJDnyHlB+XIv1QZw97N2+qNGG2bNSLH/7MuJ6puCdJv+lVPBEO5wBjE3JIbaEzPOQCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/lefthook-freebsd-arm64": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/lefthook-freebsd-arm64/-/lefthook-freebsd-arm64-2.1.14.tgz", + "integrity": "sha512-po/pmjbp/7BjE9RFfeDnh+CeNsA0d+utppOYKB425I7mgI1GcmT8GAZ9DDut4DIutNjVqKL/lzrj+F87Gm40Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/lefthook-freebsd-x64": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/lefthook-freebsd-x64/-/lefthook-freebsd-x64-2.1.14.tgz", + "integrity": "sha512-+KiXiFjJf7YdHUYjzhDsklcA5bAFZN+pNcz/NbBftrhVTwzNALNoK+SYEKt+9QJsrzL6tpDWmHHFpeLGp+8t8w==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "node_modules/lefthook-linux-arm64": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/lefthook-linux-arm64/-/lefthook-linux-arm64-2.1.14.tgz", + "integrity": "sha512-bjeJB16ftJaPWGTedssh8ZrqRy1ACfQuTV7a0O3NU1FoIsJtfHOm0o0mV4cPf4q+ZCVCwq1Xf2I11qUpuJFV4Q==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "node_modules/lefthook-linux-x64": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/lefthook-linux-x64/-/lefthook-linux-x64-2.1.14.tgz", + "integrity": "sha512-gYExzjU2w3uKrP1MklbHENS1cXUdCQRIiEJkykI7q4RjhIHi+mQEUAZLxZw93a6jE2pNWaO0me5tB8EbEt4/GQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC", - "optional": true + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "node_modules/lefthook-openbsd-arm64": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/lefthook-openbsd-arm64/-/lefthook-openbsd-arm64-2.1.14.tgz", + "integrity": "sha512-MCW76gTlEMBF3Ds8O63UTxjaSguha/5zT0U3Vr719I1sXueSpttfRmm/5YfIB4MygB116yA/Vi+DsizT/X0kcg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } + "optional": true, + "os": [ + "openbsd" + ] }, - "node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "node_modules/lefthook-openbsd-x64": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/lefthook-openbsd-x64/-/lefthook-openbsd-x64-2.1.14.tgz", + "integrity": "sha512-salAc1PAhLI6xrB9pth5UxALQlTOGvZAfaAvgFwixhkdu8D//uW8zS3faFq31ktd1bDAy5wDI8p9yg+fF5+2uA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } + "optional": true, + "os": [ + "openbsd" + ] }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "node_modules/lefthook-windows-arm64": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/lefthook-windows-arm64/-/lefthook-windows-arm64-2.1.14.tgz", + "integrity": "sha512-q3JC6lBrYLzkhXEhfnmFxJF47gSd5JeDweH3aNrHoLGX/KkRiOrk9RSyoRwSuGq03EYxSkJhl1FLj2GUxF7VqQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/lazy-val": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", - "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "license": "MIT" + "node_modules/lefthook-windows-x64": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/lefthook-windows-x64/-/lefthook-windows-x64-2.1.14.tgz", + "integrity": "sha512-WwIxdwW1lDAaJUU3ETcKlCtdqluGr3Cy+LjKvP4QIQnFf0FEXL78THstIkZ6k6qhfUn7/WvyqSIf0cbTAKw2nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/lightningcss": { "version": "1.32.0", @@ -6346,6 +8347,13 @@ "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", "license": "MIT" }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -6779,6 +8787,44 @@ "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, + "node_modules/normalize-package-data": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-8.0.0.tgz", + "integrity": "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^9.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", @@ -6834,6 +8880,151 @@ "wrappy": "1" } }, + "node_modules/oxc-parser": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.150.0.tgz", + "integrity": "sha512-zwajKw1GUa57IOdafIS7/yOObGF2NihwIfZfs5yJTRprMSkiwhWOc/k+n8bwnUPbHr2/XsjfBO8KyTPaW3VpWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.150.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.150.0", + "@oxc-parser/binding-android-arm64": "0.150.0", + "@oxc-parser/binding-darwin-arm64": "0.150.0", + "@oxc-parser/binding-darwin-x64": "0.150.0", + "@oxc-parser/binding-freebsd-x64": "0.150.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.150.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.150.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.150.0", + "@oxc-parser/binding-linux-arm64-musl": "0.150.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.150.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.150.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.150.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.150.0", + "@oxc-parser/binding-linux-x64-gnu": "0.150.0", + "@oxc-parser/binding-linux-x64-musl": "0.150.0", + "@oxc-parser/binding-openharmony-arm64": "0.150.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.150.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.150.0", + "@oxc-parser/binding-win32-x64-msvc": "0.150.0" + } + }, + "node_modules/oxc-parser/node_modules/@oxc-project/types": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.150.0.tgz", + "integrity": "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/oxc-resolver": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.24.2.tgz", + "integrity": "sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.24.2", + "@oxc-resolver/binding-android-arm64": "11.24.2", + "@oxc-resolver/binding-darwin-arm64": "11.24.2", + "@oxc-resolver/binding-darwin-x64": "11.24.2", + "@oxc-resolver/binding-freebsd-x64": "11.24.2", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.24.2", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.24.2", + "@oxc-resolver/binding-linux-arm64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-arm64-musl": "11.24.2", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-riscv64-musl": "11.24.2", + "@oxc-resolver/binding-linux-s390x-gnu": "11.24.2", + "@oxc-resolver/binding-linux-x64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-x64-musl": "11.24.2", + "@oxc-resolver/binding-openharmony-arm64": "11.24.2", + "@oxc-resolver/binding-wasm32-wasi": "11.24.2", + "@oxc-resolver/binding-win32-arm64-msvc": "11.24.2", + "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" + } + }, + "node_modules/oxlint": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.85.0.tgz", + "integrity": "sha512-bc26s97nuvPj1ViyPsqmKecVkUWFMEdtayO8MaQ6oiLfs1pj94cQlZZhrh4BPNlr9HQosjhIlwgZKsfcwmcNgg==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.85.0", + "@oxlint/binding-android-arm64": "1.85.0", + "@oxlint/binding-darwin-arm64": "1.85.0", + "@oxlint/binding-darwin-x64": "1.85.0", + "@oxlint/binding-freebsd-x64": "1.85.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.85.0", + "@oxlint/binding-linux-arm-musleabihf": "1.85.0", + "@oxlint/binding-linux-arm64-gnu": "1.85.0", + "@oxlint/binding-linux-arm64-musl": "1.85.0", + "@oxlint/binding-linux-ppc64-gnu": "1.85.0", + "@oxlint/binding-linux-riscv64-gnu": "1.85.0", + "@oxlint/binding-linux-riscv64-musl": "1.85.0", + "@oxlint/binding-linux-s390x-gnu": "1.85.0", + "@oxlint/binding-linux-x64-gnu": "1.85.0", + "@oxlint/binding-linux-x64-musl": "1.85.0", + "@oxlint/binding-openharmony-arm64": "1.85.0", + "@oxlint/binding-win32-arm64-msvc": "1.85.0", + "@oxlint/binding-win32-ia32-msvc": "1.85.0", + "@oxlint/binding-win32-x64-msvc": "1.85.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/oxlint-tsgolint": { + "version": "7.0.2003", + "resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-7.0.2003.tgz", + "integrity": "sha512-VnK4zlqgmgq/7ZcjzCk/WpN8kKFsYGcB85Io9qT3wL4K8Un3RKmJkp793crNETieMusPMKOA4a+Mw2wbteI6TQ==", + "dev": true, + "license": "MIT", + "bin": { + "tsgolint": "bin/tsgolint.js" + }, + "optionalDependencies": { + "@oxlint-tsgolint/darwin-arm64": "7.0.2003", + "@oxlint-tsgolint/darwin-x64": "7.0.2003", + "@oxlint-tsgolint/linux-arm64": "7.0.2003", + "@oxlint-tsgolint/linux-x64": "7.0.2003", + "@oxlint-tsgolint/win32-arm64": "7.0.2003", + "@oxlint-tsgolint/win32-x64": "7.0.2003" + } + }, "node_modules/p-cancelable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", @@ -6860,6 +9051,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.8.tgz", + "integrity": "sha512-MitaVsCuCFIvOLLPIU7NnfrZvS9H9h7kwMUkDo+T2pEISaJD48IV9S8iIdXB7PsvvdxyYcsSTTrr90XKsbulNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", + "dev": true, + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7298,6 +9509,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/rc-config-loader": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.4.tgz", + "integrity": "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "json5": "^2.2.3", + "require-from-string": "^2.0.2" + } + }, "node_modules/react": { "version": "19.3.0", "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", @@ -7319,6 +9543,22 @@ "react": "^19.3.0" } }, + "node_modules/react-hook-form": { + "version": "7.88.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.88.0.tgz", + "integrity": "sha512-QRaLOWhX93YCnMiRfnOFRSwWXZNt8qhm2JTZwypoDvKpSffTJHmpzMXt8U6PV5UThvL3IiiLDUWe2nHMtz6Mmw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-redux": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", @@ -7334,41 +9574,106 @@ "react": "^18.0 || ^19", "redux": "^5.0.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "redux": { - "optional": true - } + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/read-pkg": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-10.1.0.tgz", + "integrity": "sha512-I8g2lArQiP78ll51UeMZojewtYgIRCKCWqZEgOO8c/uefTI+XDXvCSXu3+YNUaTNvZzobrL5+SqHjBrByRRTdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.4", + "normalize-package-data": "^8.0.0", + "parse-json": "^8.3.0", + "type-fest": "^5.4.4", + "unicorn-magic": "^0.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/parse-json/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-use-measure": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", - "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.13", - "react-dom": ">=16.13" + "node_modules/read-pkg/node_modules/type-fest": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.10.0.tgz", + "integrity": "sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/read-binary-file-arch": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", - "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "node_modules/read-pkg/node_modules/unicorn-magic": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.1.tgz", + "integrity": "sha512-lzlXPoVB0Uy/FvTaUgX39RbixYiSKoc3JSoSf01+0c2B6FR3qdYIPN7Qjo/AV7n6sqLCk0509cT3LRj1oSZ1+A==", "dev": true, "license": "MIT", - "dependencies": { - "debug": "^4.3.4" + "engines": { + "node": ">=20" }, - "bin": { - "read-binary-file-arch": "cli.js" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/readable-stream": { @@ -7432,7 +9737,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7480,6 +9785,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/responselike": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", @@ -7648,6 +9963,29 @@ "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", "license": "MIT" }, + "node_modules/secretlint": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-13.0.6.tgz", + "integrity": "sha512-9l+ZtJTOukuRrFOY9iTNEteO84zyPJ68LgYMUj6T5GOBRH7tnyV+UDIO/SrADbjRSx7eDGZ8q8+CxHkqhoVOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-creator": "13.0.6", + "@secretlint/formatter": "13.0.6", + "@secretlint/node": "13.0.6", + "@secretlint/profiler": "13.0.6", + "@secretlint/resolver": "13.0.6", + "@secretlint/walker": "13.0.6", + "debug": "^4.4.3", + "read-pkg": "^10.1.0" + }, + "bin": { + "secretlint": "bin/secretlint.js" + }, + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -7755,6 +10093,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/smol-toml": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.0.tgz", + "integrity": "sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -7786,6 +10155,42 @@ "source-map": "^0.6.0" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.24", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.24.tgz", + "integrity": "sha512-cLS9TtWkIQFyLkJ3/5aFQAOHOSKTlOs/7WDut/XPSdjom1fJhUdkepeJAKXa9Y05+FabHd0sti+Et559vDtkpQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -7847,6 +10252,29 @@ "steiger": "dist/cli.mjs" } }, + "node_modules/steiger/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/steiger/node_modules/zod-validation-error": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.5.4.tgz", + "integrity": "sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.24.4" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -7885,6 +10313,29 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/structured-source": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", + "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boundary": "^2.0.0" + } + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -7911,6 +10362,79 @@ "node": ">=8" } }, + "node_modules/supports-hyperlinks": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.5.0.tgz", + "integrity": "sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^5.0.1", + "supports-color": "^10.2.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/supports-hyperlinks/node_modules/has-flag": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-5.0.1.tgz", + "integrity": "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-hyperlinks/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tailwind-merge": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.7.0.tgz", @@ -7995,6 +10519,46 @@ "fs-extra": "^10.0.0" } }, + "node_modules/terminal-link": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-5.0.0.tgz", + "integrity": "sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "supports-hyperlinks": "^4.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/textextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", + "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/tiny-async-pool": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", @@ -8187,6 +10751,16 @@ "@typescript/typescript-win32-x64": "7.0.2" } }, + "node_modules/unbash": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/unbash/-/unbash-4.0.11.tgz", + "integrity": "sha512-FoSOKV7NEofQSkAefMVHam4ZPKYMxjAydxiV72UFEDNV/YofxjGfiZ2A9pZjdL/lRJzTjcu4PABo1JYJX8N5iQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/undici": { "version": "7.29.1", "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", @@ -8311,6 +10885,30 @@ "dev": true, "license": "MIT" }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/version-range": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", + "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", + "dev": true, + "license": "Artistic-2.0", + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/vite": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", @@ -8838,6 +11436,16 @@ "dev": true, "license": "MIT" }, + "node_modules/walk-up-path": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", + "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/web-tree-sitter": { "version": "0.26.13", "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.26.13.tgz", @@ -8944,6 +11552,22 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "17.7.3", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", @@ -8987,28 +11611,14 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "dev": true, + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/zod-validation-error": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.5.4.tgz", - "integrity": "sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.24.4" - } - }, "node_modules/zustand": { "version": "5.0.15", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.15.tgz", diff --git a/package.json b/package.json index 92bae0ee..716f9da6 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,12 @@ "test:packaged": "node --no-warnings test/smoke/packaged.ts", "test:update": "node --no-warnings test/smoke/update.ts", "demo-site": "node --no-warnings test/fixtures/site.ts", + "lint": "oxlint --type-aware", "lint:fsd": "steiger ./src/renderer/src", - "lint:structure": "node --no-warnings scripts/check-structure.ts" + "lint:structure": "node --no-warnings scripts/check-structure.ts", + "lint:unused": "knip", + "lint:duplicates": "jscpd src scripts --format typescript,tsx --baseline .jscpd-baseline.json --fail-on-new-clones --reporters console", + "lint:secrets": "secretlint \"**/*\"" }, "devDependencies": { "@angular/common": "^22.2.0", @@ -55,6 +59,7 @@ "@ngrx/store": "^22.0.1", "@ngrx/store-devtools": "^22.0.1", "@reduxjs/toolkit": "^2.12.0", + "@secretlint/secretlint-rule-preset-recommend": "^13.0.6", "@tailwindcss/vite": "^4.3.3", "@types/js-beautify": "^1.14.3", "@types/node": "^24.13.6", @@ -64,11 +69,17 @@ "electron": "^44.4.5", "electron-builder": "^26.16.1", "electron-vite": "^6.0.0-beta.1", + "jscpd": "^5.3.2", + "knip": "^6.38.0", + "lefthook": "^2.1.14", "lit": "^3.3.3", + "oxlint": "^1.85.0", + "oxlint-tsgolint": "^7.0.2003", "pinia": "^4.0.3", "playwright-core": "^1.63.0", "react-redux": "^9.3.0", "rxjs": "^7.8.2", + "secretlint": "^13.0.6", "steiger": "^0.6.0", "tailwindcss": "^4.3.3", "typescript": "^7.0.2", @@ -82,6 +93,7 @@ "@babel/parser": "^7.29.9", "@fontsource-variable/geist": "^5.3.0", "@fontsource-variable/geist-mono": "^5.3.0", + "@hookform/resolvers": "^5.9.1", "@hugeicons/core-free-icons": "^4.3.5", "@hugeicons/react": "^1.1.10", "@jridgewell/trace-mapping": "^0.3.31", @@ -96,8 +108,9 @@ "motion": "^13.4.2", "react": "^19.3.0", "react-dom": "^19.3.0", - "react-use-measure": "^2.1.7", + "react-hook-form": "^7.88.0", "tailwind-merge": "^3.7.0", + "zod": "^4.6.5", "zustand": "^5.0.15" }, "overrides": { diff --git a/scripts/agent-hooks/constants.ts b/scripts/agent-hooks/constants.ts new file mode 100644 index 00000000..12ae314c --- /dev/null +++ b/scripts/agent-hooks/constants.ts @@ -0,0 +1,21 @@ +/** The exit code with which a Claude Code hook hands its stderr to the agent: after an edit, to fix; on Stop, to go on. */ +export const BLOCKING_EXIT = 2; + +/** Files oxlint reads. */ +export const LINTED_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']; + +/** + * The npm scripts that have to pass before the agent stops: CLAUDE.md › Before pushing, but for the tests, which take + * longer (git's pre-push hook runs the unit tests). Each takes seconds, and they run side by side. + */ +export const STOP_CHECKS = ['typecheck', 'lint:fsd', 'lint:structure', 'lint', 'lint:unused', 'lint:duplicates', 'lint:secrets']; + +/** How much of a failing check's output goes to the agent: its end, where the summary is. */ +export const OUTPUT_TAIL_LINES = 40; + +/** Plain output, for the agent to read. */ +export const NO_COLOR_ENV = { NO_COLOR: '1', FORCE_COLOR: '0' }; + +/** npm runs through its `.cmd` shim on Windows, which only a shell starts. */ +export const NPM = 'npm'; +export const WINDOWS = 'win32'; diff --git a/scripts/agent-hooks/hasChanges.ts b/scripts/agent-hooks/hasChanges.ts new file mode 100644 index 00000000..81dc02fa --- /dev/null +++ b/scripts/agent-hooks/hasChanges.ts @@ -0,0 +1,6 @@ +import { execFileSync } from 'node:child_process'; + +/** Whether the working tree differs from HEAD: changed, staged or new files that git doesn't ignore. */ +export function hasChanges(root: string): boolean { + return execFileSync('git', ['status', '--porcelain'], { cwd: root, encoding: 'utf8' }).trim() !== ''; +} diff --git a/scripts/agent-hooks/lintableFile.ts b/scripts/agent-hooks/lintableFile.ts new file mode 100644 index 00000000..6058758c --- /dev/null +++ b/scripts/agent-hooks/lintableFile.ts @@ -0,0 +1,20 @@ +import { spawnSync } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { extname, isAbsolute, join, relative, resolve, sep } from 'node:path'; +import { LINTED_EXTENSIONS } from './constants.ts'; + +/** The first part of a path that leaves the folder it is relative to. */ +const PARENT = '..'; + +/** + * `path` from `root`, when oxlint should lint it: a script in the project that exists and git doesn't ignore (oxlint + * lints a file it is handed even if it is built output). + */ +export function lintableFile(root: string, path: string | undefined): string | undefined { + if (!path) return undefined; + const file = relative(root, resolve(root, path)); + if (isAbsolute(file) || file.split(sep)[0] === PARENT) return undefined; + if (!LINTED_EXTENSIONS.includes(extname(file)) || !existsSync(join(root, file))) return undefined; + const ignored = spawnSync('git', ['check-ignore', '--quiet', file], { cwd: root }).status === 0; + return ignored ? undefined : file; +} diff --git a/scripts/agent-hooks/readHookInput.ts b/scripts/agent-hooks/readHookInput.ts new file mode 100644 index 00000000..66da7edf --- /dev/null +++ b/scripts/agent-hooks/readHookInput.ts @@ -0,0 +1,12 @@ +import { readFileSync } from 'node:fs'; +import type { HookInput } from './types.ts'; + +/** STDIN, where Claude Code writes a hook's input. */ +const STDIN = 0; + +/** The JSON Claude Code writes to a hook's stdin, or nothing when it wrote none (a hook run by hand). */ +export function readHookInput(): HookInput { + const text = readFileSync(STDIN, 'utf8').trim(); + const input: unknown = text ? JSON.parse(text) : {}; + return input && typeof input === 'object' ? input : {}; +} diff --git a/scripts/agent-hooks/runScript.ts b/scripts/agent-hooks/runScript.ts new file mode 100644 index 00000000..95febe66 --- /dev/null +++ b/scripts/agent-hooks/runScript.ts @@ -0,0 +1,19 @@ +import { spawn } from 'node:child_process'; +import { NO_COLOR_ENV, NPM, WINDOWS } from './constants.ts'; +import type { ScriptResult } from './types.ts'; + +/** Runs `npm run

framed

\n`; +const XFO_FRAME_HTML = `

framed

\n`; -export function xfoHostHtml(port: number): string { +function xfoHostHtml(port: number): string { return ` @@ -110,7 +110,7 @@ export const API_JSON = `{"ok":true,"api":"json"}`; export const API_PUT_PATH = '/headers/api-put.json'; export const API_PUT = `{"ok":true,"api":"put"}`; /** Redirects to API_JSON_PATH on the same site. */ -export const API_REDIRECT_PATH = '/headers/api-redirect.json'; +const API_REDIRECT_PATH = '/headers/api-redirect.json'; /** * A page on 127.0.0.1 reading the `localhost` API with credentials. Each call @@ -119,7 +119,7 @@ export const API_REDIRECT_PATH = '/headers/api-redirect.json'; */ export const CORS_PATH = '/headers/cors.html'; -export function corsHtml(port: number): string { +function corsHtml(port: number): string { const api = `http://localhost:${port}`; return ` diff --git a/test/fixtures/networkPages.ts b/test/fixtures/networkPages.ts index 60c81079..e38aa87a 100644 --- a/test/fixtures/networkPages.ts +++ b/test/fixtures/networkPages.ts @@ -12,7 +12,7 @@ export const CART_PATH = '/network/api/cart'; export const GRAPHQL_PATH = '/network/graphql'; export const EVENTS_PATH = '/network/events'; export const BROKEN_PATH = '/network/api/broken'; -export const NETWORK_WORKER_PATH = '/network/worker.js'; +const NETWORK_WORKER_PATH = '/network/worker.js'; export const WORKER_DATA_PATH = '/network/api/worker'; /** A WebSocket that greets, then echoes (socketServer.ts). */ export const SOCKET_PATH = '/network/socket'; @@ -25,9 +25,9 @@ export const CART_JSON = JSON.stringify({ items: [{ id: 1, name: 'Alpha', price: /** What the GraphQL endpoint answers, whatever the operation. */ export const GRAPHQL_JSON = JSON.stringify({ data: { user: { name: 'Ada', id: 7 } } }); -export const WORKER_DATA_JSON = JSON.stringify({ source: 'upstream' }); -export const BROKEN_JSON = JSON.stringify({ error: 'boom' }); -export const ORDERS_JSON = JSON.stringify({ id: 1, state: 'created' }); +const WORKER_DATA_JSON = JSON.stringify({ source: 'upstream' }); +const BROKEN_JSON = JSON.stringify({ error: 'boom' }); +const ORDERS_JSON = JSON.stringify({ id: 1, state: 'created' }); /** How often the event stream sends, in ms. */ export const EVENT_INTERVAL_MS = 200; diff --git a/test/fixtures/site.ts b/test/fixtures/site.ts index 0463619c..2926e538 100644 --- a/test/fixtures/site.ts +++ b/test/fixtures/site.ts @@ -416,9 +416,9 @@ export async function startFixtureSite(port = 0): Promise { }; } -if (process.argv[1] && /site\.ts$/.test(process.argv[1])) { +if (process.argv[1]?.endsWith('site.ts')) { const port = Number(process.env.PORT ?? 5174); - startFixtureSite(port).then((site) => + void startFixtureSite(port).then((site) => console.log(`Demo site running:\n ${site.url}/store/ a shop checkout with a bug to fix\n ${site.url}/ files built to be awkward (gzip, SRI, hashed names)\n ${site.url}/frames.html cross-site and nested iframes\n ${site.url}/services.html services in iframes that log and message each other\n ${site.url}/workers/ dedicated, shared and service workers, and a worklet\n ${site.url}/maps.html source maps named every way there is\n ${site.url}/network/ a page that talks to a JSON API, GraphQL and an event stream\n ${site.url}/stack.html what Vue, Angular, Next.js and webpack leave in a page`), ); } diff --git a/test/fixtures/sourceMaps.ts b/test/fixtures/sourceMaps.ts index 425ca80c..1b0ee8dd 100644 --- a/test/fixtures/sourceMaps.ts +++ b/test/fixtures/sourceMaps.ts @@ -59,7 +59,7 @@ export function encodeVlq(values: number[]): string { } /** A version 3 source map of `generated` with one segment per anchor, as JSON. */ -export function buildSourceMap(generated: string, sources: MapSource[], anchors: MapAnchor[], extra: Record = {}): string { +function buildSourceMap(generated: string, sources: MapSource[], anchors: MapAnchor[], extra: Record = {}): string { const segments = anchors .map((anchor) => { const offset = generated.indexOf(anchor.snippet); @@ -316,7 +316,7 @@ export const THEME_CSS = `a{color:#2563eb;text-decoration:none}a:hover{text-decoration:underline}code{font-family:ui-monospace,monospace}\n` + `/*# sourceMappingURL=theme.css.map */\n`; -export const THEME_CSS_SOURCES: MapSource[] = [{ url: '../scss/theme.scss', content: THEME_SCSS }]; +const THEME_CSS_SOURCES: MapSource[] = [{ url: '../scss/theme.scss', content: THEME_SCSS }]; export const THEME_CSS_MAP = buildSourceMap(THEME_CSS, THEME_CSS_SOURCES, [ { snippet: '.title{', source: 0, line: 3, column: 0 }, diff --git a/test/helpers/electronApp.ts b/test/helpers/electronApp.ts index 1d9b4bb6..789244e8 100644 --- a/test/helpers/electronApp.ts +++ b/test/helpers/electronApp.ts @@ -14,7 +14,7 @@ export const built = existsSync(join(root, 'out/main/index.js')); export const sandboxArgs = process.getuid?.() === 0 ? ['--no-sandbox'] : []; /** Polls until `fn` returns a truthy value (usable outside tests, unlike expect.poll). */ -export async function waitFor(fn: () => T | undefined, timeout = 30_000): Promise { +async function waitFor(fn: () => T | undefined, timeout = 30_000): Promise { const deadline = Date.now() + timeout; for (;;) { const value = fn(); diff --git a/test/perf/apps/largeStores.ts b/test/perf/apps/largeStores.ts index c405b70b..b6eb6711 100644 --- a/test/perf/apps/largeStores.ts +++ b/test/perf/apps/largeStores.ts @@ -41,6 +41,7 @@ const useItems = defineStore('items', { let next = 0; createApp({ setup() { + // oxlint-disable-next-line react/rules-of-hooks -- Pinia's store in Vue's setup(), not a React hook const pinia = useItems(); return () => h('div', null, [ diff --git a/test/renderer/bridge-events.test.ts b/test/renderer/bridge-events.test.ts index 87e645f3..4bd08c3d 100644 --- a/test/renderer/bridge-events.test.ts +++ b/test/renderer/bridge-events.test.ts @@ -12,6 +12,8 @@ import { useOverrideStore } from '@/entities/override'; import { usePageStore } from '@/entities/page'; import { useResourceStore } from '@/entities/resource'; import { toRuleInput, useRuleStore } from '@/entities/rule'; +import { openRuleEditor } from '@/features/rule/edit'; +import { ruleForms } from '@/features/rule/edit/model/ruleForms'; import { useSourceMapStore } from '@/entities/source-map'; import { useWorkspaceStore } from '@/entities/workspace'; import { toggleBaseDiff } from '@/features/compare-changes'; @@ -257,16 +259,19 @@ describe('app event bridge', () => { it("takes the new rule list: a deleted rule's page closes, and an applied rule keeps only the edits typed since", () => { const [r1, r2, r3, r4] = [rule('r1'), rule('r2'), rule('r3'), rule('r4')]; useRuleStore.getState().setAll([r1, r2, r3, r4]); - const tabs = useTabStore.getState(); + for (const r of [r1, r2, r3, r4]) openRuleEditor(r); + const formOf = (id: string) => ruleForms.get(`page:rule:${id}`)!.form; + const edit = (id: string, value: CreateRuleInput) => { + formOf(id).setValue('match', value.match, { shouldDirty: true }); + formOf(id).setValue('resourceTypes', value.resourceTypes, { shouldDirty: true }); + }; const scripts = (input: CreateRuleInput): CreateRuleInput => ({ ...input, resourceTypes: ['Script'] }); - const draftOf = (base: CreateRuleInput, value = scripts(base)) => ({ base, value, rowKeys: [] }); // r1's edits were all applied; r2's pattern was applied while its types were still being edited. + edit('r1', { ...scripts(toRuleInput(r1)), match: { ...r1.match, pattern: ' https://b.com/* ' } }); const r2Typed = { ...scripts(toRuleInput(r2)), match: { ...r2.match, pattern: 'https://b.com/*' } }; - tabs.openPage({ id: 'page:rule:r1', page: 'rule', ruleId: 'r1', title: 'r1', draft: draftOf(toRuleInput(r1), { ...scripts(toRuleInput(r1)), match: { ...r1.match, pattern: ' https://b.com/* ' } }) }); - tabs.openPage({ id: 'page:rule:r2', page: 'rule', ruleId: 'r2', title: 'r2', draft: draftOf(toRuleInput(r2), r2Typed) }); - tabs.openPage({ id: 'page:rule:r3', page: 'rule', ruleId: 'r3', title: 'r3' }); - const r4Draft = draftOf(toRuleInput(r4)); - tabs.openPage({ id: 'page:rule:r4', page: 'rule', ruleId: 'r4', title: 'r4', draft: r4Draft }); + edit('r2', r2Typed); + edit('r4', scripts(toRuleInput(r4))); + const r4Form = ruleForms.get('page:rule:r4'); // r4 was only turned off (not part of the input), r3 deleted. const r1Applied: Rule = { ...rule('r1', 'https://b.com/*'), resourceTypes: ['Script'] }; @@ -276,10 +281,14 @@ describe('app event bridge', () => { expect(useRuleStore.getState().byId).toEqual({ r1: r1Applied, r2: r2Applied, r4: { ...r4, enabled: false } }); const pages = useTabStore.getState().pages; expect(pages.map((p) => p.id)).toEqual(['page:rule:r1', 'page:rule:r2', 'page:rule:r4']); - const drafts = pages.map((p) => ('draft' in p ? p.draft : null)); - expect(drafts[0]).toBeUndefined(); - expect(drafts[1]).toEqual({ base: toRuleInput(r2Applied), value: r2Typed, rowKeys: [] }); - expect(drafts[2]).toBe(r4Draft); + expect(pages.map((p) => 'dirty' in p && p.dirty)).toEqual([false, true, true]); + expect(formOf('r1').getValues()).toEqual(toRuleInput(r1Applied)); + expect(formOf('r2').getValues()).toEqual(r2Typed); + expect(ruleForms.get('page:rule:r4')).toBe(r4Form); + expect(formOf('r4').getValues()).toEqual(scripts(toRuleInput(r4))); + // r2's edits are now made to the version applied. + formOf('r2').reset(); + expect(formOf('r2').getValues()).toEqual(toRuleInput(r2Applied)); expect(useTabStore.getState().activeId).toBe('page:rule:r4'); }); diff --git a/test/renderer/data-flow.test.ts b/test/renderer/data-flow.test.ts index 1993f226..fb3ac562 100644 --- a/test/renderer/data-flow.test.ts +++ b/test/renderer/data-flow.test.ts @@ -256,7 +256,7 @@ describe('save override', () => { const create = deferred(); api.createOverride.mockReturnValue(create.promise); - saveTab(id); + void saveTab(id); const again = saveTab(id); create.resolve(meta('o9')); await again; diff --git a/test/renderer/pageTabs.test.ts b/test/renderer/pageTabs.test.ts index a9505c82..4e525a12 100644 --- a/test/renderer/pageTabs.test.ts +++ b/test/renderer/pageTabs.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { CreateRuleInput } from '../../src/shared/types'; -import { isPageDirty, PAGE_SCOPES, type PageTab, type RulePageDraft, type TabMeta, useTabStore } from '@/entities/editor-tab'; +import { isPageDirty, PAGE_SCOPES, type PageTab, type TabMeta, useTabStore } from '@/entities/editor-tab'; import { closeTab } from '@/features/close-tab'; import { closeSessionTabs } from '@/pages/editor/model/session'; @@ -19,12 +19,13 @@ vi.mock('@/shared/monaco', () => ({ })); const INPUT: CreateRuleInput = { action: 'block', match: { type: 'glob', pattern: 'https://ads.test/*', ignoreQuery: true }, resourceTypes: [] }; -const DRAFT: RulePageDraft = { base: INPUT, value: { ...INPUT, resourceTypes: ['Script'] }, rowKeys: [] }; +/** A page whose form holds edits not applied yet. */ +const EDITED = true; const WHATS_NEW: PageTab = { id: 'page:whats-new', page: 'whats-new', title: "What's New" }; const STACK: PageTab = { id: 'page:stack', page: 'stack', title: 'Page stack' }; const COMPONENT: PageTab = { id: 'page:component', page: 'component', title: 'CartItem' }; -const rulePage = (ruleId: string, draft?: RulePageDraft): PageTab => ({ id: `page:rule:${ruleId}`, page: 'rule', ruleId, title: ruleId, ...(draft ? { draft } : {}) }); -const newRulePage = (id: string, draft?: RulePageDraft): PageTab => ({ id: `page:new-rule:${id}`, page: 'new-rule', seed: INPUT, title: 'New block requests', ...(draft ? { draft } : {}) }); +const rulePage = (ruleId: string, dirty?: boolean): PageTab => ({ id: `page:rule:${ruleId}`, page: 'rule', ruleId, title: ruleId, ...(dirty ? { dirty } : {}) }); +const newRulePage = (id: string, dirty?: boolean): PageTab => ({ id: `page:new-rule:${id}`, page: 'new-rule', seed: INPUT, title: 'New block requests', ...(dirty ? { dirty } : {}) }); const file = (id: string): TabMeta => ({ id, url: `https://site.test/${id}.js`, kind: 'Script', originalHash: null, lite: false, dirty: false, saving: false }); const pageIds = () => useTabStore.getState().pages.map((p) => p.id); @@ -34,32 +35,32 @@ beforeEach(() => { }); describe('page tabs', () => { - it('opening an open page updates it in place, keeping its position and draft, and brings it to front', () => { + it('opening an open page updates it in place, keeping its position and whether it holds edits, and brings it to front', () => { const tabs = useTabStore.getState(); - tabs.openPage(rulePage('r1', DRAFT)); + tabs.openPage(rulePage('r1', EDITED)); tabs.openPage(rulePage('r2')); tabs.openPage({ id: 'page:rule:r1', page: 'rule', ruleId: 'r1', title: 'renamed' }); expect(pageIds()).toEqual(['page:rule:r1', 'page:rule:r2']); - expect(useTabStore.getState().pages[0]).toEqual({ ...rulePage('r1', DRAFT), title: 'renamed' }); + expect(useTabStore.getState().pages[0]).toEqual({ ...rulePage('r1', EDITED), title: 'renamed' }); expect(useTabStore.getState().activeId).toBe('page:rule:r1'); }); - it('keeps a rule page’s draft, and drops it with undefined; What’s New takes none', () => { + it('marks a rule page as holding edits, or not; What’s New holds none', () => { const tabs = useTabStore.getState(); tabs.openPage(WHATS_NEW); tabs.openPage(rulePage('r1')); const before = useTabStore.getState().pages; - tabs.setPageDraft('page:rule:r1', DRAFT); - expect(useTabStore.getState().pages[1]).toMatchObject({ draft: DRAFT }); - tabs.setPageDraft('page:rule:r1', undefined); + tabs.setPageDirty('page:rule:r1', true); + expect(isPageDirty(useTabStore.getState().pages[1]!)).toBe(true); + tabs.setPageDirty('page:rule:r1', false); expect(isPageDirty(useTabStore.getState().pages[1]!)).toBe(false); const unchanged = useTabStore.getState().pages; - tabs.setPageDraft('page:rule:r1', undefined); - tabs.setPageDraft(WHATS_NEW.id, DRAFT); - tabs.setPageDraft('page:rule:gone', DRAFT); + tabs.setPageDirty('page:rule:r1', false); + tabs.setPageDirty(WHATS_NEW.id, true); + tabs.setPageDirty('page:rule:gone', true); expect(useTabStore.getState().pages).toBe(unchanged); expect(unchanged).not.toBe(before); }); @@ -86,22 +87,22 @@ describe('page tabs', () => { it('tells which pages would lose edits, and which belong to the workspace', () => { expect(isPageDirty(WHATS_NEW)).toBe(false); expect(isPageDirty(rulePage('r1'))).toBe(false); - expect(isPageDirty(rulePage('r1', DRAFT))).toBe(true); + expect(isPageDirty(rulePage('r1', EDITED))).toBe(true); expect(isPageDirty(newRulePage('n1'))).toBe(false); - expect(isPageDirty(newRulePage('n1', DRAFT))).toBe(true); + expect(isPageDirty(newRulePage('n1', EDITED))).toBe(true); expect(isPageDirty(STACK)).toBe(false); expect(isPageDirty(COMPONENT)).toBe(false); expect(PAGE_SCOPES).toEqual({ 'whats-new': 'app', stack: 'app', rule: 'workspace', 'new-rule': 'workspace', component: 'workspace' }); }); it("closes the workspace's rule and new-rule pages with its tabs; What's New and the Page stack stay", () => { - useTabStore.setState({ tabs: [file('a')], pages: [rulePage('r1', DRAFT), WHATS_NEW, newRulePage('n1'), STACK], activeId: 'page:rule:r1' }); + useTabStore.setState({ tabs: [file('a')], pages: [rulePage('r1', EDITED), WHATS_NEW, newRulePage('n1'), STACK], activeId: 'page:rule:r1' }); closeSessionTabs(); expect(useTabStore.getState()).toMatchObject({ tabs: [], pages: [WHATS_NEW, STACK], activeId: WHATS_NEW.id }); }); it('asks before closing a rule page with unapplied edits, and not one without', async () => { - useTabStore.setState({ pages: [rulePage('r1', DRAFT), newRulePage('n1')], activeId: 'page:rule:r1' }); + useTabStore.setState({ pages: [rulePage('r1', EDITED), newRulePage('n1')], activeId: 'page:rule:r1' }); confirm.mockResolvedValueOnce(false); await closeTab('page:rule:r1'); expect(confirm).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ title: 'Discard your changes to r1?', tone: 'danger' })); diff --git a/test/renderer/pretty-positions.test.ts b/test/renderer/pretty-positions.test.ts index 4ebe0f76..e7e8b761 100644 --- a/test/renderer/pretty-positions.test.ts +++ b/test/renderer/pretty-positions.test.ts @@ -7,7 +7,7 @@ import type { LoadedMap } from '@/shared/lib/source-map/host/types'; import { ESBUILD_APP_CSS, ESBUILD_APP_CSS_MAP, ESBUILD_APP_JS, ESBUILD_APP_JS_MAP } from '../fixtures/esbuildApp'; import { MAIN_JS, MAIN_JS_ANCHORS, STYLE_CSS, THEME_CSS } from '../fixtures/sourceMaps'; -const code = (text: string) => [...text].filter((c) => !isAlignWhitespace(c.charCodeAt(0))).join(''); +const code = (text: string) => text.split('').filter((c) => !isAlignWhitespace(c.charCodeAt(0))).join(''); const lineOf = (text: string, offset: number) => text.slice(0, offset).split('\n').length; /** A map entry holding `raw`, as the worker keeps one, lined up with `view`. */ @@ -125,7 +125,7 @@ describe('lining a pretty-printed tab up with the text its map describes', () => for (let round = 0; round < 50; round++) { const raw = Array.from({ length: 200 }, () => tokens[random(tokens.length)]).join(''); // Add or drop whitespace between code characters, as pretty-printing does. - const view = [...raw].map((c) => (isAlignWhitespace(c.charCodeAt(0)) ? ['', ' ', '\n '][random(3)] : c + ['', ' ', '\n'][random(3)])).join(''); + const view = raw.split('').map((c) => (isAlignWhitespace(c.charCodeAt(0)) ? ['', ' ', '\n '][random(3)] : c + ['', ' ', '\n'][random(3)])).join(''); const { toView, toRaw } = lineUp(raw, view); const rawCode = codeOffsets(raw); const viewCode = codeOffsets(view); diff --git a/test/renderer/rules.test.ts b/test/renderer/rules.test.ts index 877a0fd8..558f79ec 100644 --- a/test/renderer/rules.test.ts +++ b/test/renderer/rules.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { DEFAULT_SETTINGS, type CreateRuleInput, type HeaderEdit, type ResourceEntry, type Rule, type RulePatch } from '../../src/shared/types'; import { saveActive } from '@/app/model/bridge/commands/saveActive'; import type { MenuAction } from '@/shared/ui/menu'; -import { type RulePageDraft, useTabStore } from '@/entities/editor-tab'; +import { isPageDirty, useTabStore } from '@/entities/editor-tab'; import { blockingRuleFor, HEADER_PRESETS, @@ -18,7 +18,8 @@ import { import { useSettingsStore } from '@/entities/settings'; import { useWorkspaceStore } from '@/entities/workspace'; import { deleteRule } from '@/features/rule/delete'; -import { applyRulePage, createRulePage, openNewRule, openRuleEditor, RULE_SEEDS, rulePageId, setRuleDraft } from '@/features/rule/edit'; +import { applyRulePage, createRulePage, openNewRule, openRuleEditor, RULE_SEEDS, rulePageId } from '@/features/rule/edit'; +import { ruleForms } from '@/features/rule/edit/model/ruleForms'; import { patchFor } from '@/features/rule/edit/model/patchFor'; import { ruleNotes } from '@/features/rule/edit/ui/RuleForm/ruleNotes'; import { withPreset } from '@/features/rule/edit/ui/RuleForm/withPreset'; @@ -59,7 +60,15 @@ function rule(overrides: Partial & Pick = { action: 'block return { ...base, ...(overrides.action === 'headers' ? { headers: [{ operation: 'set', name: 'X-A', value: '1' }] } : {}), ...overrides } as Rule; } const blockInput = (pattern = 'https://ads.test/*'): CreateRuleInput => ({ action: 'block', match: { type: 'glob', pattern, ignoreQuery: true }, resourceTypes: [] }); -const draftOf = (base: CreateRuleInput, value: CreateRuleInput, rowKeys: string[] = []): RulePageDraft => ({ base, value, rowKeys }); +const formOf = (pageId: string) => ruleForms.get(pageId)!.form; +const pageOf = (pageId: string) => useTabStore.getState().pages.find((p) => p.id === pageId); +/** Types `value` into a rule page's form, as its fields do. */ +function edit(pageId: string, value: CreateRuleInput): void { + const form = formOf(pageId); + form.setValue('match', value.match, { shouldDirty: true }); + form.setValue('resourceTypes', value.resourceTypes, { shouldDirty: true }); + if ('headers' in value) form.setValue('headers', value.headers, { shouldDirty: true }); +} const entry = (url: string, extra: Partial = {}): ResourceEntry => ({ url, kind: 'Script', mimeType: 'text/javascript', status: 200, ...extra }); const setReload = (on: boolean) => useSettingsStore.setState({ settings: { ...DEFAULT_SETTINGS, autoReloadOnSave: on } }); @@ -176,14 +185,24 @@ describe('toggling and deleting rules', () => { }); describe('rule pages', () => { - it('keeps edits as a draft, and none once they lead back to where they started', () => { - useTabStore.getState().openPage({ id: 'page:rule:r1', page: 'rule', ruleId: 'r1', title: 'r1' }); - const base = blockInput(); - const edited = draftOf(base, { ...base, resourceTypes: ['Script'] }); - setRuleDraft('page:rule:r1', edited); - expect(useTabStore.getState().pages[0]).toMatchObject({ draft: edited }); - setRuleDraft('page:rule:r1', draftOf(base, { ...base, match: { ...base.match } })); - expect(useTabStore.getState().pages[0]).not.toHaveProperty('draft', expect.anything()); + it('marks the tab while its form holds edits, and not once they lead back to where they started', () => { + const r = rule({ id: 'r1', action: 'block' }); + openRuleEditor(r); + const saved = toRuleInput(r); + edit('page:rule:r1', { ...saved, resourceTypes: ['Script'] }); + expect(pageOf('page:rule:r1')).toMatchObject({ dirty: true }); + edit('page:rule:r1', { ...saved, match: { ...saved.match } }); + expect(isPageDirty(pageOf('page:rule:r1')!)).toBe(false); + }); + + it('starts a page closed and opened again from the rule as saved', () => { + const r = rule({ id: 'r2', action: 'block' }); + openRuleEditor(r); + edit(rulePageId('r2'), { ...toRuleInput(r), resourceTypes: ['Image'] }); + useTabStore.getState().remove(rulePageId('r2')); + openRuleEditor(r); + expect(formOf(rulePageId('r2')).getValues()).toEqual(toRuleInput(r)); + expect(isPageDirty(pageOf(rulePageId('r2'))!)).toBe(false); }); it('sends only the fields that changed', () => { @@ -203,7 +222,7 @@ describe('rule pages', () => { expect(RULE_SEEDS.block().match).not.toBe(RULE_SEEDS.block().match); }); - it('opens one page per rule, and new-rule pages titled by what they do', () => { + it('opens one page per rule, and new-rule pages titled by what they do, each with its form', () => { const r = rule({ id: 'r9', action: 'block', match: { type: 'glob', pattern: 'https://ads.test/*', ignoreQuery: true } }); openRuleEditor(r); openRuleEditor(r); @@ -212,18 +231,20 @@ describe('rule pages', () => { expect(pages.map((p) => p.title)).toEqual(['https://ads.test/*', 'New allow cross-origin requests']); expect(pages[0]).toMatchObject({ id: rulePageId('r9'), page: 'rule', ruleId: 'r9' }); expect(pages[1]!.id).toMatch(/^page:new-rule:/); + expect(formOf(pages[0]!.id).getValues()).toEqual(toRuleInput(r)); + expect(formOf(pages[1]!.id).getValues()).toEqual(RULE_SEEDS.cors()); useWorkspaceStore.setState({ switchingTo: 'w2' }); openNewRule(RULE_SEEDS.block()); expect(useTabStore.getState().pages).toHaveLength(2); }); - it('applies valid edits: only what changed is sent, the draft goes and the tab is retitled', async () => { + it('applies valid edits: only what changed is sent, the edits go and the tab is retitled', async () => { const r = rule({ id: 'r1', action: 'block', resourceTypes: ['Script'] }); useRuleStore.getState().setAll([r]); + openRuleEditor(r); const saved = toRuleInput(r); - const value: CreateRuleInput = { ...saved, match: { type: 'glob', pattern: ' https://ads.test/* ', ignoreQuery: true } }; - useTabStore.getState().openPage({ id: 'page:rule:r1', page: 'rule', ruleId: 'r1', title: 'r1.js · site.test', draft: draftOf(saved, value) }); + edit('page:rule:r1', { ...saved, match: { type: 'glob', pattern: ' https://ads.test/* ', ignoreQuery: true } }); const updated = { ...r, match: { type: 'glob' as const, pattern: 'https://ads.test/*', ignoreQuery: true } }; api.updateRule.mockResolvedValueOnce(updated); @@ -231,7 +252,9 @@ describe('rule pages', () => { expect(api.updateRule).toHaveBeenCalledExactlyOnceWith('r1', { match: updated.match } satisfies RulePatch); expect(useRuleStore.getState().byId.r1).toEqual(updated); - expect(useTabStore.getState().pages).toEqual([{ id: 'page:rule:r1', page: 'rule', ruleId: 'r1', title: 'https://ads.test/*' }]); + expect(useTabStore.getState().pages).toEqual([{ id: 'page:rule:r1', page: 'rule', ruleId: 'r1', title: 'https://ads.test/*', dirty: false }]); + // The form shows the rule as saved: the spaces around the pattern are gone. + expect(formOf('page:rule:r1').getValues()).toEqual(toRuleInput(updated)); expect(toasts()[0]).toMatchObject({ title: 'Rule updated' }); expect(api.reload).toHaveBeenCalledTimes(1); }); @@ -239,48 +262,56 @@ describe('rule pages', () => { it('keeps what is typed while an Apply runs, as edits to the version it saved', async () => { const r = rule({ id: 'r1', action: 'block' }); useRuleStore.getState().setAll([r]); + openRuleEditor(r); const saved = toRuleInput(r); - const sent: CreateRuleInput = { ...saved, resourceTypes: ['Script'] }; - useTabStore.getState().openPage({ id: 'page:rule:r1', page: 'rule', ruleId: 'r1', title: 'r1', draft: draftOf(saved, sent) }); + edit('page:rule:r1', { ...saved, resourceTypes: ['Script'] }); let resolve!: (value: Rule) => void; api.updateRule.mockReturnValueOnce(new Promise((res) => (resolve = res))); const done = applyRulePage('page:rule:r1'); - const typed: CreateRuleInput = { ...sent, match: { ...sent.match, pattern: 'https://site.test/r1.mjs' } }; - setRuleDraft('page:rule:r1', draftOf(saved, typed)); + // The form is checked first; typing goes on while the edits are on their way. + await vi.waitFor(() => expect(api.updateRule).toHaveBeenCalled()); + formOf('page:rule:r1').setValue('match.pattern', 'https://site.test/r1.mjs', { shouldDirty: true }); const updated = { ...r, resourceTypes: ['Script' as const] }; resolve(updated); await done; - expect(useTabStore.getState().pages[0]).toMatchObject({ draft: draftOf(toRuleInput(updated), typed) }); + const typed: CreateRuleInput = { ...toRuleInput(updated), match: { ...saved.match, pattern: 'https://site.test/r1.mjs' } }; + expect(formOf('page:rule:r1').getValues()).toEqual(typed); + expect(pageOf('page:rule:r1')).toMatchObject({ dirty: true }); + // Revert goes back to the version the Apply saved. + formOf('page:rule:r1').reset(); + expect(formOf('page:rule:r1').getValues()).toEqual(toRuleInput(updated)); }); it('does not send invalid edits, nor edits made to an older version of the rule', async () => { const r = rule({ id: 'r1', action: 'headers' }); useRuleStore.getState().setAll([r]); + openRuleEditor(r); const saved = toRuleInput(r); - const invalid = { ...saved, headers: [{ operation: 'set' as const, name: 'Set-Cookie', value: 'a=1' }] }; - useTabStore.getState().openPage({ id: 'page:rule:r1', page: 'rule', ruleId: 'r1', title: 'r1', draft: draftOf(saved, invalid) }); + edit('page:rule:r1', { action: 'headers', match: saved.match, resourceTypes: saved.resourceTypes, headers: [{ operation: 'set', name: 'Set-Cookie', value: 'a=1' }] }); await applyRulePage('page:rule:r1'); expect(api.updateRule).not.toHaveBeenCalled(); expect(toasts()[0]).toMatchObject({ title: 'Rule not applied', tone: 'danger', description: expect.stringContaining('Set-Cookie') }); - const stale = { ...saved, resourceTypes: ['XHR' as const] }; - useTabStore.getState().setPageDraft('page:rule:r1', draftOf(stale, { ...stale, resourceTypes: [] })); + // The rule changed without the page hearing of it: its edits were made to the version before. + edit('page:rule:r1', { ...saved, resourceTypes: ['Image'] }); + useRuleStore.getState().setAll([{ ...r, resourceTypes: ['XHR'] }]); await applyRulePage('page:rule:r1'); expect(api.updateRule).not.toHaveBeenCalled(); expect(toast).toHaveBeenCalledTimes(1); }); it('creates the rule a new-rule page describes, and swaps the page for the rule’s own', async () => { - const seed = RULE_SEEDS.block(); + openNewRule(RULE_SEEDS.block()); + const pageId = useTabStore.getState().pages[0]!.id; const value = blockInput('https://www.google-analytics.com/*'); - useTabStore.getState().openPage({ id: 'page:new-rule:n1', page: 'new-rule', seed, title: 'New block requests', draft: draftOf(seed, value) }); + edit(pageId, value); const created = rule({ id: 'c1', action: 'block', match: value.match }); api.createRule.mockResolvedValueOnce(created); - await createRulePage('page:new-rule:n1'); + await createRulePage(pageId); expect(api.createRule).toHaveBeenCalledExactlyOnceWith(value); expect(useTabStore.getState().pages).toEqual([{ id: 'page:rule:c1', page: 'rule', ruleId: 'c1', title: 'https://www.google-analytics.com/*' }]); @@ -290,8 +321,9 @@ describe('rule pages', () => { }); it('does not create an invalid rule', async () => { - useTabStore.getState().openPage({ id: 'page:new-rule:n1', page: 'new-rule', seed: RULE_SEEDS.headers(), title: 'New' }); - await createRulePage('page:new-rule:n1'); + openNewRule(RULE_SEEDS.headers()); + const pageId = useTabStore.getState().pages[0]!.id; + await createRulePage(pageId); expect(api.createRule).not.toHaveBeenCalled(); expect(toasts()[0]).toMatchObject({ title: 'Rule not added', tone: 'danger' }); expect(useTabStore.getState().pages).toHaveLength(1); @@ -324,15 +356,17 @@ describe('results that arrive after a workspace switch', () => { expect(toasts().at(-1)).toMatchObject({ title: 'Blocked', description: undefined }); useWorkspaceStore.setState({ activeId: 'w1' }); - useTabStore.getState().openPage({ id: 'page:new-rule:n1', page: 'new-rule', seed: blockInput(), title: 'New' }); + openNewRule(blockInput()); + const newPageId = useTabStore.getState().pages[0]!.id; afterSwitch(api.createRule, rule({ id: 'c1', action: 'block' })); - await createRulePage('page:new-rule:n1'); + await createRulePage(newPageId); expect(useRuleStore.getState().byId).toEqual({}); - expect(useTabStore.getState().pages.map((p) => p.id)).toEqual(['page:new-rule:n1']); + expect(useTabStore.getState().pages.map((p) => p.id)).toEqual([newPageId]); useWorkspaceStore.setState({ activeId: 'w1' }); useRuleStore.getState().setAll([r]); - useTabStore.getState().openPage({ id: 'page:rule:r1', page: 'rule', ruleId: 'r1', title: 'r1', draft: draftOf(toRuleInput(r), { ...toRuleInput(r), resourceTypes: ['Script'] }) }); + openRuleEditor(r); + edit('page:rule:r1', { ...toRuleInput(r), resourceTypes: ['Script'] }); afterSwitch(api.updateRule, { ...r, resourceTypes: ['Script'] }); await applyRulePage('page:rule:r1'); expect(useRuleStore.getState().byId.r1).toEqual(r); @@ -350,14 +384,15 @@ describe('saving what is in front', () => { const r = rule({ id: 'r1', action: 'block' }); useRuleStore.getState().setAll([r]); - const saved = toRuleInput(r); - useTabStore.getState().openPage({ id: 'page:rule:r1', page: 'rule', ruleId: 'r1', title: 'r1', draft: draftOf(saved, { ...saved, resourceTypes: ['Script'] }) }); + openRuleEditor(r); + edit('page:rule:r1', { ...toRuleInput(r), resourceTypes: ['Script'] }); api.updateRule.mockResolvedValueOnce({ ...r, resourceTypes: ['Script'] }); saveActive(); await vi.waitFor(() => expect(api.updateRule).toHaveBeenCalledExactlyOnceWith('r1', { resourceTypes: ['Script'] })); const seed = RULE_SEEDS.cors(); - useTabStore.getState().openPage({ id: 'page:new-rule:n1', page: 'new-rule', seed, title: 'New', draft: draftOf(seed, { ...seed, match: { ...seed.match, pattern: 'https://api.test/*' } }) }); + openNewRule(seed); + edit(useTabStore.getState().activeId!, { ...seed, match: { ...seed.match, pattern: 'https://api.test/*' } }); api.createRule.mockResolvedValueOnce(rule({ id: 'c1', action: 'cors' })); saveActive(); await vi.waitFor(() => expect(api.createRule).toHaveBeenCalledTimes(1)); @@ -458,13 +493,10 @@ describe('rule entity', () => { }); describe('header presets', () => { - it('adds a preset’s changes, in place of blank rows, with new row keys', () => { + it('adds a preset’s changes, in place of blank rows', () => { const blank: HeaderEdit = { operation: 'set', name: '', value: '' }; const kept: HeaderEdit = { operation: 'set', name: 'X-A', value: '1' }; - const next = withPreset([blank, kept], ['saved-0', 'saved-1'], NO_STORE_PRESET); - expect(next.headers).toEqual([kept, { operation: 'set', name: 'Cache-Control', value: 'no-store' }]); - expect(next.rowKeys[0]).toBe('saved-1'); - expect(next.rowKeys[1]).toMatch(/^row-/); + expect(withPreset([blank, kept], NO_STORE_PRESET)).toEqual([kept, { operation: 'set', name: 'Cache-Control', value: 'no-store' }]); expect(HEADER_PRESETS.map((p) => p.label)).toContain(REMOVE_CSP_PRESET.label); expect(REMOVE_CSP_PRESET.edits.map((e) => e.name)).toEqual(['Content-Security-Policy', 'Content-Security-Policy-Report-Only']); }); diff --git a/test/renderer/session.test.ts b/test/renderer/session.test.ts index e4bb8b7f..36fa4f9a 100644 --- a/test/renderer/session.test.ts +++ b/test/renderer/session.test.ts @@ -231,11 +231,10 @@ describe('session sync', () => { it('on close, reports unapplied rule edits as not kept, so closing asks first', async () => { stopSync = startSessionSync(); - const input = { action: 'block' as const, match: { type: 'glob' as const, pattern: 'https://a.test/*', ignoreQuery: true }, resourceTypes: [] }; useTabStore.getState().openPage({ id: 'page:rule:r1', page: 'rule', ruleId: 'r1', title: 'r1' }); expect(await pageSession.flush()).toBe(true); - useTabStore.getState().setPageDraft('page:rule:r1', { base: input, value: { ...input, resourceTypes: ['Script'] }, rowKeys: [] }); + useTabStore.getState().setPageDirty('page:rule:r1', true); expect(await pageSession.flush()).toBe(false); }); }); @@ -378,9 +377,8 @@ describe('switching workspaces', () => { it('asks before losing a rule page’s unapplied edits; cancelling stays in the workspace', async () => { useRuleStore.getState().setAll([rule('r1')]); - const input = { action: 'block' as const, match: rule('r1').match, resourceTypes: [] }; useTabStore.getState().openPage({ id: 'page:rule:r1', page: 'rule', ruleId: 'r1', title: 'r1' }); - useTabStore.getState().setPageDraft('page:rule:r1', { base: input, value: { ...input, resourceTypes: ['Script'] }, rowKeys: [] }); + useTabStore.getState().setPageDirty('page:rule:r1', true); confirm.mockResolvedValueOnce(false); mainSwitchesTo('wsb00000', { url: '', tabs: [], activeTabId: null }); diff --git a/test/smoke/lib.ts b/test/smoke/lib.ts index a5c7b644..2f5d69ab 100644 --- a/test/smoke/lib.ts +++ b/test/smoke/lib.ts @@ -3,7 +3,7 @@ import { spawn, type ChildProcess } from 'node:child_process'; import { createServer } from 'node:net'; import { chromium, type Browser, type Page } from 'playwright-core'; -export async function freePort(): Promise { +async function freePort(): Promise { const server = createServer(); await new Promise((done) => server.listen(0, '127.0.0.1', done)); const { port } = server.address() as { port: number }; diff --git a/test/unit/agentHooks.test.ts b/test/unit/agentHooks.test.ts new file mode 100644 index 00000000..dc7348fe --- /dev/null +++ b/test/unit/agentHooks.test.ts @@ -0,0 +1,23 @@ +import { createRequire } from 'node:module'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { lintableFile } from '../../scripts/agent-hooks/lintableFile'; + +const root = resolve(__dirname, '../..'); +/** A script git ignores: an installed package's. */ +const installed = createRequire(import.meta.url).resolve('vitest'); + +describe("Claude Code's lint hook", () => { + it('lints scripts in the project, by their path from its root', () => { + expect(lintableFile(root, resolve(root, 'src/main/index.ts'))).toBe('src/main/index.ts'); + expect(lintableFile(root, 'scripts/check-structure.ts')).toBe('scripts/check-structure.ts'); + }); + + it('leaves alone other files, files outside the project, ignored and missing ones', () => { + expect(lintableFile(root, undefined)).toBeUndefined(); + expect(lintableFile(root, resolve(root, 'README.md'))).toBeUndefined(); + expect(lintableFile(root, resolve(root, '../elsewhere.ts'))).toBeUndefined(); + expect(lintableFile(root, installed)).toBeUndefined(); + expect(lintableFile(root, resolve(root, 'src/missing.ts'))).toBeUndefined(); + }); +}); diff --git a/test/unit/checkCommitMessage.test.ts b/test/unit/checkCommitMessage.test.ts new file mode 100644 index 00000000..d7d9514d --- /dev/null +++ b/test/unit/checkCommitMessage.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; +import { agentCredits } from '../../scripts/commit-message/agentCredits'; +import { isAgentIdentity } from '../../scripts/commit-message/isAgentIdentity'; + +const message = (...body: string[]) => ['Fix the page window race', '', 'Wait for the window to close first.', '', ...body].join('\n'); + +describe('commit-message check', () => { + it('passes a message that credits people only', () => { + expect(agentCredits(message())).toEqual([]); + expect(agentCredits(message('Co-Authored-By: Ann Lee ', 'Co-authored-by: Claude Monet '))).toEqual([]); + // Prose about the rule isn't a trailer. + expect(agentCredits(message('Refuse Co-Authored-By trailers that name an agent, and Claude-Session links.'))).toEqual([]); + }); + + it('finds co-author trailers that name an agent, by address, GitHub account or name', () => { + const trailers = [ + 'Co-Authored-By: Claude Opus 5.5 ', + 'co-authored-by: Cursor Agent ', + 'Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>', + 'Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>', + 'Co-Authored-By: Claude ', + ]; + expect(agentCredits(message(...trailers))).toEqual(trailers); + }); + + it('finds session links and "Generated with" lines, whatever they name', () => { + const lines = ['Claude-Session: https://claude.ai/code/session_01ABC', 'See https://claude.ai/code/session_01ABC', '🤖 Generated with [Claude Code](https://claude.com/claude-code)']; + expect(agentCredits(message(...lines))).toEqual(lines); + }); + + it("leaves out git's comments and the diff under the scissors line", () => { + const verbose = message('# Please enter the commit message for your changes.', '# Co-Authored-By: Claude ', '# ------------------------ >8 ------------------------', '+Claude-Session: https://claude.ai/code/session_01ABC', 'Co-Authored-By: Claude '); + expect(agentCredits(verbose)).toEqual([]); + }); + + it("tells an agent's identity from a person's", () => { + expect(isAgentIdentity('Claude ')).toBe(true); + expect(isAgentIdentity('Anyone ')).toBe(true); + expect(isAgentIdentity('copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>')).toBe(true); + expect(isAgentIdentity('Oleh H <12345+olehwebdev@users.noreply.github.com>')).toBe(false); + expect(isAgentIdentity('Ann Lee ')).toBe(false); + expect(isAgentIdentity('not an identity')).toBe(false); + }); +}); diff --git a/test/unit/matcher.test.ts b/test/unit/matcher.test.ts index 453045bf..7390cd81 100644 --- a/test/unit/matcher.test.ts +++ b/test/unit/matcher.test.ts @@ -6,8 +6,9 @@ import { stripQuery, suggestHashGlob, toCdpUrlPattern, - validateMatcher, + urlMatcherSchema, } from '../../src/shared/matcher'; +import { firstIssue } from '../../src/shared/validation'; describe('a match type this build does not know', () => { // A newer version's type, or a hand edit of overrides.json, must not take down interception. @@ -80,11 +81,11 @@ describe('compileMatcher', () => { }); }); -describe('validateMatcher', () => { +describe('urlMatcherSchema', () => { it('rejects empty patterns and bad regexes', () => { - expect(validateMatcher({ type: 'exact', pattern: ' ', ignoreQuery: true })).toMatch(/empty/); - expect(validateMatcher({ type: 'regex', pattern: '([', ignoreQuery: true })).toMatch(/Invalid regular expression/); - expect(validateMatcher({ type: 'glob', pattern: 'https://a.com/*.js', ignoreQuery: true })).toBeNull(); + expect(firstIssue(urlMatcherSchema, { type: 'exact', pattern: ' ', ignoreQuery: true })).toMatch(/empty/); + expect(firstIssue(urlMatcherSchema, { type: 'regex', pattern: '([', ignoreQuery: true })).toMatch(/Invalid regular expression/); + expect(firstIssue(urlMatcherSchema, { type: 'glob', pattern: 'https://a.com/*.js', ignoreQuery: true })).toBeNull(); }); }); diff --git a/test/unit/pageInterception.test.ts b/test/unit/pageInterception.test.ts index fe1059d1..f0024644 100644 --- a/test/unit/pageInterception.test.ts +++ b/test/unit/pageInterception.test.ts @@ -1041,7 +1041,7 @@ describe('PageInterception', () => { describe('rules', () => { const requestPatterns = (c: { params?: Record }) => - (c.params?.patterns as Array<{ requestStage: string }>).filter((p) => p.requestStage === 'Request'); + (c.params!.patterns as Array<{ requestStage: string }>).filter((p) => p.requestStage === 'Request'); it("blocks a request paused on an iframe's session, on that session (every engine reads the rules)", async () => { const { cdp, events } = await setup([], { rules: [blockWidget] }); diff --git a/test/unit/ruleSanitizers.test.ts b/test/unit/ruleSanitizers.test.ts index 7b2ff3fd..676aee27 100644 --- a/test/unit/ruleSanitizers.test.ts +++ b/test/unit/ruleSanitizers.test.ts @@ -1,78 +1,78 @@ import { describe, expect, it } from 'vitest'; -import { sanitizeHeaderEdits } from '../../src/main/store/sanitizeHeaderEdits'; -import { sanitizeMatcher } from '../../src/main/store/sanitizeMatcher'; -import { sanitizeResourceTypes } from '../../src/main/store/sanitizeResourceTypes'; -import { sanitizeRuleInput } from '../../src/main/store/sanitizeRuleInput'; -import { sanitizeRulePatch } from '../../src/main/store/sanitizeRulePatch'; +import { parseInput } from '../../src/main/store/parseInput'; +import { rulePatchSchema } from '../../src/main/store/rulePatchSchema'; import { sanitizeStoredRule } from '../../src/main/store/sanitizeStoredRule'; import { toRule } from '../../src/main/store/toRule'; -import { MAX_HEADER_EDITS } from '../../src/shared/rules'; +import { MAX_HEADER_EDITS, resourceTypesSchema, ruleInputSchema } from '../../src/shared/rules'; const match = { type: 'glob', pattern: 'https://a.com/*', ignoreQuery: true }; const edit = { operation: 'set', name: 'X-Mode', value: 'on' }; +const parseRule = (input: unknown) => parseInput(ruleInputSchema, input, 'rule'); +const parsePatch = (input: unknown) => parseInput(rulePatchSchema, input, 'rule'); -describe('sanitizeRuleInput (IPC)', () => { +describe('reading a rule to create (IPC)', () => { it("keeps only the known fields of the input's action, as fresh copies", () => { const input = { action: 'headers', match: { ...match, extra: 1 }, resourceTypes: ['Script'], headers: [{ ...edit, extra: 2 }], id: 'ffffffff', enabled: false, workspaceId: 'x' }; - const clean = sanitizeRuleInput(input); + const clean = parseRule(input); expect(clean).toEqual({ action: 'headers', match, resourceTypes: ['Script'], headers: [edit] }); expect(clean.match).not.toBe(input.match); - expect(sanitizeRuleInput({ action: 'block', match, resourceTypes: [], headers: [edit] })).toEqual({ action: 'block', match, resourceTypes: [] }); - expect(sanitizeRuleInput({ action: 'cors', match, resourceTypes: ['XHR'] })).toEqual({ action: 'cors', match, resourceTypes: ['XHR'] }); + expect(parseRule({ action: 'block', match, resourceTypes: [], headers: [edit] })).toEqual({ action: 'block', match, resourceTypes: [] }); + expect(parseRule({ action: 'cors', match, resourceTypes: ['XHR'] })).toEqual({ action: 'cors', match, resourceTypes: ['XHR'] }); }); it('puts request types in their canonical order, once each', () => { - expect(sanitizeRuleInput({ action: 'block', match, resourceTypes: ['XHR', 'Document', 'XHR', 'Script'] }).resourceTypes).toEqual(['Document', 'Script', 'XHR']); + expect(parseRule({ action: 'block', match, resourceTypes: ['XHR', 'Document', 'XHR', 'Script'] }).resourceTypes).toEqual(['Document', 'Script', 'XHR']); }); - it('names the field of the wrong shape', () => { + it('names the field of the wrong shape, and says what is wrong with a value', () => { const valid = { action: 'headers', match, resourceTypes: [], headers: [edit] }; const cases: Array<[unknown, string]> = [ [null, 'Invalid rule: not an object'], [[valid], 'Invalid rule: not an object'], - [{ ...valid, action: 'redirect' }, 'Invalid rule: action'], + [{ ...valid, action: 'redirect' }, 'Unknown rule action'], [{ ...valid, match: 'https://a.com/' }, 'Invalid rule: match'], [{ ...valid, match: { ...match, type: 'prefix' } }, 'Invalid rule: match'], [{ ...valid, match: { ...match, pattern: 1 } }, 'Invalid rule: match'], [{ ...valid, match: { ...match, ignoreQuery: 'yes' } }, 'Invalid rule: match'], + [{ ...valid, match: { ...match, pattern: ' ' } }, 'Pattern is empty'], [{ ...valid, resourceTypes: 'Script' }, 'Invalid rule: resourceTypes'], - [{ ...valid, resourceTypes: ['Script', 'WebSocket'] }, 'Invalid rule: resourceTypes'], + [{ ...valid, resourceTypes: ['Script', 'WebSocket'] }, 'Unknown request type'], [{ ...valid, headers: undefined }, 'Invalid rule: headers'], - [{ ...valid, headers: [{ ...edit, operation: 'append' }] }, 'Invalid rule: headers'], + [{ ...valid, headers: [{ ...edit, operation: 'append' }] }, 'Unknown header operation'], [{ ...valid, headers: [{ ...edit, name: 7 }] }, 'Invalid rule: headers'], [{ ...valid, headers: [{ ...edit, value: null }] }, 'Invalid rule: headers'], - [{ ...valid, headers: Array(MAX_HEADER_EDITS + 1).fill(edit) }, 'Invalid rule: headers'], + [{ ...valid, headers: [{ ...edit, name: 'Set-Cookie' }] }, "Set-Cookie can't be changed: the browser stores cookies before a rule runs"], + [{ ...valid, headers: Array(MAX_HEADER_EDITS + 1).fill(edit) }, `A rule makes at most ${MAX_HEADER_EDITS} header changes`], ]; - for (const [input, message] of cases) expect(() => sanitizeRuleInput(input), JSON.stringify(input)).toThrow(message); + for (const [input, message] of cases) expect(() => parseRule(input), JSON.stringify(input)).toThrow(message); }); }); -describe('sanitizeRulePatch (IPC)', () => { +describe('reading a rule edit (IPC)', () => { it('keeps only the fields present, as fresh copies', () => { - expect(sanitizeRulePatch({})).toEqual({}); - expect(sanitizeRulePatch({ enabled: false, action: 'block', id: 'x' })).toEqual({ enabled: false }); - const patch = sanitizeRulePatch({ match: { ...match, extra: 1 }, resourceTypes: ['Ping', 'Image'], headers: [edit] }); + expect(parsePatch({})).toEqual({}); + expect(parsePatch({ enabled: false, action: 'block', id: 'x' })).toEqual({ enabled: false }); + const patch = parsePatch({ match: { ...match, extra: 1 }, resourceTypes: ['Ping', 'Image'], headers: [edit] }); expect(patch).toEqual({ match, resourceTypes: ['Image', 'Ping'], headers: [edit] }); - expect(Object.keys(sanitizeRulePatch({ match }))).toEqual(['match']); + expect(Object.keys(parsePatch({ match }))).toEqual(['match']); }); - it('names the field of the wrong shape', () => { - expect(() => sanitizeRulePatch('x')).toThrow('Invalid rule: not an object'); - expect(() => sanitizeRulePatch({ enabled: 'yes' })).toThrow('Invalid rule: enabled'); - expect(() => sanitizeRulePatch({ match: null })).toThrow('Invalid rule: match'); - expect(() => sanitizeRulePatch({ resourceTypes: ['Nope'] })).toThrow('Invalid rule: resourceTypes'); - expect(() => sanitizeRulePatch({ headers: {} })).toThrow('Invalid rule: headers'); + it('names the field of the wrong shape, and refuses a field given as undefined', () => { + expect(() => parsePatch('x')).toThrow('Invalid rule: not an object'); + expect(() => parsePatch({ enabled: 'yes' })).toThrow('Invalid rule: enabled'); + expect(() => parsePatch({ enabled: undefined })).toThrow('Invalid rule: enabled'); + expect(() => parsePatch({ match: null })).toThrow('Invalid rule: match'); + expect(() => parsePatch({ resourceTypes: ['Nope'] })).toThrow('Unknown request type'); + expect(() => parsePatch({ headers: {} })).toThrow('Invalid rule: headers'); + expect(() => parsePatch({ headers: [] })).toThrow('Add at least one header change'); }); }); -describe('field sanitizers', () => { - it('return null for anything of the wrong shape', () => { - expect(sanitizeMatcher(match)).toEqual(match); - expect(sanitizeMatcher(undefined)).toBeNull(); - expect(sanitizeResourceTypes([])).toEqual([]); - expect(sanitizeResourceTypes(undefined)).toBeNull(); - expect(sanitizeHeaderEdits([])).toEqual([]); - expect(sanitizeHeaderEdits([null])).toBeNull(); +describe('request types', () => { + it('are known ones only, in order, once each', () => { + expect(resourceTypesSchema.parse([])).toEqual([]); + expect(resourceTypesSchema.safeParse(undefined).success).toBe(false); + expect(resourceTypesSchema.parse(['Other', 'Document', 'Other'])).toEqual(['Document', 'Other']); }); }); diff --git a/test/unit/ruleValidation.test.ts b/test/unit/ruleValidation.test.ts index 33870939..cbe35f1d 100644 --- a/test/unit/ruleValidation.test.ts +++ b/test/unit/ruleValidation.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from 'vitest'; -import { compareRuleAge, sameRuleInput, validateHeaderEdit, validateRuleInput } from '../../src/shared/rules'; +import { compareRuleAge, headerEditSchema, ruleInputSchema, sameRuleInput } from '../../src/shared/rules'; import type { CreateRuleInput, HeaderEdit } from '../../src/shared/types'; +import { firstIssue } from '../../src/shared/validation'; + +// What the form, the IPC handlers and the stores show: the first problem, in field order. +const validateRuleInput = (input: CreateRuleInput) => firstIssue(ruleInputSchema, input); +const validateHeaderEdit = (edit: HeaderEdit) => firstIssue(headerEditSchema, edit); const match = { type: 'exact' as const, pattern: 'https://a.com/app.js', ignoreQuery: true }; const block: CreateRuleInput = { action: 'block', match, resourceTypes: [] }; diff --git a/test/unit/storeTimeline.test.ts b/test/unit/storeTimeline.test.ts index 8f28f195..f3bccd4b 100644 --- a/test/unit/storeTimeline.test.ts +++ b/test/unit/storeTimeline.test.ts @@ -86,7 +86,8 @@ describe('recording store actions (main)', () => { expect(install!.params).toEqual({ source: HOOKS_SOURCE }); expect(HOOKS_SOURCE).toContain('__REDUX_DEVTOOLS_EXTENSION_COMPOSE__'); expect(HOOKS_SOURCE).toContain('__REACT_DEVTOOLS_GLOBAL_HOOK__'); - // The script is valid JavaScript. + // The script is valid JavaScript: compiled, not run. + // oxlint-disable-next-line typescript/no-implied-eval -- compiling the script is the check expect(() => new Function(HOOKS_SOURCE)).not.toThrow(); }); diff --git a/test/unit/workspaceController.test.ts b/test/unit/workspaceController.test.ts index 7c75b159..e8de2b63 100644 --- a/test/unit/workspaceController.test.ts +++ b/test/unit/workspaceController.test.ts @@ -95,7 +95,7 @@ describe('WorkspaceController', () => { await override('https://a.com/a.js'); const second = await workspaces.create(); vi.spyOn(session, 'setActive').mockImplementationOnce(function (this: SessionStore, id: unknown) { - SessionStore.prototype.setActive.call(this, id); + void SessionStore.prototype.setActive.call(this, id); return Promise.reject(new Error('disk full')); }); await expect(workspaces.switchTo(second.id)).rejects.toThrow('disk full');